solo_storage/
key_material.rs1use argon2::{Algorithm, Argon2, Params, Version};
16use solo_core::{Error, Result};
17use zeroize::Zeroizing;
18
19pub const ARGON2_M_COST_KIB: u32 = 64 * 1024;
21pub const ARGON2_T_COST: u32 = 3;
23pub const ARGON2_P_COST: u32 = 4;
25pub const KEY_LEN: usize = 32;
27pub const SALT_LEN: usize = 16;
30
31pub struct KeyMaterial {
37 raw: Zeroizing<[u8; KEY_LEN]>,
38}
39
40impl KeyMaterial {
41 pub fn derive(passphrase: &str, salt: &[u8; SALT_LEN]) -> Result<Self> {
46 let params = Params::new(
47 ARGON2_M_COST_KIB,
48 ARGON2_T_COST,
49 ARGON2_P_COST,
50 Some(KEY_LEN),
51 )
52 .map_err(|e| Error::storage(format!("argon2 params: {e}")))?;
53 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
54
55 let mut out: Zeroizing<[u8; KEY_LEN]> = Zeroizing::new([0u8; KEY_LEN]);
56 argon2
57 .hash_password_into(passphrase.as_bytes(), salt, out.as_mut())
58 .map_err(|e| Error::storage(format!("argon2 hash: {e}")))?;
59 Ok(Self { raw: out })
60 }
61
62 pub fn fresh_salt() -> Result<[u8; SALT_LEN]> {
65 let mut salt = [0u8; SALT_LEN];
66 getrandom::getrandom(&mut salt)
67 .map_err(|e| Error::storage(format!("getrandom: {e}")))?;
68 Ok(salt)
69 }
70
71 pub fn as_hex(&self) -> Zeroizing<String> {
84 Zeroizing::new(hex::encode(self.raw.as_ref()))
85 }
86
87 #[cfg(test)]
90 fn eq_for_test(&self, other: &Self) -> bool {
91 self.raw.as_ref() == other.raw.as_ref()
92 }
93}
94
95impl Clone for KeyMaterial {
96 fn clone(&self) -> Self {
97 Self {
98 raw: Zeroizing::new(*self.raw),
99 }
100 }
101}
102
103impl std::fmt::Debug for KeyMaterial {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.write_str("KeyMaterial { raw: <redacted> }")
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn derive_fast(passphrase: &str, salt: &[u8; SALT_LEN]) -> Result<KeyMaterial> {
116 let params = Params::new(8, 1, 1, Some(KEY_LEN))
117 .map_err(|e| Error::storage(format!("params: {e}")))?;
118 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
119 let mut out: Zeroizing<[u8; KEY_LEN]> = Zeroizing::new([0u8; KEY_LEN]);
120 argon2
121 .hash_password_into(passphrase.as_bytes(), salt, out.as_mut())
122 .map_err(|e| Error::storage(format!("hash: {e}")))?;
123 Ok(KeyMaterial { raw: out })
124 }
125
126 #[test]
127 fn derive_is_deterministic_with_same_salt() {
128 let salt = [0u8; SALT_LEN];
129 let a = derive_fast("hunter2", &salt).unwrap();
130 let b = derive_fast("hunter2", &salt).unwrap();
131 assert!(a.eq_for_test(&b));
132 assert_eq!(&*a.as_hex(), &*b.as_hex());
133 }
134
135 #[test]
136 fn derive_differs_with_different_salt() {
137 let s1 = [0u8; SALT_LEN];
138 let mut s2 = [0u8; SALT_LEN];
139 s2[0] = 1;
140 let a = derive_fast("hunter2", &s1).unwrap();
141 let b = derive_fast("hunter2", &s2).unwrap();
142 assert!(!a.eq_for_test(&b));
143 }
144
145 #[test]
146 fn derive_differs_with_different_passphrase() {
147 let salt = [0u8; SALT_LEN];
148 let a = derive_fast("hunter2", &salt).unwrap();
149 let b = derive_fast("hunter3", &salt).unwrap();
150 assert!(!a.eq_for_test(&b));
151 }
152
153 #[test]
154 fn fresh_salt_is_random() {
155 let s1 = KeyMaterial::fresh_salt().unwrap();
156 let s2 = KeyMaterial::fresh_salt().unwrap();
157 assert_ne!(s1, s2);
160 }
161
162 #[test]
163 fn as_hex_has_correct_length_and_charset() {
164 let salt = [0u8; SALT_LEN];
165 let k = derive_fast("hunter2", &salt).unwrap();
166 let h = k.as_hex();
167 assert_eq!(h.len(), KEY_LEN * 2);
168 assert!(h.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
169 }
170
171 #[test]
172 fn debug_redacts_key_material() {
173 let salt = [0u8; SALT_LEN];
174 let k = derive_fast("hunter2", &salt).unwrap();
175 let dbg = format!("{k:?}");
176 assert!(dbg.contains("redacted"));
177 assert!(!dbg.contains(&k.as_hex()[..8]));
178 }
179}