1use argon2::Argon2;
13use zeroize::{Zeroize, Zeroizing};
14
15use crate::crypto::KEY_LEN;
16use crate::error::CoreError;
17
18pub struct MasterKey([u8; KEY_LEN]);
24
25impl MasterKey {
26 pub fn new(bytes: [u8; KEY_LEN]) -> Self {
28 Self(bytes)
29 }
30
31 pub fn expose(&self) -> &[u8; KEY_LEN] {
33 &self.0
34 }
35}
36
37impl Drop for MasterKey {
38 fn drop(&mut self) {
39 self.0.zeroize();
40 }
41}
42
43impl core::fmt::Debug for MasterKey {
45 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46 f.write_str("MasterKey(REDACTED)")
47 }
48}
49
50pub trait Keyring {
53 fn get_master_key(&self) -> Result<MasterKey, CoreError>;
56
57 fn set_master_key(&self, key: &MasterKey) -> Result<(), CoreError>;
61}
62
63#[derive(Default)]
65pub struct MockKeyring {
66 key: std::sync::Mutex<Option<[u8; KEY_LEN]>>,
67}
68
69impl MockKeyring {
70 pub fn empty() -> Self {
72 Self::default()
73 }
74
75 pub fn with_key(bytes: [u8; KEY_LEN]) -> Self {
77 Self {
78 key: std::sync::Mutex::new(Some(bytes)),
79 }
80 }
81}
82
83impl Keyring for MockKeyring {
84 fn get_master_key(&self) -> Result<MasterKey, CoreError> {
85 self.key
86 .lock()
87 .expect("mock keyring mutex poisoned")
88 .map(MasterKey::new)
89 .ok_or_else(|| CoreError::Keyring("no master key set".to_string()))
90 }
91
92 fn set_master_key(&self, key: &MasterKey) -> Result<(), CoreError> {
93 *self.key.lock().expect("mock keyring mutex poisoned") = Some(*key.expose());
94 Ok(())
95 }
96}
97
98pub struct OsKeyring {
103 service: String,
104 user: String,
105}
106
107impl OsKeyring {
108 pub fn new() -> Self {
111 Self {
112 service: "kovra".to_string(),
113 user: "master-key".to_string(),
114 }
115 }
116}
117
118impl Default for OsKeyring {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl Keyring for OsKeyring {
125 fn get_master_key(&self) -> Result<MasterKey, CoreError> {
126 let entry = keyring::Entry::new(&self.service, &self.user)
127 .map_err(|e| CoreError::Keyring(e.to_string()))?;
128 let secret = entry
129 .get_secret()
130 .map_err(|e| CoreError::Keyring(e.to_string()))?;
131 let bytes: [u8; KEY_LEN] = secret
132 .as_slice()
133 .try_into()
134 .map_err(|_| CoreError::Keyring("stored key has wrong length".to_string()))?;
135 Ok(MasterKey::new(bytes))
136 }
137
138 fn set_master_key(&self, key: &MasterKey) -> Result<(), CoreError> {
139 let entry = keyring::Entry::new(&self.service, &self.user)
140 .map_err(|e| CoreError::Keyring(e.to_string()))?;
141 entry
142 .set_secret(key.expose())
143 .map_err(|e| CoreError::Keyring(e.to_string()))
144 }
145}
146
147pub struct Argon2Keyring {
152 passphrase: Zeroizing<Vec<u8>>,
153 salt: Vec<u8>,
154}
155
156pub const MIN_SALT_LEN: usize = 8;
158
159impl Argon2Keyring {
160 pub fn new(
164 passphrase: impl Into<Vec<u8>>,
165 salt: impl Into<Vec<u8>>,
166 ) -> Result<Self, CoreError> {
167 let salt = salt.into();
168 if salt.len() < MIN_SALT_LEN {
169 return Err(CoreError::Keyring(format!(
170 "salt must be at least {MIN_SALT_LEN} bytes"
171 )));
172 }
173 Ok(Self {
174 passphrase: Zeroizing::new(passphrase.into()),
175 salt,
176 })
177 }
178}
179
180const ARGON2_STACK_BYTES: usize = 8 * 1024 * 1024;
187
188impl Keyring for Argon2Keyring {
189 fn get_master_key(&self) -> Result<MasterKey, CoreError> {
190 let derive = || {
191 let mut key = [0u8; KEY_LEN];
192 let res = Argon2::default()
193 .hash_password_into(&self.passphrase, &self.salt, &mut key)
194 .map_err(|e| CoreError::Keyring(e.to_string()));
195 let out = res.map(|()| MasterKey::new(key));
196 key.zeroize();
197 out
198 };
199 std::thread::scope(|s| {
202 std::thread::Builder::new()
203 .stack_size(ARGON2_STACK_BYTES)
204 .spawn_scoped(s, derive)
205 .map_err(|e| CoreError::Keyring(format!("spawning Argon2 worker: {e}")))?
206 .join()
207 .map_err(|_| CoreError::Keyring("Argon2 worker panicked".to_string()))?
208 })
209 }
210
211 fn set_master_key(&self, _key: &MasterKey) -> Result<(), CoreError> {
212 Err(CoreError::Keyring(
213 "passphrase-derived key cannot be stored; it is recomputed from the passphrase"
214 .to_string(),
215 ))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn mock_keyring_round_trips() {
225 let kr = MockKeyring::empty();
226 assert!(kr.get_master_key().is_err());
227 kr.set_master_key(&MasterKey::new([5u8; KEY_LEN])).unwrap();
228 assert_eq!(kr.get_master_key().unwrap().expose(), &[5u8; KEY_LEN]);
229 }
230
231 #[test]
232 fn mock_with_key_seeds_value() {
233 let kr = MockKeyring::with_key([9u8; KEY_LEN]);
234 assert_eq!(kr.get_master_key().unwrap().expose(), &[9u8; KEY_LEN]);
235 }
236
237 #[test]
238 fn master_key_debug_is_redacted() {
239 let mk = MasterKey::new([1u8; KEY_LEN]);
240 assert_eq!(format!("{mk:?}"), "MasterKey(REDACTED)");
241 }
242
243 #[test]
244 fn argon2_is_deterministic_for_same_inputs() {
245 let a =
246 Argon2Keyring::new(b"correct horse".to_vec(), b"stable-salt-1234".to_vec()).unwrap();
247 let b =
248 Argon2Keyring::new(b"correct horse".to_vec(), b"stable-salt-1234".to_vec()).unwrap();
249 assert_eq!(
250 a.get_master_key().unwrap().expose(),
251 b.get_master_key().unwrap().expose()
252 );
253 }
254
255 #[test]
256 fn argon2_differs_for_different_passphrase() {
257 let salt = b"stable-salt-1234".to_vec();
258 let a = Argon2Keyring::new(b"passphrase-a".to_vec(), salt.clone()).unwrap();
259 let b = Argon2Keyring::new(b"passphrase-b".to_vec(), salt).unwrap();
260 assert_ne!(
261 a.get_master_key().unwrap().expose(),
262 b.get_master_key().unwrap().expose()
263 );
264 }
265
266 #[test]
267 fn argon2_rejects_short_salt() {
268 assert!(matches!(
269 Argon2Keyring::new(b"pw".to_vec(), b"short".to_vec()),
270 Err(CoreError::Keyring(_))
271 ));
272 }
273
274 #[test]
275 fn argon2_key_is_not_settable() {
276 let kr = Argon2Keyring::new(b"pw".to_vec(), b"stable-salt-1234".to_vec()).unwrap();
277 assert!(kr.set_master_key(&MasterKey::new([0u8; KEY_LEN])).is_err());
278 }
279}