1use zeroize::Zeroize;
29
30use crate::aead::{AAD_COMMIT, AAD_METADATA, AAD_SHARD};
31use crate::blob_types::{EncryptedCommit, EncryptedMetadata, EncryptedShard};
32use crate::envelope::{encrypt_with_envelope, generate_key_nonce};
33use crate::kdf::{ContentKey, KeyNonce, KeyRing, SecretKey};
34use crate::reader::CommitReader;
35use crate::{CryptoError, CryptoResult};
36
37enum VaultMode {
39 RootKey {
41 root_key: [u8; 32],
42 keyring: KeyRing,
43 },
44 ContentKey {
46 content_key: ContentKey,
47 },
48}
49
50pub struct KeyVault {
62 mode: VaultMode,
63}
64
65impl KeyVault {
66 pub fn new(root_key: [u8; 32]) -> CryptoResult<Self> {
74 let keyring = KeyRing::from_root(&root_key)?;
75 Ok(Self {
76 mode: VaultMode::RootKey { root_key, keyring },
77 })
78 }
79
80 pub fn from_content_key(content_key: ContentKey) -> Self {
86 Self {
87 mode: VaultMode::ContentKey { content_key },
88 }
89 }
90
91 pub fn from_content_key_bytes(bytes: [u8; 32]) -> Self {
96 Self::from_content_key(ContentKey::new(bytes))
97 }
98
99 pub fn is_content_key_mode(&self) -> bool {
101 matches!(self.mode, VaultMode::ContentKey { .. })
102 }
103
104 fn require_root_key(&self) -> CryptoResult<&[u8; 32]> {
110 match &self.mode {
111 VaultMode::RootKey { root_key, .. } => Ok(root_key),
112 VaultMode::ContentKey { .. } => Err(CryptoError::ReadOnlyVault),
113 }
114 }
115
116 fn require_keyring(&self) -> CryptoResult<&KeyRing> {
118 match &self.mode {
119 VaultMode::RootKey { keyring, .. } => Ok(keyring),
120 VaultMode::ContentKey { .. } => Err(CryptoError::ReadOnlyVault),
121 }
122 }
123
124 pub fn open_commit(&self, blob: &EncryptedCommit) -> CryptoResult<(Vec<u8>, CommitReader)> {
137 match &self.mode {
138 VaultMode::RootKey { root_key, .. } => {
139 CommitReader::open(root_key, blob.as_bytes())
140 }
141 VaultMode::ContentKey { content_key } => {
142 let reader = CommitReader::from_content_key(*content_key);
143 let (plaintext, _nonce) =
144 reader.decrypt_envelope_body(blob.as_bytes(), AAD_COMMIT)?;
145 Ok((plaintext, reader))
146 }
147 }
148 }
149
150 pub fn seal_commit(&self, plaintext: &[u8]) -> CryptoResult<EncryptedCommit> {
154 let root_key = self.require_root_key()?;
155 let nonce = generate_key_nonce();
156 let bytes = encrypt_with_envelope(root_key, &nonce, plaintext, AAD_COMMIT)?;
157 Ok(EncryptedCommit::from_bytes(bytes))
158 }
159
160 pub fn seal_commit_with_nonce(
164 &self,
165 plaintext: &[u8],
166 nonce: &KeyNonce,
167 ) -> CryptoResult<EncryptedCommit> {
168 let root_key = self.require_root_key()?;
169 let bytes = encrypt_with_envelope(root_key, nonce, plaintext, AAD_COMMIT)?;
170 Ok(EncryptedCommit::from_bytes(bytes))
171 }
172
173 pub fn derive_commit_key(&self) -> CryptoResult<(ContentKey, KeyNonce)> {
178 let root_key = self.require_root_key()?;
179 let nonce = generate_key_nonce();
180 let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
181 let derived = crate::kdf::derive_scoped_key(root_key, &scope)?;
182 Ok((ContentKey::new(derived), nonce))
183 }
184
185 pub fn index_key(&self) -> CryptoResult<&SecretKey> {
193 Ok(&self.require_keyring()?.index)
194 }
195
196 pub fn stash_key(&self) -> CryptoResult<&SecretKey> {
200 Ok(&self.require_keyring()?.stash)
201 }
202
203 pub fn staged_key(&self) -> CryptoResult<&SecretKey> {
207 Ok(&self.require_keyring()?.staged)
208 }
209
210 pub fn commits_key(&self) -> CryptoResult<&SecretKey> {
214 Ok(&self.require_keyring()?.commits)
215 }
216
217 pub fn metadata_key(&self) -> CryptoResult<&SecretKey> {
221 Ok(&self.require_keyring()?.metadata)
222 }
223
224 pub fn content_key(&self) -> CryptoResult<&SecretKey> {
228 Ok(&self.require_keyring()?.content)
229 }
230
231 pub fn keyring(&self) -> CryptoResult<&KeyRing> {
235 self.require_keyring()
236 }
237
238 pub fn decrypt_blob(&self, blob: &[u8], aad: &[u8]) -> CryptoResult<Vec<u8>> {
246 let root_key = self.require_root_key()?;
247 crate::decrypt_envelope(root_key, blob, aad)
248 .map(|(plaintext, _nonce)| plaintext)
249 }
250
251 pub fn decrypt_blob_raw(
256 &self,
257 blob: &[u8],
258 aad: &[u8],
259 ) -> CryptoResult<Vec<u8>> {
260 let root_key = self.require_root_key()?;
261 use crate::kdf::derive_scoped_key;
262
263 const ENVELOPE_HEADER_SIZE: usize = 4 + KeyNonce::SIZE;
264
265 if blob.len() <= ENVELOPE_HEADER_SIZE || !blob.starts_with(crate::MAGIC_V1) {
266 return Err(CryptoError::Decryption(
267 "missing VD01 envelope header".into(),
268 ));
269 }
270
271 let nonce = KeyNonce::from_bytes(&blob[4..ENVELOPE_HEADER_SIZE])
272 .ok_or_else(|| CryptoError::Decryption(
273 "invalid envelope nonce length".into(),
274 ))?;
275 let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
276 let derived_key = derive_scoped_key(root_key, &scope)?;
277 crate::decrypt(&derived_key, &blob[ENVELOPE_HEADER_SIZE..], aad)
278 }
279
280 pub fn seal_metadata(&self, plaintext: &[u8]) -> CryptoResult<EncryptedMetadata> {
289 let root_key = self.require_root_key()?;
290 let bytes = crate::encrypt(root_key, plaintext, AAD_METADATA)?;
291 Ok(EncryptedMetadata::from_bytes(bytes))
292 }
293
294 pub fn seal_metadata_with_key(&self, key: &ContentKey, plaintext: &[u8]) -> CryptoResult<EncryptedMetadata> {
300 let bytes = crate::encrypt(key.as_bytes(), plaintext, AAD_METADATA)?;
301 Ok(EncryptedMetadata::from_bytes(bytes))
302 }
303
304 pub fn seal_shard(&self, plaintext: &[u8]) -> CryptoResult<EncryptedShard> {
308 let root_key = self.require_root_key()?;
309 let bytes = crate::encrypt(root_key, plaintext, AAD_SHARD)?;
310 Ok(EncryptedShard::from_bytes(bytes))
311 }
312
313 pub fn unseal_shard(&self, blob: &EncryptedShard) -> CryptoResult<Vec<u8>> {
317 let root_key = self.require_root_key()?;
318 crate::decrypt(root_key, blob.as_bytes(), AAD_SHARD)
319 }
320
321 pub fn unseal_commit_with_nonce(&self, blob: &[u8]) -> CryptoResult<(Vec<u8>, KeyNonce)> {
325 let root_key = self.require_root_key()?;
326 crate::decrypt_envelope(root_key, blob, AAD_COMMIT)
327 }
328
329 pub fn derive_scoped_key(&self, scope: &str) -> CryptoResult<ContentKey> {
333 let root_key = self.require_root_key()?;
334 let derived = crate::kdf::derive_scoped_key(root_key, scope)?;
335 Ok(ContentKey::new(derived))
336 }
337
338 pub fn repo_secret_fallback(&self) -> CryptoResult<SecretKey> {
342 let root_key = self.require_root_key()?;
343 Ok(SecretKey::new(
344 crate::derive_key(root_key, "void-v1-repo-secret")
345 .expect("HKDF derivation is infallible")
346 ))
347 }
348}
349
350impl Drop for KeyVault {
351 fn drop(&mut self) {
352 match &mut self.mode {
353 VaultMode::RootKey { root_key, .. } => root_key.zeroize(),
354 VaultMode::ContentKey { content_key } => content_key.zeroize(),
355 }
356 }
357}
358
359impl std::fmt::Debug for KeyVault {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 match &self.mode {
362 VaultMode::RootKey { .. } => {
363 f.debug_struct("KeyVault")
364 .field("mode", &"RootKey [REDACTED]")
365 .finish()
366 }
367 VaultMode::ContentKey { .. } => {
368 f.debug_struct("KeyVault")
369 .field("mode", &"ContentKey [REDACTED]")
370 .finish()
371 }
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::{encrypt, AAD_SHARD};
380
381 #[test]
382 fn vault_new_and_derived_keys() {
383 let root_key = crate::generate_key();
384 let vault = KeyVault::new(root_key).unwrap();
385
386 assert_ne!(vault.index_key().unwrap().as_bytes(), vault.stash_key().unwrap().as_bytes());
388 assert_ne!(vault.stash_key().unwrap().as_bytes(), vault.staged_key().unwrap().as_bytes());
389 assert_ne!(
390 vault.index_key().unwrap().as_bytes(),
391 vault.staged_key().unwrap().as_bytes()
392 );
393 }
394
395 #[test]
396 fn vault_open_commit_roundtrip() {
397 let root_key = crate::generate_key();
398 let vault = KeyVault::new(root_key).unwrap();
399
400 let plaintext = b"commit data";
401 let sealed = vault.seal_commit(plaintext).unwrap();
402
403 let (decrypted, reader) = vault.open_commit(&sealed).unwrap();
404 assert_eq!(decrypted, plaintext);
405
406 assert_ne!(reader.content_key().as_bytes(), &root_key);
408 }
409
410 #[test]
411 fn vault_seal_then_decrypt_shard() {
412 let root_key = crate::generate_key();
413 let vault = KeyVault::new(root_key).unwrap();
414
415 let commit_data = b"commit";
416 let sealed = vault.seal_commit(commit_data).unwrap();
417 let (_decrypted, reader) = vault.open_commit(&sealed).unwrap();
418
419 let shard_data = b"shard content";
421 let shard_blob =
422 encrypt(reader.content_key().as_bytes(), shard_data, AAD_SHARD).unwrap();
423
424 let decrypted_shard = reader.decrypt_shard(&shard_blob, None, &[]).unwrap();
425 assert_eq!(decrypted_shard, shard_data);
426 }
427
428 #[test]
429 fn vault_derive_commit_key() {
430 let root_key = crate::generate_key();
431 let vault = KeyVault::new(root_key).unwrap();
432
433 let (ck1, nonce1) = vault.derive_commit_key().unwrap();
434 let (ck2, nonce2) = vault.derive_commit_key().unwrap();
435
436 assert_ne!(nonce1, nonce2);
438 assert_ne!(ck1.as_bytes(), ck2.as_bytes());
439 }
440
441 #[test]
442 fn vault_debug_redacts_key() {
443 let root_key = crate::generate_key();
444 let vault = KeyVault::new(root_key).unwrap();
445
446 let debug = format!("{:?}", vault);
447 assert!(debug.contains("REDACTED"));
448 assert!(debug.contains("RootKey"));
449 }
450
451 #[test]
454 fn content_key_vault_open_commit() {
455 let root_key = crate::generate_key();
456 let root_vault = KeyVault::new(root_key).unwrap();
457
458 let plaintext = b"test commit payload";
460 let sealed = root_vault.seal_commit(plaintext).unwrap();
461
462 let (_, root_reader) = root_vault.open_commit(&sealed).unwrap();
464 let ck = *root_reader.content_key();
465
466 let ck_vault = KeyVault::from_content_key(ck);
468 let (ck_plaintext, ck_reader) = ck_vault.open_commit(&sealed).unwrap();
469
470 assert_eq!(ck_plaintext, plaintext);
471 assert_eq!(ck_reader.content_key(), &ck);
472 }
473
474 #[test]
475 fn content_key_vault_rejects_seal() {
476 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
477 let vault = KeyVault::from_content_key(ck);
478
479 assert!(vault.seal_commit(b"test").is_err());
480 assert!(vault.seal_metadata(b"test").is_err());
481 assert!(vault.seal_shard(b"test").is_err());
482 }
483
484 #[test]
485 fn content_key_vault_rejects_keyring_access() {
486 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
487 let vault = KeyVault::from_content_key(ck);
488
489 assert!(vault.index_key().is_err());
490 assert!(vault.staged_key().is_err());
491 assert!(vault.stash_key().is_err());
492 }
493
494 #[test]
495 fn content_key_vault_is_content_key_mode() {
496 let root_vault = KeyVault::new(crate::generate_key()).unwrap();
497 assert!(!root_vault.is_content_key_mode());
498
499 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
500 let ck_vault = KeyVault::from_content_key(ck);
501 assert!(ck_vault.is_content_key_mode());
502 }
503
504 #[test]
505 fn content_key_vault_debug_redacts() {
506 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
507 let vault = KeyVault::from_content_key(ck);
508 let debug = format!("{:?}", vault);
509 assert!(debug.contains("REDACTED"));
510 assert!(debug.contains("ContentKey"));
511 }
512
513 #[test]
514 fn root_seal_then_content_key_open_roundtrip() {
515 let root_key = crate::generate_key();
516 let root_vault = KeyVault::new(root_key).unwrap();
517
518 let commit_data = b"commit payload";
520 let sealed = root_vault.seal_commit(commit_data).unwrap();
521 let (_, root_reader) = root_vault.open_commit(&sealed).unwrap();
522 let ck = *root_reader.content_key();
523
524 let metadata = b"metadata payload";
525 let enc_metadata = root_vault.seal_metadata_with_key(&ck, metadata).unwrap();
526
527 let ck_vault = KeyVault::from_content_key(ck);
529 let (ck_plaintext, ck_reader) = ck_vault.open_commit(&sealed).unwrap();
530 assert_eq!(ck_plaintext, commit_data);
531
532 let dec_root: Vec<u8> = root_reader.decrypt_metadata_raw(enc_metadata.as_bytes()).unwrap();
534 let dec_ck: Vec<u8> = ck_reader.decrypt_metadata_raw(enc_metadata.as_bytes()).unwrap();
535 assert_eq!(dec_root, metadata);
536 assert_eq!(dec_ck, metadata);
537 }
538}