Skip to content

Argon2id runs on a tokio worker and is re-derived on every vault operation #51

Description

@Sadykhzadeh

Tested against main (160b096).

What happens

vault.rs:189 is a plain synchronous Argon2id call:

fn derive_key(passphrase: &str, salt: &[u8]) -> Result<[u8; 32]> {
    let params = Params::new(64 * 1024, 3, 4, Some(32))   // 64 MiB, t=3, p=4

Every call site is inside an async fn, and none of them offloads it:

grep -rn "spawn_blocking\|block_in_place\|block_on" core/src desktop/src-tauri/src returns three hits (biometric.rs:13, biometric_commands.rs:31, vault_keychain_cleanup.rs:260) and all three wrap Keychain calls. The KDF is never wrapped, so each derivation parks a tokio worker for its whole duration.

Nothing caches the result either, so add_key (vault.rs:113-124) and remove_key (:127-140) each run it twice — once to open the payload, once to re-seal it.

Measured

AMD Ryzen 5 9600X (6C/12T), Windows 11, release profile (opt-level = "s", lto = true — the profile the app ships with). Medians.

before after
one derivation, production params 102 ms 107 ms (unchanged work, now on a blocking thread)
get_key on the SSH connect path 109 ms 0.003 ms
add_key / remove_key 2 derivations ≈ 205 ms 1.5 ms
unrelated command queued behind a derivation on the same worker 107 ms 0.0 ms

That last row is a direct measurement, not an inference: a 1-worker tokio runtime, one task running the KDF, a second task spawned right after that records how long it waited before its first poll. Inline it waits out the whole derivation; via spawn_blocking it runs immediately. Whether an end user perceives those 107 ms as a UI freeze depends on how many workers the Tauri runtime has and what else is queued — that part I did not measure, and I am not claiming it.

Debug builds are ~12x slower (1.20 s per derivation), which is worth knowing when profiling from tauri dev.

p=4 buys nothing here

argon2 is pulled without any parallelism support — 0.5.3 has no such feature at all:

$ cargo tree -p clavyn-core -e features -i argon2
argon2 v0.5.3
├── argon2 feature "alloc" ...
# available features: alloc, default, password-hash, rand, simple, std, zeroize

Measured, 7 runs each, same machine and profile: m=64MiB t=3 p=4 → 102.3 ms median; m=64MiB t=3 p=1 → 100.0 ms median. The four lanes are computed sequentially, so the defender pays the same wall clock either way while an attacker with four cores gets a 4x head start.

I did not change this, deliberately. The two ways to fix it are:

  • upgrade argon2 to a release with rayon-backed lanes — output-identical, so existing vaults keep opening, but it is a dependency bump that wants its own review; or
  • set p=1 and spend the budget on m_cost — strictly better cryptographically, but it changes every derived key, so it needs a versioned vault re-key migration or every existing vault becomes undecryptable.

Neither belongs in a latency fix. Filing it here so it is not lost. The current parameters (64 MiB, t=3) stay well above the OWASP 2024 floor of 19 MiB / t=2 in the meantime.

Suggested fix

  1. Make the KDF reachable only through async wrappers on Vault that spawn_blocking, so no synchronous path can reach it by accident.
  2. Cache the derived key for the unlocked session and invalidate it wherever the passphrase is invalidated.

PR to follow.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions