Skip to content

Instantly share code, notes, and snippets.

@frozolotl
Created April 25, 2026 08:55
Show Gist options
  • Select an option

  • Save frozolotl/66c32ccbd4106cf0f130006128a11a98 to your computer and use it in GitHub Desktop.

Select an option

Save frozolotl/66c32ccbd4106cf0f130006128a11a98 to your computer and use it in GitHub Desktop.
pskey brute force
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