Skip to content

[HIGH] KeyManager::from_password derives AES key with single SHA-256 (broken KDF; no salt, no work factor) #40

Description

@Alqku

Overview

KeyManager::from_password derives the 32-byte AES-256 master key by feeding the user's password through a single round of SHA-256 with no salt and no work factor (crates/tools/src/key_manager.rs lines 52–62). This is a known-broken key-derivation function: an attacker who obtains an encrypted vault file can recover the password from a candidate list at the cost of a single SHA-256 per guess, with no per-user salt to slow rainbow-table attacks. Open issue #12 covers a DIFFERENT SHA-256 misuse (server-side transaction signing) — this issue is specific to the vault's master-key derivation.

Evidence

// crates/tools/src/key_manager.rs (KeyManager::from_password)
pub fn from_password(password: &str) -> Result<Self> {
    let mut hasher = Sha256::new();
    hasher.update(password.as_bytes());
    let key_bytes = hasher.finalize();

    let mut master_key = [0u8; 32];
    master_key.copy_from_slice(&key_bytes);

    Ok(Self { master_key })
}

The downstream call site EncryptedVault::with_password and the keymanager encrypt / decrypt / init-vault CLI subcommands (main.rs) all use this as the password-to-key path, meaning every secret in the encrypted vault (admin secret key, issuing secret key, etc.) is protected by a single SHA-256 hash of the operator password.

Impact

  • An operator who chooses hunter2 (or any word from a 100k-word dictionary) is vulnerable to an offline dictionary attack that costs roughly one sha256() invocation per guess — on the order of microseconds on commodity hardware, and trivially parallelisable on GPUs (≈10^10 guesses/s).
  • Two operators with the same password derive identical master keys (no salt), so a vault for one operator is brute-forceable against any cracker's existing dictionary.
  • This contradicts the AES-256-GCM choice upstream: the encryption itself is sound, but the key derivation has effectively no security.

Recommended Approach

Replace SHA-256 with a memory-hard password-based KDF that is the de-facto standard in 2026 Rust crates:

use argon2::{Algorithm, Argon2, Params, Version};

pub fn from_password(password: &str) -> Result<Self> {
    // Random per-vault salt: callers persist it alongside the vault file
    // (e.g. <VAULT_MASTER_SALT> in .env, or a leading line in the vault file).
    let salt: [u8; 16] = rand::random();
    let params = Params::new(19_456 /* KiB */, 2 /* iterations */, 1 /* lanes */, Some(32))
        .map_err(|e| anyhow::anyhow!("argon2 params: {e}"))?;
    let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
    let mut key = [0u8; 32];
    argon.hash_password_into(password.as_bytes(), &salt, &mut key)
        .map_err(|e| anyhow::anyhow!("argon2 derive: {e}"))?;
    Ok(Self { master_key: key })
    // `salt` should be returned alongside the key or persisted by the caller.
}

Then:

  1. Plumb the salt through EncryptedVault::with_password so it is stored next to the encrypted file (e.g. as a leading SALT=… line, parsed by EncryptedVault::load_from_file).
  2. Bump the Cargo.toml of crates/tools to depend on argon2 = "0.5".
  3. Document the migration: existing vault files encrypted under SHA-256 must be re-encrypted under Argon2id (one-shot script or auto-detect), otherwise decrypt will fail.

Acceptance Criteria

  • KeyManager::from_password derives the master key via Argon2id (or scrypt) with a random per-instance salt
  • EncryptedVault::with_password and EncryptedVault::load_from_file round-trip the salt
  • Salt length and KDF parameters are validated on load
  • Unit test: round-trip encrypt_key / decrypt_key still passes after the swap
  • Testnet workflow (CLI subcommands keymanager encrypt/decrypt/init-vault) produces working vaults under Argon2id
  • SECURITY.md (or equivalent) notes the migration
  • Old SHA-256-derived vaults either fail explicitly with a clear "please re-encrypt" message or are detected and upgraded transparently

Affected Files

  • crates/tools/src/key_manager.rs
  • crates/tools/src/encrypted_vault.rs
  • crates/tools/src/main.rs
  • crates/tools/Cargo.toml

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignbugSomething isn't workingrefactoringCode restructuring without behavioral changesecuritySecurity vulnerability or hardening

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions