1use std::sync::{Arc, RwLock};
8
9use crate::encryption::MasterKeyring;
10pub use crate::envelope::Passphrase;
11use crate::envelope::PassphraseVault;
12use crate::keys::{KeyError, MasterKeyCustody, StoreKeys};
13use crate::store_dir::StoreDir;
14
15pub enum KeyCustody {
19 Keyring,
21 Passphrase(Passphrase),
24 InMemory(MasterKeyring),
26 Custom(Arc<dyn MasterKeyCustody>),
28}
29
30impl KeyCustody {
31 pub fn resolve(self, store_id: &str, store_dir: &StoreDir) -> Arc<dyn MasterKeyCustody> {
37 match self {
38 KeyCustody::Keyring => Arc::new(KeyringCustody::new(store_id.to_string())),
39 KeyCustody::Passphrase(passphrase) => {
40 Arc::new(PassphraseCustody::new(passphrase, store_dir))
41 }
42 KeyCustody::InMemory(keyring) => Arc::new(InMemoryCustody::new(keyring)),
43 KeyCustody::Custom(custody) => custody,
44 }
45 }
46}
47
48struct KeyringCustody {
59 keys: StoreKeys,
60}
61
62impl KeyringCustody {
63 fn new(store_id: String) -> Self {
64 Self {
65 keys: StoreKeys::new(store_id),
66 }
67 }
68}
69
70impl MasterKeyCustody for KeyringCustody {
71 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
72 self.keys
73 .get_encryption_key()?
74 .map(|s| {
75 MasterKeyring::from_serialized(&s).map_err(|e| KeyError::Crypto(e.to_string()))
76 })
77 .transpose()
78 }
79
80 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
81 self.keys.set_encryption_key(&keyring.to_serialized())
82 }
83
84 fn forget(&self) -> Result<(), KeyError> {
85 self.keys.delete_encryption_key()
86 }
87}
88
89struct InMemoryCustody {
95 keyring: RwLock<Option<MasterKeyring>>,
96}
97
98impl InMemoryCustody {
99 fn new(seed: MasterKeyring) -> Self {
100 Self {
101 keyring: RwLock::new(Some(seed)),
102 }
103 }
104}
105
106impl MasterKeyCustody for InMemoryCustody {
107 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
108 Ok(self.keyring.read().unwrap().clone())
109 }
110
111 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
112 *self.keyring.write().unwrap() = Some(keyring.clone());
113 Ok(())
114 }
115
116 fn forget(&self) -> Result<(), KeyError> {
117 *self.keyring.write().unwrap() = None;
118 Ok(())
119 }
120}
121
122struct PassphraseCustody {
130 vault: PassphraseVault,
131}
132
133impl PassphraseCustody {
134 fn new(passphrase: Passphrase, store_dir: &StoreDir) -> Self {
135 Self {
136 vault: PassphraseVault::new(passphrase, store_dir.join("master.keyring")),
137 }
138 }
139}
140
141impl MasterKeyCustody for PassphraseCustody {
142 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
143 let Some(plaintext) = self.vault.unlock()? else {
144 return Ok(None);
145 };
146 let serialized = String::from_utf8(plaintext)
147 .map_err(|e| KeyError::Crypto(format!("decrypted master keyring is not UTF-8: {e}")))?;
148 MasterKeyring::from_serialized(&serialized)
149 .map(Some)
150 .map_err(|e| KeyError::Crypto(e.to_string()))
151 }
152
153 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
154 self.vault.persist(keyring.to_serialized().as_bytes())
155 }
156
157 fn forget(&self) -> Result<(), KeyError> {
158 self.vault.forget()
159 }
160}
161
162pub fn rewrap_passphrase_custody(
170 store_dir: &StoreDir,
171 old: Passphrase,
172 new: &Passphrase,
173) -> Result<(), KeyError> {
174 PassphraseCustody::new(old, store_dir).vault.rewrap(new)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::encryption::EncryptionService;
181
182 fn temp_store_dir() -> (tempfile::TempDir, StoreDir) {
183 let tmp = tempfile::tempdir().expect("temp dir");
184 let dir = StoreDir::new(tmp.path());
185 (tmp, dir)
186 }
187
188 #[test]
193 fn keyring_preset_unlock_persist_forget_round_trip() {
194 crate::keys::test_keyring::install();
195 let custody = KeyringCustody::new("custody-keyring-roundtrip".to_string());
196
197 assert!(
198 custody.unlock().expect("unlock a fresh store").is_none(),
199 "a fresh store has no established keyring",
200 );
201
202 let keyring = MasterKeyring::generate();
203 custody.persist(&keyring).expect("persist");
204 let unlocked = custody
205 .unlock()
206 .expect("unlock after persist")
207 .expect("keyring is established");
208 assert_eq!(unlocked.fingerprint(), keyring.fingerprint());
209
210 custody.forget().expect("forget");
211 assert!(
212 custody.unlock().expect("unlock after forget").is_none(),
213 "forget removes the established keyring",
214 );
215 }
216
217 #[test]
223 fn keyring_preset_unlock_does_not_read_a_corrupt_empty_entry_as_absent() {
224 crate::keys::test_keyring::install();
225 let store_id = "custody-keyring-corrupt-empty".to_string();
226 let account = crate::keys::KeyringSlot::EncryptionMasterKey(store_id.clone()).account();
227 crate::keys::entry_for(&account)
228 .expect("create entry")
229 .set_password("")
230 .expect("write empty entry");
231
232 let custody = KeyringCustody::new(store_id);
233 let error = custody.unlock().expect_err("empty entry is corrupt");
234 assert!(error.to_string().contains("present but empty"));
235 }
236
237 #[test]
242 fn in_memory_preset_unlock_returns_the_seeded_keyring() {
243 let seed = MasterKeyring::generate();
244 let fingerprint = seed.fingerprint();
245 let custody = InMemoryCustody::new(seed);
246
247 let unlocked = custody
248 .unlock()
249 .expect("unlock")
250 .expect("seeded keyring is present");
251 assert_eq!(unlocked.fingerprint(), fingerprint);
252 }
253
254 #[test]
255 fn in_memory_preset_persist_replaces_and_forget_clears() {
256 let custody = InMemoryCustody::new(MasterKeyring::generate());
257
258 let rotated = MasterKeyring::generate();
259 custody.persist(&rotated).expect("persist");
260 assert_eq!(
261 custody.unlock().unwrap().unwrap().fingerprint(),
262 rotated.fingerprint(),
263 );
264
265 custody.forget().expect("forget");
266 assert!(custody.unlock().unwrap().is_none());
267 }
268
269 #[test]
270 fn in_memory_preset_never_writes_under_the_store_dir() {
271 let (tmp, _dir) = temp_store_dir();
272 let custody = InMemoryCustody::new(MasterKeyring::generate());
273 custody
274 .persist(&MasterKeyring::generate())
275 .expect("persist");
276 custody.forget().expect("forget");
277
278 let entries: Vec<_> = std::fs::read_dir(tmp.path())
279 .expect("read store dir")
280 .collect();
281 assert!(
282 entries.is_empty(),
283 "InMemory custody must touch no file under the store dir",
284 );
285 }
286
287 #[test]
292 fn passphrase_preset_establish_then_unlock_round_trips() {
293 let (_tmp, dir) = temp_store_dir();
294 let custody = PassphraseCustody::new(
295 Passphrase::new("correct horse battery staple".to_string()),
296 &dir,
297 );
298
299 assert!(custody.unlock().expect("unlock before establish").is_none());
300
301 let keyring = MasterKeyring::generate();
302 custody.persist(&keyring).expect("establish");
303 let unlocked = custody
304 .unlock()
305 .expect("unlock after establish")
306 .expect("keyring is established");
307 assert_eq!(unlocked.fingerprint(), keyring.fingerprint());
308 }
309
310 #[test]
311 fn passphrase_preset_wrong_passphrase_is_err_not_none() {
312 let (_tmp, dir) = temp_store_dir();
313 let writer = PassphraseCustody::new(Passphrase::new("right passphrase".to_string()), &dir);
314 writer
315 .persist(&MasterKeyring::generate())
316 .expect("establish");
317
318 let reader = PassphraseCustody::new(Passphrase::new("wrong passphrase".to_string()), &dir);
319 let error = reader
320 .unlock()
321 .expect_err("wrong passphrase must not unlock");
322 assert!(
323 error.to_string().to_lowercase().contains("passphrase")
324 || matches!(error, KeyError::Crypto(_))
325 );
326 }
327
328 #[test]
329 fn passphrase_preset_missing_file_is_none() {
330 let (_tmp, dir) = temp_store_dir();
331 let custody = PassphraseCustody::new(Passphrase::new("unused".to_string()), &dir);
332 assert!(custody.unlock().expect("unlock with no file").is_none());
333 }
334
335 const V1_FIXTURE_PASSPHRASE: &str = "fixture-passphrase";
347 const V1_FIXTURE_ENVELOPE_JSON: &str = concat!(
348 r#"{"v":1,"kdf":{"algo":"argon2id","m_cost":65536,"t_cost":3,"p_cost":4,"#,
349 r#""salt_b64":"3q2+7wEjRWeJq83vABEiMw=="},"#,
350 r#""nonce_b64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYX","#,
351 r#""ciphertext_b64":"+7/Z7TSK5xtqL6fqDzh5ayBkPPtuzf/0FyBy3mrgtiFjfabWOqVb8FonvR7SwvntJd9ERnTDljuE0o3Ofzs8a6XMbf0VJ6HlDp2aB62apAV3Fv1e1eb8su6/TxVOCskR9cvDmPr2P3CnsX3YRaGcEuilgSW8uKosW6gowDDZHqGat1XSPbnG02GO5QYuMxM="}"#
352 );
353
354 #[test]
359 fn rewrap_passphrase_custody_moves_the_master_keyring_to_the_new_passphrase() {
360 let (_tmp, dir) = temp_store_dir();
361 let established = PassphraseCustody::new(Passphrase::new("old".to_string()), &dir);
362 let keyring = MasterKeyring::generate();
363 established.persist(&keyring).expect("establish");
364
365 rewrap_passphrase_custody(
366 &dir,
367 Passphrase::new("old".to_string()),
368 &Passphrase::new("new".to_string()),
369 )
370 .expect("re-wrap under the new passphrase");
371
372 let with_old = PassphraseCustody::new(Passphrase::new("old".to_string()), &dir);
373 assert!(
374 with_old.unlock().is_err(),
375 "the old passphrase must no longer unlock after a re-wrap",
376 );
377 let with_new = PassphraseCustody::new(Passphrase::new("new".to_string()), &dir);
378 assert_eq!(
379 with_new
380 .unlock()
381 .expect("the new passphrase unlocks")
382 .expect("keyring present")
383 .fingerprint(),
384 keyring.fingerprint(),
385 );
386 }
387
388 #[test]
389 fn passphrase_preset_envelope_fixture_v1_unlocks() {
390 let (_tmp, dir) = temp_store_dir();
391 std::fs::write(dir.join("master.keyring"), V1_FIXTURE_ENVELOPE_JSON)
392 .expect("write fixture envelope");
393
394 let custody =
395 PassphraseCustody::new(Passphrase::new(V1_FIXTURE_PASSPHRASE.to_string()), &dir);
396 let keyring = custody
397 .unlock()
398 .expect("the v1 fixture must still unlock")
399 .expect("the fixture names an established keyring");
400 assert_eq!(
401 keyring.fingerprint(),
402 EncryptionService::from_key([0x11u8; 32]).fingerprint(),
403 );
404 }
405}