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 is_content_key_mode(&self) -> bool {
93 matches!(self.mode, VaultMode::ContentKey { .. })
94 }
95
96 fn require_root_key(&self) -> CryptoResult<&[u8; 32]> {
102 match &self.mode {
103 VaultMode::RootKey { root_key, .. } => Ok(root_key),
104 VaultMode::ContentKey { .. } => Err(CryptoError::ReadOnlyVault),
105 }
106 }
107
108 fn require_keyring(&self) -> CryptoResult<&KeyRing> {
110 match &self.mode {
111 VaultMode::RootKey { keyring, .. } => Ok(keyring),
112 VaultMode::ContentKey { .. } => Err(CryptoError::ReadOnlyVault),
113 }
114 }
115
116 pub fn open_commit(&self, blob: &EncryptedCommit) -> CryptoResult<(Vec<u8>, CommitReader)> {
129 match &self.mode {
130 VaultMode::RootKey { root_key, .. } => {
131 CommitReader::open(root_key, blob.as_bytes())
132 }
133 VaultMode::ContentKey { content_key } => {
134 let reader = CommitReader::from_content_key(*content_key);
135 let (plaintext, _nonce) =
136 reader.decrypt_envelope_body(blob.as_bytes(), AAD_COMMIT)?;
137 Ok((plaintext, reader))
138 }
139 }
140 }
141
142 pub fn seal_commit(&self, plaintext: &[u8]) -> CryptoResult<EncryptedCommit> {
146 let root_key = self.require_root_key()?;
147 let nonce = generate_key_nonce();
148 let bytes = encrypt_with_envelope(root_key, &nonce, plaintext, AAD_COMMIT)?;
149 Ok(EncryptedCommit::from_bytes(bytes))
150 }
151
152 pub fn seal_commit_with_nonce(
156 &self,
157 plaintext: &[u8],
158 nonce: &KeyNonce,
159 ) -> CryptoResult<EncryptedCommit> {
160 let root_key = self.require_root_key()?;
161 let bytes = encrypt_with_envelope(root_key, nonce, plaintext, AAD_COMMIT)?;
162 Ok(EncryptedCommit::from_bytes(bytes))
163 }
164
165 pub fn derive_commit_key(&self) -> CryptoResult<(ContentKey, KeyNonce)> {
170 let root_key = self.require_root_key()?;
171 let nonce = generate_key_nonce();
172 let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
173 let derived = crate::kdf::derive_scoped_key(root_key, &scope)?;
174 Ok((ContentKey::new(derived), nonce))
175 }
176
177 pub fn index_key(&self) -> CryptoResult<&SecretKey> {
185 Ok(&self.require_keyring()?.index)
186 }
187
188 pub fn stash_key(&self) -> CryptoResult<&SecretKey> {
192 Ok(&self.require_keyring()?.stash)
193 }
194
195 pub fn staged_key(&self) -> CryptoResult<&SecretKey> {
199 Ok(&self.require_keyring()?.staged)
200 }
201
202 pub fn commits_key(&self) -> CryptoResult<&SecretKey> {
206 Ok(&self.require_keyring()?.commits)
207 }
208
209 pub fn metadata_key(&self) -> CryptoResult<&SecretKey> {
213 Ok(&self.require_keyring()?.metadata)
214 }
215
216 pub fn content_key(&self) -> CryptoResult<&SecretKey> {
220 Ok(&self.require_keyring()?.content)
221 }
222
223 pub fn keyring(&self) -> CryptoResult<&KeyRing> {
227 self.require_keyring()
228 }
229
230 pub fn decrypt_blob(&self, blob: &[u8], aad: &[u8]) -> CryptoResult<Vec<u8>> {
238 let root_key = self.require_root_key()?;
239 crate::decrypt_envelope(root_key, blob, aad)
240 .map(|(plaintext, _nonce)| plaintext)
241 }
242
243 pub fn decrypt_blob_raw(
248 &self,
249 blob: &[u8],
250 aad: &[u8],
251 ) -> CryptoResult<Vec<u8>> {
252 let root_key = self.require_root_key()?;
253 use crate::kdf::derive_scoped_key;
254
255 const ENVELOPE_HEADER_SIZE: usize = 4 + KeyNonce::SIZE;
256
257 if blob.len() <= ENVELOPE_HEADER_SIZE || !blob.starts_with(crate::MAGIC_V1) {
258 return Err(CryptoError::Decryption(
259 "missing VD01 envelope header".into(),
260 ));
261 }
262
263 let nonce = KeyNonce::from_bytes(&blob[4..ENVELOPE_HEADER_SIZE])
264 .ok_or_else(|| CryptoError::Decryption(
265 "invalid envelope nonce length".into(),
266 ))?;
267 let scope = format!("commit:{}", hex::encode(nonce.as_bytes()));
268 let derived_key = derive_scoped_key(root_key, &scope)?;
269 crate::decrypt(&derived_key, &blob[ENVELOPE_HEADER_SIZE..], aad)
270 }
271
272 pub fn seal_metadata(&self, plaintext: &[u8]) -> CryptoResult<EncryptedMetadata> {
281 let root_key = self.require_root_key()?;
282 let bytes = crate::encrypt(root_key, plaintext, AAD_METADATA)?;
283 Ok(EncryptedMetadata::from_bytes(bytes))
284 }
285
286 pub fn seal_metadata_with_key(&self, key: &ContentKey, plaintext: &[u8]) -> CryptoResult<EncryptedMetadata> {
292 let bytes = crate::encrypt(key.as_bytes(), plaintext, AAD_METADATA)?;
293 Ok(EncryptedMetadata::from_bytes(bytes))
294 }
295
296 pub fn seal_shard(&self, plaintext: &[u8]) -> CryptoResult<EncryptedShard> {
300 let root_key = self.require_root_key()?;
301 let bytes = crate::encrypt(root_key, plaintext, AAD_SHARD)?;
302 Ok(EncryptedShard::from_bytes(bytes))
303 }
304
305 pub fn unseal_shard(&self, blob: &EncryptedShard) -> CryptoResult<Vec<u8>> {
309 let root_key = self.require_root_key()?;
310 crate::decrypt(root_key, blob.as_bytes(), AAD_SHARD)
311 }
312
313 pub fn unseal_commit_with_nonce(&self, blob: &[u8]) -> CryptoResult<(Vec<u8>, KeyNonce)> {
317 let root_key = self.require_root_key()?;
318 crate::decrypt_envelope(root_key, blob, AAD_COMMIT)
319 }
320
321 pub fn derive_scoped_key(&self, scope: &str) -> CryptoResult<ContentKey> {
325 let root_key = self.require_root_key()?;
326 let derived = crate::kdf::derive_scoped_key(root_key, scope)?;
327 Ok(ContentKey::new(derived))
328 }
329
330 pub fn repo_secret_fallback(&self) -> CryptoResult<SecretKey> {
334 let root_key = self.require_root_key()?;
335 Ok(SecretKey::new(
336 crate::derive_key(root_key, "void-v1-repo-secret")
337 .expect("HKDF derivation is infallible")
338 ))
339 }
340}
341
342impl Drop for KeyVault {
343 fn drop(&mut self) {
344 match &mut self.mode {
345 VaultMode::RootKey { root_key, .. } => root_key.zeroize(),
346 VaultMode::ContentKey { content_key } => content_key.zeroize(),
347 }
348 }
349}
350
351impl std::fmt::Debug for KeyVault {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 match &self.mode {
354 VaultMode::RootKey { .. } => {
355 f.debug_struct("KeyVault")
356 .field("mode", &"RootKey [REDACTED]")
357 .finish()
358 }
359 VaultMode::ContentKey { .. } => {
360 f.debug_struct("KeyVault")
361 .field("mode", &"ContentKey [REDACTED]")
362 .finish()
363 }
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::{encrypt, AAD_SHARD};
372
373 #[test]
374 fn vault_new_and_derived_keys() {
375 let root_key = crate::generate_key();
376 let vault = KeyVault::new(root_key).unwrap();
377
378 assert_ne!(vault.index_key().unwrap().as_bytes(), vault.stash_key().unwrap().as_bytes());
380 assert_ne!(vault.stash_key().unwrap().as_bytes(), vault.staged_key().unwrap().as_bytes());
381 assert_ne!(
382 vault.index_key().unwrap().as_bytes(),
383 vault.staged_key().unwrap().as_bytes()
384 );
385 }
386
387 #[test]
388 fn vault_open_commit_roundtrip() {
389 let root_key = crate::generate_key();
390 let vault = KeyVault::new(root_key).unwrap();
391
392 let plaintext = b"commit data";
393 let sealed = vault.seal_commit(plaintext).unwrap();
394
395 let (decrypted, reader) = vault.open_commit(&sealed).unwrap();
396 assert_eq!(decrypted, plaintext);
397
398 assert_ne!(reader.content_key().as_bytes(), &root_key);
400 }
401
402 #[test]
403 fn vault_seal_then_decrypt_shard() {
404 let root_key = crate::generate_key();
405 let vault = KeyVault::new(root_key).unwrap();
406
407 let commit_data = b"commit";
408 let sealed = vault.seal_commit(commit_data).unwrap();
409 let (_decrypted, reader) = vault.open_commit(&sealed).unwrap();
410
411 let shard_data = b"shard content";
413 let shard_blob =
414 encrypt(reader.content_key().as_bytes(), shard_data, AAD_SHARD).unwrap();
415
416 let decrypted_shard = reader.decrypt_shard(&shard_blob, None, &[]).unwrap();
417 assert_eq!(decrypted_shard, shard_data);
418 }
419
420 #[test]
421 fn vault_derive_commit_key() {
422 let root_key = crate::generate_key();
423 let vault = KeyVault::new(root_key).unwrap();
424
425 let (ck1, nonce1) = vault.derive_commit_key().unwrap();
426 let (ck2, nonce2) = vault.derive_commit_key().unwrap();
427
428 assert_ne!(nonce1, nonce2);
430 assert_ne!(ck1.as_bytes(), ck2.as_bytes());
431 }
432
433 #[test]
434 fn vault_debug_redacts_key() {
435 let root_key = crate::generate_key();
436 let vault = KeyVault::new(root_key).unwrap();
437
438 let debug = format!("{:?}", vault);
439 assert!(debug.contains("REDACTED"));
440 assert!(debug.contains("RootKey"));
441 }
442
443 #[test]
446 fn content_key_vault_open_commit() {
447 let root_key = crate::generate_key();
448 let root_vault = KeyVault::new(root_key).unwrap();
449
450 let plaintext = b"test commit payload";
452 let sealed = root_vault.seal_commit(plaintext).unwrap();
453
454 let (_, root_reader) = root_vault.open_commit(&sealed).unwrap();
456 let ck = *root_reader.content_key();
457
458 let ck_vault = KeyVault::from_content_key(ck);
460 let (ck_plaintext, ck_reader) = ck_vault.open_commit(&sealed).unwrap();
461
462 assert_eq!(ck_plaintext, plaintext);
463 assert_eq!(ck_reader.content_key(), &ck);
464 }
465
466 #[test]
467 fn content_key_vault_rejects_seal() {
468 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
469 let vault = KeyVault::from_content_key(ck);
470
471 assert!(vault.seal_commit(b"test").is_err());
472 assert!(vault.seal_metadata(b"test").is_err());
473 assert!(vault.seal_shard(b"test").is_err());
474 }
475
476 #[test]
477 fn content_key_vault_rejects_keyring_access() {
478 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
479 let vault = KeyVault::from_content_key(ck);
480
481 assert!(vault.index_key().is_err());
482 assert!(vault.staged_key().is_err());
483 assert!(vault.stash_key().is_err());
484 }
485
486 #[test]
487 fn content_key_vault_is_content_key_mode() {
488 let root_vault = KeyVault::new(crate::generate_key()).unwrap();
489 assert!(!root_vault.is_content_key_mode());
490
491 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
492 let ck_vault = KeyVault::from_content_key(ck);
493 assert!(ck_vault.is_content_key_mode());
494 }
495
496 #[test]
497 fn content_key_vault_debug_redacts() {
498 let ck = ContentKey::from_hex(&hex::encode([0x42u8; 32])).unwrap();
499 let vault = KeyVault::from_content_key(ck);
500 let debug = format!("{:?}", vault);
501 assert!(debug.contains("REDACTED"));
502 assert!(debug.contains("ContentKey"));
503 }
504
505 #[test]
506 fn root_seal_then_content_key_open_roundtrip() {
507 let root_key = crate::generate_key();
508 let root_vault = KeyVault::new(root_key).unwrap();
509
510 let commit_data = b"commit payload";
512 let sealed = root_vault.seal_commit(commit_data).unwrap();
513 let (_, root_reader) = root_vault.open_commit(&sealed).unwrap();
514 let ck = *root_reader.content_key();
515
516 let metadata = b"metadata payload";
517 let enc_metadata = root_vault.seal_metadata_with_key(&ck, metadata).unwrap();
518
519 let ck_vault = KeyVault::from_content_key(ck);
521 let (ck_plaintext, ck_reader) = ck_vault.open_commit(&sealed).unwrap();
522 assert_eq!(ck_plaintext, commit_data);
523
524 let dec_root: Vec<u8> = root_reader.decrypt_metadata_raw(enc_metadata.as_bytes()).unwrap();
526 let dec_ck: Vec<u8> = ck_reader.decrypt_metadata_raw(enc_metadata.as_bytes()).unwrap();
527 assert_eq!(dec_root, metadata);
528 assert_eq!(dec_ck, metadata);
529 }
530}