coven_keys/
identity_custody.rs1use std::sync::Arc;
9
10use crate::custody::preset::{CustodySecret, InMemoryCustody, PassphraseCustody};
11use crate::keys::{DeviceIdentityCustody, KeyError, StoreKeys, UserKeypair, SIGN_SECRETKEYBYTES};
12use coven_foundation::store_dir::StoreDir;
13
14pub(crate) use crate::envelope::Passphrase;
15
16pub enum IdentityCustody {
20 Keyring,
22 Passphrase(Passphrase),
25 InMemory(UserKeypair),
27 Custom(Arc<dyn DeviceIdentityCustody>),
29}
30
31impl IdentityCustody {
32 pub fn resolve(
42 self,
43 store_keys: &StoreKeys,
44 store_dir: &StoreDir,
45 ) -> Arc<dyn DeviceIdentityCustody> {
46 match self {
47 IdentityCustody::Keyring => Arc::new(store_keys.clone()),
48 IdentityCustody::Passphrase(passphrase) => {
49 Arc::new(PassphraseCustody::<UserKeypair>::new(passphrase, store_dir))
50 }
51 IdentityCustody::InMemory(keypair) => Arc::new(InMemoryCustody::new(keypair)),
52 IdentityCustody::Custom(custody) => custody,
53 }
54 }
55}
56
57impl CustodySecret for UserKeypair {
62 const FILE: &'static str = "identity.envelope";
63
64 fn to_bytes(&self) -> Vec<u8> {
65 self.to_keypair_bytes().to_vec()
66 }
67
68 fn from_bytes(bytes: Vec<u8>) -> Result<Self, KeyError> {
69 let len = bytes.len();
70 let signing_key: [u8; SIGN_SECRETKEYBYTES] =
71 bytes.try_into().map_err(|_| KeyError::InvalidLength {
72 subject: "decrypted device identity",
73 expected: SIGN_SECRETKEYBYTES,
74 actual: len,
75 })?;
76 UserKeypair::from_signing_key_bytes(&signing_key)
77 }
78}
79
80impl DeviceIdentityCustody for InMemoryCustody<UserKeypair> {
81 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError> {
82 InMemoryCustody::unlock(self)
83 }
84
85 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError> {
86 InMemoryCustody::persist(self, keypair)
87 }
88
89 fn forget(&self) -> Result<(), KeyError> {
90 InMemoryCustody::forget(self)
91 }
92}
93
94impl DeviceIdentityCustody for PassphraseCustody<UserKeypair> {
95 fn unlock(&self) -> Result<Option<UserKeypair>, KeyError> {
96 PassphraseCustody::unlock(self)
97 }
98
99 fn persist(&self, keypair: &UserKeypair) -> Result<(), KeyError> {
100 PassphraseCustody::persist(self, keypair)
101 }
102
103 fn forget(&self) -> Result<(), KeyError> {
104 PassphraseCustody::forget(self)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::keys::test_keyring;
112
113 fn temp_store_dir() -> (tempfile::TempDir, StoreDir) {
114 let tmp = tempfile::tempdir().expect("temp dir");
115 let dir = StoreDir::new_ephemeral(tmp.path());
116 (tmp, dir)
117 }
118
119 #[test]
124 fn keyring_preset_unlock_persist_forget_round_trip() {
125 test_keyring::install();
126 let store_keys = StoreKeys::bind("identity-keyring-roundtrip".to_string());
127 let custody =
128 IdentityCustody::Keyring.resolve(&store_keys, &StoreDir::new_ephemeral("unused"));
129
130 assert!(
131 custody.unlock().expect("unlock a fresh store").is_none(),
132 "a fresh store has no established identity",
133 );
134
135 let keypair = UserKeypair::generate();
136 custody.persist(&keypair).expect("persist");
137 let unlocked = custody
138 .unlock()
139 .expect("unlock after persist")
140 .expect("identity is established");
141 assert_eq!(unlocked.public_key(), keypair.public_key());
142
143 custody.forget().expect("forget");
144 assert!(
145 custody.unlock().expect("unlock after forget").is_none(),
146 "forget removes the established identity",
147 );
148 }
149
150 #[test]
154 fn keyring_preset_is_scoped_to_its_store() {
155 test_keyring::install();
156 let store_a_keys = StoreKeys::bind("identity-keyring-scope-a".to_string());
157 let store_b_keys = StoreKeys::bind("identity-keyring-scope-b".to_string());
158 let store_a =
159 IdentityCustody::Keyring.resolve(&store_a_keys, &StoreDir::new_ephemeral("unused"));
160 let store_b =
161 IdentityCustody::Keyring.resolve(&store_b_keys, &StoreDir::new_ephemeral("unused"));
162
163 let keypair_a = UserKeypair::generate();
164 store_a.persist(&keypair_a).expect("persist to store a");
165
166 assert_eq!(
167 store_a.unlock().unwrap().unwrap().public_key(),
168 keypair_a.public_key(),
169 );
170 assert!(
171 store_b.unlock().unwrap().is_none(),
172 "store b must not see store a's identity",
173 );
174 }
175
176 #[test]
181 fn in_memory_preset_unlock_returns_the_seeded_keypair() {
182 let seed = UserKeypair::generate();
183 let expected = seed.public_key();
184 let custody = InMemoryCustody::new(seed);
185
186 let unlocked = custody
187 .unlock()
188 .expect("unlock")
189 .expect("seeded keypair is present");
190 assert_eq!(unlocked.public_key(), expected);
191 }
192
193 #[test]
194 fn in_memory_preset_persist_replaces_and_forget_clears() {
195 let custody = InMemoryCustody::new(UserKeypair::generate());
196
197 let rotated = UserKeypair::generate();
198 custody.persist(&rotated).expect("persist");
199 assert_eq!(
200 custody.unlock().unwrap().unwrap().public_key(),
201 rotated.public_key(),
202 );
203
204 custody.forget().expect("forget");
205 assert!(custody.unlock().unwrap().is_none());
206 }
207
208 #[test]
213 fn passphrase_preset_establish_then_unlock_round_trips() {
214 let (_tmp, dir) = temp_store_dir();
215 let custody = PassphraseCustody::<UserKeypair>::new(
216 Passphrase::new("correct horse battery staple".to_string()),
217 &dir,
218 );
219
220 assert!(custody.unlock().expect("unlock before establish").is_none());
221
222 let keypair = UserKeypair::generate();
223 custody.persist(&keypair).expect("establish");
224 let unlocked = custody
225 .unlock()
226 .expect("unlock after establish")
227 .expect("identity is established");
228 assert_eq!(unlocked.public_key(), keypair.public_key());
229 }
230
231 #[test]
232 fn passphrase_preset_wrong_passphrase_is_err_not_none() {
233 let (_tmp, dir) = temp_store_dir();
234 let writer = PassphraseCustody::<UserKeypair>::new(
235 Passphrase::new("right passphrase".to_string()),
236 &dir,
237 );
238 writer.persist(&UserKeypair::generate()).expect("establish");
239
240 let reader = PassphraseCustody::<UserKeypair>::new(
241 Passphrase::new("wrong passphrase".to_string()),
242 &dir,
243 );
244 match reader.unlock() {
245 Err(error) => assert!(
246 matches!(error, KeyError::PassphraseEnvelopeDecryption),
247 "got {error:?}"
248 ),
249 Ok(_) => panic!("wrong passphrase must not unlock"),
250 }
251 }
252
253 #[test]
257 fn passphrase_preset_lives_inside_the_store_directory() {
258 let (_tmp, dir) = temp_store_dir();
259 let custody =
260 PassphraseCustody::<UserKeypair>::new(Passphrase::new("unused".to_string()), &dir);
261 custody.persist(&UserKeypair::generate()).expect("persist");
262
263 let path = dir.join("identity.envelope");
264 assert!(
265 path.exists(),
266 "the envelope is written inside the store directory"
267 );
268 }
269
270 const V1_FIXTURE_PASSPHRASE: &str = "fixture-passphrase";
279 const V1_FIXTURE_ENVELOPE_JSON: &str = concat!(
280 r#"{"v":1,"kdf":{"algo":"argon2id","m_cost":65536,"t_cost":3,"p_cost":4,"#,
281 r#""salt_b64":"yfYYT3S+eUdDHpvRkRJZZg=="},"#,
282 r#""nonce_b64":"+pRYN/2QyizRpYZrpG++Y9fU7R7POwp6","#,
283 r#""ciphertext_b64":"IrHxxF+oOCv4n80oKVo2VAjPA7m1rbX654FW8u4kt+0FIhqhpotFOke8JL2E8TuKuXperOtbHOtxluSb6LBGtYISbxc3RMnTot98mFXdX8A="}"#
284 );
285
286 #[test]
287 fn passphrase_preset_envelope_fixture_v1_unlocks() {
288 let (_tmp, dir) = temp_store_dir();
289 std::fs::write(dir.join("identity.envelope"), V1_FIXTURE_ENVELOPE_JSON)
290 .expect("write fixture envelope");
291
292 let custody = PassphraseCustody::<UserKeypair>::new(
293 Passphrase::new(V1_FIXTURE_PASSPHRASE.to_string()),
294 &dir,
295 );
296 let keypair = custody
297 .unlock()
298 .expect("the v1 fixture must still unlock")
299 .expect("the fixture names an established identity");
300
301 let expected = UserKeypair::from_signing_key_bytes(
302 &ed25519_dalek::SigningKey::from_bytes(&[0x11u8; 32]).to_keypair_bytes(),
303 )
304 .expect("build the expected keypair from the fixture's seed");
305 assert_eq!(keypair.public_key(), expected.public_key());
306 }
307}