eidetica/account/
crypto.rs1use argon2::{Algorithm, Argon2, Params, Version, password_hash::SaltString};
7use zeroize::Zeroizing;
8
9use super::errors::AccountError;
10use crate::Result;
11
12pub const SALT_LENGTH: usize = 22;
14
15pub const NONCE_LENGTH: usize = 12;
17
18pub const KEY_LENGTH: usize = 32;
20
21pub 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
32pub 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 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)] fn test_generate_salt() {
105 let salt1 = generate_salt();
106 let salt2 = generate_salt();
107
108 assert_eq!(salt1.len(), SALT_LENGTH);
110 assert_eq!(salt2.len(), SALT_LENGTH);
111
112 assert_ne!(salt1, salt2);
114 }
115}