huddle_core/storage/
keychain.rs1use std::fs;
13use std::path::PathBuf;
14
15use argon2::{Algorithm, Argon2, Params, Version};
16use hkdf::Hkdf;
17use rand::RngCore;
18use sha2::Sha256;
19
20use crate::config;
21use crate::error::{HuddleError, Result};
22
23pub const MASTER_KEY_LEN: usize = 32;
24pub const KEYCHAIN_SALT_LEN: usize = 16;
25
26pub fn keychain_salt_path() -> PathBuf {
29 config::data_dir().join("keychain.salt")
30}
31
32pub fn load_or_create_salt() -> Result<[u8; KEYCHAIN_SALT_LEN]> {
34 let path = keychain_salt_path();
35 if let Ok(bytes) = fs::read(&path) {
36 if bytes.len() == KEYCHAIN_SALT_LEN {
37 let mut out = [0u8; KEYCHAIN_SALT_LEN];
38 out.copy_from_slice(&bytes);
39 return Ok(out);
40 }
41 }
42 config::ensure_data_dir()?;
43 let mut salt = [0u8; KEYCHAIN_SALT_LEN];
44 rand::thread_rng().fill_bytes(&mut salt);
45 fs::write(&path, salt).map_err(|e| HuddleError::Other(format!("write salt: {e}")))?;
46 Ok(salt)
47}
48
49pub fn derive_master_key(
54 passphrase: &str,
55 salt: &[u8; KEYCHAIN_SALT_LEN],
56) -> Result<[u8; MASTER_KEY_LEN]> {
57 let params = Params::new(65_536, 3, 4, Some(MASTER_KEY_LEN))
58 .map_err(|e| HuddleError::Other(format!("argon2 params: {e}")))?;
59 let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
60 let mut out = [0u8; MASTER_KEY_LEN];
61 argon
62 .hash_password_into(passphrase.as_bytes(), salt, &mut out)
63 .map_err(|e| HuddleError::Other(format!("argon2 derive: {e}")))?;
64 Ok(out)
65}
66
67pub fn derive_subkey(master_key: &[u8; MASTER_KEY_LEN], purpose: &[u8]) -> [u8; 32] {
72 let hk = Hkdf::<Sha256>::new(None, master_key);
73 let mut out = [0u8; 32];
74 hk.expand(purpose, &mut out)
75 .expect("32 bytes is well within HKDF-SHA256's output limit");
76 out
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn derive_is_deterministic() {
85 let salt = [42u8; KEYCHAIN_SALT_LEN];
86 let k1 = derive_master_key("hunter2", &salt).unwrap();
87 let k2 = derive_master_key("hunter2", &salt).unwrap();
88 assert_eq!(k1, k2);
89 }
90
91 #[test]
92 fn derive_differs_with_passphrase() {
93 let salt = [42u8; KEYCHAIN_SALT_LEN];
94 let k1 = derive_master_key("hunter2", &salt).unwrap();
95 let k2 = derive_master_key("hunter3", &salt).unwrap();
96 assert_ne!(k1, k2);
97 }
98
99 #[test]
100 fn subkeys_are_purpose_separated() {
101 let mk = [9u8; MASTER_KEY_LEN];
102 let a = derive_subkey(&mk, b"megolm-persist");
103 let b = derive_subkey(&mk, b"db-encryption");
104 assert_ne!(a, b);
105 }
106}