1use std::sync::Arc;
8
9use crate::encryption::MasterKeyring;
10pub use crate::envelope::Passphrase;
11use crate::keys::{KeyError, MasterKeyCustody, StoreKeys};
12use coven_foundation::store_dir::StoreDir;
13
14pub(crate) mod preset;
15use preset::{CustodySecret, InMemoryCustody, PassphraseCustody};
16
17pub enum KeyCustody {
21 Keyring,
23 Passphrase(Passphrase),
26 InMemory(MasterKeyring),
28 Custom(Arc<dyn MasterKeyCustody>),
30}
31
32impl KeyCustody {
33 pub fn resolve(
38 self,
39 store_keys: &StoreKeys,
40 store_dir: &StoreDir,
41 ) -> Arc<dyn MasterKeyCustody> {
42 match self {
43 KeyCustody::Keyring => Arc::new(KeyringCustody::new(store_keys.clone())),
44 KeyCustody::Passphrase(passphrase) => Arc::new(
45 PassphraseCustody::<MasterKeyring>::new(passphrase, store_dir),
46 ),
47 KeyCustody::InMemory(keyring) => Arc::new(InMemoryCustody::new(keyring)),
48 KeyCustody::Custom(custody) => custody,
49 }
50 }
51}
52
53struct KeyringCustody {
54 keys: StoreKeys,
55}
56
57impl KeyringCustody {
58 fn new(keys: StoreKeys) -> Self {
59 Self { keys }
60 }
61}
62
63impl MasterKeyCustody for KeyringCustody {
64 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
65 self.keys
66 .get_encryption_key()?
67 .map(|serialized| {
68 MasterKeyring::from_serialized(&serialized).map_err(KeyError::Encryption)
69 })
70 .transpose()
71 }
72
73 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
74 self.keys.set_encryption_key(&keyring.to_serialized())
75 }
76
77 fn forget(&self) -> Result<(), KeyError> {
78 self.keys.delete_encryption_key()
79 }
80}
81
82impl CustodySecret for MasterKeyring {
87 const FILE: &'static str = "master.keyring";
88
89 fn to_bytes(&self) -> Vec<u8> {
90 self.to_serialized().into_bytes()
91 }
92
93 fn from_bytes(bytes: Vec<u8>) -> Result<Self, KeyError> {
94 let serialized = String::from_utf8(bytes)?;
95 MasterKeyring::from_serialized(&serialized).map_err(KeyError::Encryption)
96 }
97}
98
99impl MasterKeyCustody for InMemoryCustody<MasterKeyring> {
100 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
101 InMemoryCustody::unlock(self)
102 }
103
104 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
105 InMemoryCustody::persist(self, keyring)
106 }
107
108 fn forget(&self) -> Result<(), KeyError> {
109 InMemoryCustody::forget(self)
110 }
111}
112
113impl MasterKeyCustody for PassphraseCustody<MasterKeyring> {
114 fn unlock(&self) -> Result<Option<MasterKeyring>, KeyError> {
115 PassphraseCustody::unlock(self)
116 }
117
118 fn persist(&self, keyring: &MasterKeyring) -> Result<(), KeyError> {
119 PassphraseCustody::persist(self, keyring)
120 }
121
122 fn forget(&self) -> Result<(), KeyError> {
123 PassphraseCustody::forget(self)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::encryption::EncryptionService;
131
132 fn temp_store_dir() -> (tempfile::TempDir, StoreDir) {
133 let tmp = tempfile::tempdir().expect("temp dir");
134 let dir = StoreDir::new_ephemeral(tmp.path());
135 (tmp, dir)
136 }
137
138 #[test]
143 fn keyring_preset_unlock_persist_forget_round_trip() {
144 crate::keys::test_keyring::install();
145 let store_keys = StoreKeys::bind("custody-keyring-roundtrip".to_string());
146 let custody = KeyCustody::Keyring.resolve(&store_keys, &StoreDir::new_ephemeral("unused"));
147
148 assert!(
149 custody.unlock().expect("unlock a fresh store").is_none(),
150 "a fresh store has no established keyring",
151 );
152
153 let keyring = MasterKeyring::generate();
154 custody.persist(&keyring).expect("persist");
155 let unlocked = custody
156 .unlock()
157 .expect("unlock after persist")
158 .expect("keyring is established");
159 assert_eq!(unlocked.fingerprint(), keyring.fingerprint());
160
161 custody.forget().expect("forget");
162 assert!(
163 custody.unlock().expect("unlock after forget").is_none(),
164 "forget removes the established keyring",
165 );
166 }
167
168 #[test]
174 fn keyring_preset_unlock_does_not_read_a_corrupt_empty_entry_as_absent() {
175 crate::keys::test_keyring::install();
176 let store_id = "custody-keyring-corrupt-empty".to_string();
177 let store_keys = StoreKeys::bind(store_id.clone());
178 store_keys
179 .write_empty_encryption_key_for_test()
180 .expect("write empty entry");
181
182 let custody = KeyCustody::Keyring.resolve(&store_keys, &StoreDir::new_ephemeral("unused"));
183 let error = custody.unlock().expect_err("empty entry is corrupt");
184 assert!(error.to_string().contains("present but empty"));
185 }
186
187 #[test]
192 fn in_memory_preset_unlock_returns_the_seeded_keyring() {
193 let seed = MasterKeyring::generate();
194 let fingerprint = seed.fingerprint();
195 let custody = InMemoryCustody::new(seed);
196
197 let unlocked = custody
198 .unlock()
199 .expect("unlock")
200 .expect("seeded keyring is present");
201 assert_eq!(unlocked.fingerprint(), fingerprint);
202 }
203
204 #[test]
205 fn in_memory_preset_persist_replaces_and_forget_clears() {
206 let custody = InMemoryCustody::new(MasterKeyring::generate());
207
208 let rotated = MasterKeyring::generate();
209 custody.persist(&rotated).expect("persist");
210 assert_eq!(
211 custody.unlock().unwrap().unwrap().fingerprint(),
212 rotated.fingerprint(),
213 );
214
215 custody.forget().expect("forget");
216 assert!(custody.unlock().unwrap().is_none());
217 }
218
219 #[test]
220 fn in_memory_preset_never_writes_under_the_store_dir() {
221 let (tmp, _dir) = temp_store_dir();
222 let custody = InMemoryCustody::new(MasterKeyring::generate());
223 custody
224 .persist(&MasterKeyring::generate())
225 .expect("persist");
226 custody.forget().expect("forget");
227
228 let entries: Vec<_> = std::fs::read_dir(tmp.path())
229 .expect("read store dir")
230 .collect();
231 assert!(
232 entries.is_empty(),
233 "InMemory custody must touch no file under the store dir",
234 );
235 }
236
237 #[test]
242 fn passphrase_preset_establish_then_unlock_round_trips() {
243 let (_tmp, dir) = temp_store_dir();
244 let custody = PassphraseCustody::<MasterKeyring>::new(
245 Passphrase::new("correct horse battery staple".to_string()),
246 &dir,
247 );
248
249 assert!(custody.unlock().expect("unlock before establish").is_none());
250
251 let keyring = MasterKeyring::generate();
252 custody.persist(&keyring).expect("establish");
253 let unlocked = custody
254 .unlock()
255 .expect("unlock after establish")
256 .expect("keyring is established");
257 assert_eq!(unlocked.fingerprint(), keyring.fingerprint());
258 }
259
260 #[test]
261 fn passphrase_preset_wrong_passphrase_is_err_not_none() {
262 let (_tmp, dir) = temp_store_dir();
263 let writer = PassphraseCustody::<MasterKeyring>::new(
264 Passphrase::new("right passphrase".to_string()),
265 &dir,
266 );
267 writer
268 .persist(&MasterKeyring::generate())
269 .expect("establish");
270
271 let reader = PassphraseCustody::<MasterKeyring>::new(
272 Passphrase::new("wrong passphrase".to_string()),
273 &dir,
274 );
275 let error = reader
276 .unlock()
277 .expect_err("wrong passphrase must not unlock");
278 assert!(
279 error.to_string().to_lowercase().contains("passphrase")
280 || matches!(error, KeyError::PassphraseEnvelopeDecryption)
281 );
282 }
283
284 #[test]
285 fn passphrase_preset_missing_file_is_none() {
286 let (_tmp, dir) = temp_store_dir();
287 let custody =
288 PassphraseCustody::<MasterKeyring>::new(Passphrase::new("unused".to_string()), &dir);
289 assert!(custody.unlock().expect("unlock with no file").is_none());
290 }
291
292 const V1_FIXTURE_PASSPHRASE: &str = "fixture-passphrase";
304 const V1_FIXTURE_ENVELOPE_JSON: &str = concat!(
305 r#"{"v":1,"kdf":{"algo":"argon2id","m_cost":65536,"t_cost":3,"p_cost":4,"#,
306 r#""salt_b64":"3q2+7wEjRWeJq83vABEiMw=="},"#,
307 r#""nonce_b64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYX","#,
308 r#""ciphertext_b64":"+7/Z7TSK5xtqL6fqDzh5ayBkPPtuzf/0FyBy3mrgtiFjfabWOqVb8FonvR7SwvntJd9ERnTDljuE0o3Ofzs8a6XMbf0VJ6HlDp2aB62apAV3Fv1e1eb8su6/TxVOCskR9cvDmPr2P3CnsX3YRaGcEuilgSW8uKosW6gowDDZHqGat1XSPbnG02GO5QYuMxM="}"#
309 );
310
311 #[test]
312 fn passphrase_preset_envelope_fixture_v1_unlocks() {
313 let (_tmp, dir) = temp_store_dir();
314 std::fs::write(dir.join("master.keyring"), V1_FIXTURE_ENVELOPE_JSON)
315 .expect("write fixture envelope");
316
317 let custody = PassphraseCustody::<MasterKeyring>::new(
318 Passphrase::new(V1_FIXTURE_PASSPHRASE.to_string()),
319 &dir,
320 );
321 let keyring = custody
322 .unlock()
323 .expect("the v1 fixture must still unlock")
324 .expect("the fixture names an established keyring");
325 assert_eq!(
326 keyring.fingerprint(),
327 EncryptionService::from_key([0x11u8; 32]).fingerprint(),
328 );
329 }
330}