Skip to main content

envvault/crypto/
kdf.rs

1//! Password-based key derivation using Argon2id.
2//!
3//! Argon2id is a memory-hard KDF that protects against brute-force and
4//! GPU-based attacks.  Parameters are configurable via `Argon2Params`
5//! (loaded from `.envvault.toml` or sensible defaults).
6
7use argon2::{Algorithm, Argon2, Params, Version};
8use rand::TryRngCore;
9
10use crate::errors::{EnvVaultError, Result};
11
12/// Length of the salt in bytes (256 bits).
13const SALT_LEN: usize = 32;
14
15/// Length of the derived key in bytes (256 bits, for AES-256).
16const KEY_LEN: usize = 32;
17
18/// Configurable Argon2id parameters.
19///
20/// These map 1:1 to the fields in `Settings` so the CLI can pass
21/// whatever the user configured in `.envvault.toml`.
22#[derive(Debug, Clone, Copy)]
23pub struct Argon2Params {
24    /// Memory cost in KiB (default: 65 536 = 64 MB).
25    pub memory_kib: u32,
26    /// Number of iterations (default: 3).
27    pub iterations: u32,
28    /// Parallelism lanes (default: 4).
29    pub parallelism: u32,
30}
31
32impl Default for Argon2Params {
33    fn default() -> Self {
34        Self {
35            memory_kib: 65_536,
36            iterations: 3,
37            parallelism: 4,
38        }
39    }
40}
41
42/// Derive a 32-byte master key from a password and salt using Argon2id.
43///
44/// Uses the default Argon2id parameters (64 MB, 3 iterations, 4 lanes).
45/// Prefer `derive_master_key_with_params` when you have a `Settings`.
46pub fn derive_master_key(password: &[u8], salt: &[u8]) -> Result<[u8; KEY_LEN]> {
47    derive_master_key_with_params(password, salt, &Argon2Params::default())
48}
49
50/// Minimum safe memory cost in KiB (8 MB).
51const MIN_MEMORY_KIB: u32 = 8_192;
52
53/// Derive a 32-byte master key with explicit Argon2id parameters.
54///
55/// The same password + salt + params will always produce the same key.
56/// Enforces minimum Argon2 parameters to prevent dangerously weak KDF settings.
57pub fn derive_master_key_with_params(
58    password: &[u8],
59    salt: &[u8],
60    argon2_params: &Argon2Params,
61) -> Result<[u8; KEY_LEN]> {
62    if argon2_params.memory_kib < MIN_MEMORY_KIB {
63        return Err(EnvVaultError::KeyDerivationFailed(format!(
64            "Argon2 memory_kib must be at least {MIN_MEMORY_KIB} (got {})",
65            argon2_params.memory_kib
66        )));
67    }
68    if argon2_params.iterations < 1 {
69        return Err(EnvVaultError::KeyDerivationFailed(
70            "Argon2 iterations must be at least 1".into(),
71        ));
72    }
73    if argon2_params.parallelism < 1 {
74        return Err(EnvVaultError::KeyDerivationFailed(
75            "Argon2 parallelism must be at least 1".into(),
76        ));
77    }
78
79    let params = Params::new(
80        argon2_params.memory_kib,
81        argon2_params.iterations,
82        argon2_params.parallelism,
83        Some(KEY_LEN),
84    )
85    .map_err(|e| EnvVaultError::KeyDerivationFailed(format!("invalid Argon2 params: {e}")))?;
86
87    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
88
89    let mut key = [0u8; KEY_LEN];
90    argon2
91        .hash_password_into(password, salt, &mut key)
92        .map_err(|e| EnvVaultError::KeyDerivationFailed(format!("Argon2id hashing failed: {e}")))?;
93
94    Ok(key)
95}
96
97/// Generate a cryptographically random 32-byte salt.
98pub fn generate_salt() -> [u8; SALT_LEN] {
99    let mut salt = [0u8; SALT_LEN];
100    rand::rngs::OsRng
101        .try_fill_bytes(&mut salt)
102        .expect("OS RNG failed");
103    salt
104}