Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/account/
crypto.rs

1//! Cryptographic functions for account system
2//!
3//! Pinned Account Argon2id key derivation. Authenticated bundle wrapping lives
4//! in the credential module; no separate per-key storage format is retained.
5
6use argon2::{Algorithm, Argon2, Params, Version, password_hash::SaltString};
7use zeroize::Zeroizing;
8
9use super::errors::AccountError;
10use crate::Result;
11
12/// Salt string length for Argon2 (base64 encoded, 22 chars)
13pub const SALT_LENGTH: usize = 22;
14
15/// Nonce length for AES-GCM (12 bytes standard)
16pub const NONCE_LENGTH: usize = 12;
17
18/// Derived key length for AES-256 (32 bytes)
19pub const KEY_LENGTH: usize = 32;
20
21/// Generate a random salt string for Argon2id key derivation.
22///
23/// # Returns
24/// A base64-encoded salt string suitable for `derive_encryption_key`.
25pub fn generate_salt() -> String {
26    use argon2::password_hash::rand_core;
27    SaltString::generate(&mut rand_core::OsRng)
28        .as_str()
29        .to_string()
30}
31
32/// Derive an encryption key from a password and salt using Argon2id
33///
34/// # Arguments
35/// * `password` - The account's password
36/// * `salt` - The salt string (base64 encoded, from `generate_salt`)
37///
38/// # Returns
39/// A 32-byte encryption key suitable for AES-256
40pub fn derive_encryption_key(password: impl AsRef<str>, salt: impl AsRef<str>) -> Result<Vec<u8>> {
41    let salt_str = salt.as_ref();
42    if salt_str.len() != SALT_LENGTH {
43        return Err(AccountError::InvalidSaltLength {
44            expected: SALT_LENGTH,
45            actual: salt_str.len(),
46        }
47        .into());
48    }
49
50    let salt = SaltString::from_b64(salt_str).map_err(|e| AccountError::EncryptionFailed {
51        reason: format!("Invalid salt format: {e}"),
52    })?;
53
54    // Pin the existing Account format rather than inheriting future library
55    // defaults. PasswordStore has its own independently versioned KDF envelope.
56    let params = Params::new(19 * 1024, 2, 1, Some(KEY_LENGTH)).map_err(|_| {
57        AccountError::EncryptionFailed {
58            reason: "Invalid account KDF parameters".to_owned(),
59        }
60    })?;
61    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
62
63    let mut key = Zeroizing::new(vec![0u8; KEY_LENGTH]);
64    argon2
65        .hash_password_into(
66            password.as_ref().as_bytes(),
67            salt.as_str().as_bytes(),
68            &mut key,
69        )
70        .map_err(|e| AccountError::EncryptionFailed {
71            reason: format!("Key derivation failed: {e}"),
72        })?;
73
74    Ok(std::mem::take(&mut *key))
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    #[cfg_attr(miri, ignore)]
83    fn test_account_kdf_v1_keeps_existing_text_salt_convention() {
84        let salt = "AAAAAAAAAAAAAAAAAAAAAA";
85        let password = "synthetic fixture password";
86        let actual = derive_encryption_key(password, salt).unwrap();
87        let mut legacy = [0; KEY_LENGTH];
88        Argon2::default()
89            .hash_password_into(password.as_bytes(), salt.as_bytes(), &mut legacy)
90            .unwrap();
91        assert_eq!(actual, legacy);
92        let mut raw_salt = [0; KEY_LENGTH];
93        Argon2::default()
94            .hash_password_into(password.as_bytes(), &[0; 16], &mut raw_salt)
95            .unwrap();
96        assert_ne!(
97            actual, raw_salt,
98            "raw salt would silently change stored Account credentials"
99        );
100    }
101
102    #[test]
103    #[cfg_attr(miri, ignore)] // Argon2 is extremely slow under Miri
104    fn test_generate_salt() {
105        let salt1 = generate_salt();
106        let salt2 = generate_salt();
107
108        // Salts should be the correct length
109        assert_eq!(salt1.len(), SALT_LENGTH);
110        assert_eq!(salt2.len(), SALT_LENGTH);
111
112        // Salts should be different
113        assert_ne!(salt1, salt2);
114    }
115}