1use std::collections::{BTreeMap, BTreeSet};
8use std::fmt;
9use std::path::Path;
10
11use aes_gcm::Aes256Gcm;
12use aes_gcm_siv::aead::{Aead, KeyInit, Payload};
13use aes_gcm_siv::{Aes256GcmSiv, Nonce};
14use async_trait::async_trait;
15use thiserror::Error;
16use zeroize::Zeroizing;
17
18pub const BLOB_MAGIC: &[u8; 8] = b"CORIUMB1";
20
21pub const LOG_MAGIC: &[u8; 8] = b"CORIUML1";
23
24const ALGORITHM_AES_256_GCM_SIV: u8 = 1;
25const ALGORITHM_AES_256_GCM: u8 = 2;
26const BLOB_HEADER_LEN: usize = BLOB_MAGIC.len() + 1 + size_of::<u32>() + size_of::<u64>();
27const LOG_HEADER_LEN: usize = LOG_MAGIC.len() + 1 + size_of::<u32>() + size_of::<u64>() + NONCE_LEN;
29const AEAD_TAG_LEN: usize = 16;
30const NONCE_LEN: usize = 12;
31
32#[derive(Clone, Eq, PartialEq)]
34pub struct SecretKey(Zeroizing<[u8; 32]>);
35
36impl SecretKey {
37 #[must_use]
39 pub fn new(bytes: [u8; 32]) -> Self {
40 Self(Zeroizing::new(bytes))
41 }
42
43 pub fn generate() -> Result<Self, CryptError> {
52 let mut bytes = Zeroizing::new([0_u8; 32]);
53 getrandom::fill(bytes.as_mut_slice()).map_err(|_| CryptError::RandomnessUnavailable)?;
54 Ok(Self(bytes))
55 }
56
57 pub fn from_slice(bytes: &[u8]) -> Result<Self, CryptError> {
63 let bytes = <[u8; 32]>::try_from(bytes).map_err(|_| CryptError::InvalidKeyLength)?;
64 Ok(Self::new(bytes))
65 }
66
67 fn as_bytes(&self) -> &[u8; 32] {
68 &self.0
69 }
70}
71
72impl fmt::Debug for SecretKey {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str("SecretKey([REDACTED])")
75 }
76}
77
78#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct KeyId(String);
81
82impl KeyId {
83 pub fn new(value: impl Into<String>) -> Result<Self, KeyError> {
89 let value = value.into();
90 if value.is_empty() {
91 return Err(KeyError::InvalidId);
92 }
93 Ok(Self(value))
94 }
95
96 #[must_use]
98 pub fn as_str(&self) -> &str {
99 &self.0
100 }
101
102 #[must_use]
108 pub fn scheme(&self) -> Option<(&str, &str)> {
109 self.0.split_once(':')
110 }
111}
112
113impl fmt::Display for KeyId {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.write_str(&self.0)
116 }
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct BlobHeader {
122 pub epoch: u32,
124 pub plaintext_len: u64,
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub struct LogHeader {
131 pub epoch: u32,
133 pub t: u64,
135}
136
137#[derive(Debug, Error)]
139pub enum CryptError {
140 #[error("secret key must be exactly 32 bytes")]
142 InvalidKeyLength,
143 #[error("cryptographic randomness is unavailable")]
145 RandomnessUnavailable,
146 #[error("invalid encrypted blob header")]
148 InvalidBlobHeader,
149 #[error("invalid encrypted log record header")]
151 InvalidLogHeader,
152 #[error("unsupported encrypted blob algorithm {0}")]
154 UnsupportedAlgorithm(u8),
155 #[error("encrypted blob length does not match its header")]
157 InvalidBlobLength,
158 #[error("encryption failed")]
160 EncryptionFailed,
161 #[error("authentication failed")]
163 AuthenticationFailed,
164 #[error("plaintext is too large to encrypt")]
166 PlaintextTooLarge,
167}
168
169#[derive(Debug, Error)]
171pub enum KeyError {
172 #[error("key identity must not be empty")]
174 InvalidId,
175 #[error("key {id} has no material for epoch {epoch}")]
177 MissingKey {
178 id: KeyId,
180 epoch: u32,
182 },
183 #[error("key {0} has no current epoch")]
185 MissingCurrentEpoch(KeyId),
186 #[error("wrapped key did not contain a 256-bit key")]
188 InvalidWrappedKey,
189 #[error(
191 "key {0} names a key source this build cannot resolve; \
192 file: and env: are always available"
193 )]
194 UnsupportedScheme(KeyId),
195 #[error("key {id} could not be read: {reason}")]
199 Unreadable {
200 id: KeyId,
202 reason: String,
204 },
205 #[error("key {0} does not hold 32 bytes of key material (raw or 64 hex characters)")]
207 InvalidMaterial(KeyId),
208 #[error("wrapped key uses epoch {actual}, expected {expected}")]
210 WrappedEpochMismatch {
211 expected: u32,
213 actual: u32,
215 },
216 #[error(transparent)]
218 Crypt(#[from] CryptError),
219}
220
221#[async_trait]
223pub trait Keyring: Send + Sync {
224 async fn key(&self, id: &KeyId, epoch: u32) -> Result<SecretKey, KeyError>;
226
227 async fn current_epoch(&self, id: &KeyId) -> Result<u32, KeyError>;
229
230 async fn wrap(&self, id: &KeyId, epoch: u32, dek: &SecretKey) -> Result<Vec<u8>, KeyError>;
232
233 async fn unwrap(&self, id: &KeyId, epoch: u32, wrapped: &[u8]) -> Result<SecretKey, KeyError>;
235
236 fn key_ids(&self) -> &[KeyId];
238}
239
240#[derive(Clone, Default)]
243pub struct StaticKeyring {
244 keys: BTreeMap<(KeyId, u32), SecretKey>,
245 current_epochs: BTreeMap<KeyId, u32>,
246 key_ids: Vec<KeyId>,
247}
248
249pub const STATIC_KEY_EPOCH: u32 = 1;
256
257impl StaticKeyring {
258 pub fn resolve(ids: impl IntoIterator<Item = KeyId>) -> Result<Self, KeyError> {
269 let mut keyring = Self::default();
270 for id in ids {
271 let key = load_key(&id)?;
272 keyring.insert(id, STATIC_KEY_EPOCH, key, true);
273 }
274 Ok(keyring)
275 }
276
277 pub fn insert(&mut self, id: KeyId, epoch: u32, key: SecretKey, current: bool) {
279 if current {
280 self.current_epochs.insert(id.clone(), epoch);
281 }
282 self.keys.insert((id, epoch), key);
283 self.key_ids = self
284 .keys
285 .keys()
286 .map(|(id, _)| id.clone())
287 .collect::<BTreeSet<_>>()
288 .into_iter()
289 .collect();
290 }
291}
292
293#[async_trait]
294impl Keyring for StaticKeyring {
295 async fn key(&self, id: &KeyId, epoch: u32) -> Result<SecretKey, KeyError> {
296 self.keys
297 .get(&(id.clone(), epoch))
298 .cloned()
299 .ok_or_else(|| KeyError::MissingKey {
300 id: id.clone(),
301 epoch,
302 })
303 }
304
305 async fn current_epoch(&self, id: &KeyId) -> Result<u32, KeyError> {
306 self.current_epochs
307 .get(id)
308 .copied()
309 .ok_or_else(|| KeyError::MissingCurrentEpoch(id.clone()))
310 }
311
312 async fn wrap(&self, id: &KeyId, epoch: u32, dek: &SecretKey) -> Result<Vec<u8>, KeyError> {
313 let kek = self.key(id, epoch).await?;
314 let wrapping_key = derive_key(&kek, b"corium/key-wrap");
315 encrypt_blob(&wrapping_key, epoch, dek.as_bytes()).map_err(Into::into)
316 }
317
318 async fn unwrap(&self, id: &KeyId, epoch: u32, wrapped: &[u8]) -> Result<SecretKey, KeyError> {
319 let kek = self.key(id, epoch).await?;
320 let wrapping_key = derive_key(&kek, b"corium/key-wrap");
321 let header = parse_blob_header(wrapped)?;
322 if header.epoch != epoch {
323 return Err(KeyError::WrappedEpochMismatch {
324 expected: epoch,
325 actual: header.epoch,
326 });
327 }
328 let plaintext = Zeroizing::new(decrypt_blob(&wrapping_key, wrapped)?);
329 SecretKey::from_slice(plaintext.as_slice()).map_err(|_| KeyError::InvalidWrappedKey)
330 }
331
332 fn key_ids(&self) -> &[KeyId] {
333 &self.key_ids
334 }
335}
336
337pub fn load_key(id: &KeyId) -> Result<SecretKey, KeyError> {
353 let Some((scheme, rest)) = id.scheme() else {
354 return Err(KeyError::UnsupportedScheme(id.clone()));
355 };
356 let material = match scheme {
357 "file" => Zeroizing::new(std::fs::read(Path::new(rest)).map_err(|error| {
358 KeyError::Unreadable {
359 id: id.clone(),
360 reason: format!("cannot read {rest}: {error}"),
361 }
362 })?),
363 "env" => Zeroizing::new(
364 std::env::var(rest)
365 .map_err(|_| KeyError::Unreadable {
366 id: id.clone(),
367 reason: format!("environment variable {rest} is not set"),
368 })?
369 .into_bytes(),
370 ),
371 _ => return Err(KeyError::UnsupportedScheme(id.clone())),
372 };
373 decode_key_material(&material).ok_or_else(|| KeyError::InvalidMaterial(id.clone()))
374}
375
376fn decode_key_material(material: &[u8]) -> Option<SecretKey> {
379 if let Ok(key) = SecretKey::from_slice(material) {
380 return Some(key);
381 }
382 let trimmed = std::str::from_utf8(material).ok()?.trim();
383 if trimmed.len() != 64 {
384 return None;
385 }
386 let mut bytes = Zeroizing::new([0_u8; 32]);
387 for (slot, pair) in bytes.iter_mut().zip(trimmed.as_bytes().chunks(2)) {
388 let pair = std::str::from_utf8(pair).ok()?;
389 *slot = u8::from_str_radix(pair, 16).ok()?;
390 }
391 Some(SecretKey::new(*bytes))
392}
393
394#[must_use]
396pub fn derive_key(parent: &SecretKey, context: &[u8]) -> SecretKey {
397 let mut hasher = blake3::Hasher::new_keyed(parent.as_bytes());
398 hasher.update(b"corium/derived-key");
399 hasher.update(context);
400 SecretKey::new(*hasher.finalize().as_bytes())
401}
402
403pub fn parse_blob_header(object: &[u8]) -> Result<BlobHeader, CryptError> {
410 if object.len() < BLOB_HEADER_LEN || &object[..BLOB_MAGIC.len()] != BLOB_MAGIC {
411 return Err(CryptError::InvalidBlobHeader);
412 }
413 let algorithm = object[BLOB_MAGIC.len()];
414 if algorithm != ALGORITHM_AES_256_GCM_SIV {
415 return Err(CryptError::UnsupportedAlgorithm(algorithm));
416 }
417
418 let epoch_offset = BLOB_MAGIC.len() + 1;
419 let length_offset = epoch_offset + size_of::<u32>();
420 let epoch = u32::from_be_bytes(
421 object[epoch_offset..length_offset]
422 .try_into()
423 .map_err(|_| CryptError::InvalidBlobHeader)?,
424 );
425 let plaintext_len = u64::from_be_bytes(
426 object[length_offset..BLOB_HEADER_LEN]
427 .try_into()
428 .map_err(|_| CryptError::InvalidBlobHeader)?,
429 );
430 let plaintext_len =
431 usize::try_from(plaintext_len).map_err(|_| CryptError::InvalidBlobLength)?;
432 let expected_len = BLOB_HEADER_LEN
433 .checked_add(NONCE_LEN)
434 .and_then(|length| length.checked_add(plaintext_len))
435 .and_then(|length| length.checked_add(AEAD_TAG_LEN))
436 .ok_or(CryptError::InvalidBlobLength)?;
437 if object.len() != expected_len {
438 return Err(CryptError::InvalidBlobLength);
439 }
440 Ok(BlobHeader {
441 epoch,
442 plaintext_len: plaintext_len as u64,
443 })
444}
445
446pub fn encrypt_blob(key: &SecretKey, epoch: u32, plaintext: &[u8]) -> Result<Vec<u8>, CryptError> {
455 let plaintext_len =
456 u64::try_from(plaintext.len()).map_err(|_| CryptError::PlaintextTooLarge)?;
457 let mut header = Vec::with_capacity(BLOB_HEADER_LEN);
458 header.extend_from_slice(BLOB_MAGIC);
459 header.push(ALGORITHM_AES_256_GCM_SIV);
460 header.extend_from_slice(&epoch.to_be_bytes());
461 header.extend_from_slice(&plaintext_len.to_be_bytes());
462
463 let plaintext_digest = blake3::hash(plaintext);
464 let mut nonce_hasher = blake3::Hasher::new_keyed(key.as_bytes());
465 nonce_hasher.update(b"corium/blob-nonce");
466 nonce_hasher.update(&header);
467 nonce_hasher.update(plaintext_digest.as_bytes());
468 let nonce_digest = nonce_hasher.finalize();
469 let nonce_bytes = &nonce_digest.as_bytes()[..NONCE_LEN];
470 let cipher =
471 Aes256GcmSiv::new_from_slice(key.as_bytes()).map_err(|_| CryptError::InvalidKeyLength)?;
472 let ciphertext = cipher
473 .encrypt(
474 Nonce::from_slice(nonce_bytes),
475 Payload {
476 msg: plaintext,
477 aad: &header,
478 },
479 )
480 .map_err(|_| CryptError::EncryptionFailed)?;
481 header.extend_from_slice(nonce_bytes);
482 header.extend_from_slice(&ciphertext);
483 Ok(header)
484}
485
486pub fn decrypt_blob(key: &SecretKey, object: &[u8]) -> Result<Vec<u8>, CryptError> {
492 let _header = parse_blob_header(object)?;
493 let header = &object[..BLOB_HEADER_LEN];
494 let nonce_end = BLOB_HEADER_LEN + NONCE_LEN;
495 let nonce = Nonce::from_slice(&object[BLOB_HEADER_LEN..nonce_end]);
496 let ciphertext = &object[nonce_end..];
497 let cipher =
498 Aes256GcmSiv::new_from_slice(key.as_bytes()).map_err(|_| CryptError::InvalidKeyLength)?;
499 cipher
500 .decrypt(
501 nonce,
502 Payload {
503 msg: ciphertext,
504 aad: header,
505 },
506 )
507 .map_err(|_| CryptError::AuthenticationFailed)
508}
509
510#[must_use]
517pub fn is_encrypted_log_record(payload: &[u8]) -> bool {
518 payload.len() >= LOG_MAGIC.len() && &payload[..LOG_MAGIC.len()] == LOG_MAGIC
519}
520
521pub fn parse_log_header(payload: &[u8]) -> Result<LogHeader, CryptError> {
532 if !is_encrypted_log_record(payload) || payload.len() < LOG_HEADER_LEN + AEAD_TAG_LEN {
533 return Err(CryptError::InvalidLogHeader);
534 }
535 let algorithm = payload[LOG_MAGIC.len()];
536 if algorithm != ALGORITHM_AES_256_GCM {
537 return Err(CryptError::UnsupportedAlgorithm(algorithm));
538 }
539 let epoch_offset = LOG_MAGIC.len() + 1;
540 let t_offset = epoch_offset + size_of::<u32>();
541 let nonce_offset = t_offset + size_of::<u64>();
542 let epoch = u32::from_be_bytes(
543 payload[epoch_offset..t_offset]
544 .try_into()
545 .map_err(|_| CryptError::InvalidLogHeader)?,
546 );
547 let t = u64::from_be_bytes(
548 payload[t_offset..nonce_offset]
549 .try_into()
550 .map_err(|_| CryptError::InvalidLogHeader)?,
551 );
552 Ok(LogHeader { epoch, t })
553}
554
555fn log_header_and_aad(
557 epoch: u32,
558 lineage: &[u8],
559 log_version: u64,
560 t: u64,
561 nonce: &[u8; NONCE_LEN],
562) -> (Vec<u8>, Vec<u8>) {
563 let mut header = Vec::with_capacity(LOG_HEADER_LEN);
564 header.extend_from_slice(LOG_MAGIC);
565 header.push(ALGORITHM_AES_256_GCM);
566 header.extend_from_slice(&epoch.to_be_bytes());
567 header.extend_from_slice(&t.to_be_bytes());
568 header.extend_from_slice(nonce);
569
570 let mut aad = Vec::with_capacity(b"corium/log-v1".len() + 16 + lineage.len() + header.len());
574 aad.extend_from_slice(b"corium/log-v1");
575 aad.extend_from_slice(&(lineage.len() as u64).to_be_bytes());
576 aad.extend_from_slice(lineage);
577 aad.extend_from_slice(&log_version.to_be_bytes());
578 aad.extend_from_slice(&header);
579 (header, aad)
580}
581
582pub fn encrypt_log_record(
602 key: &SecretKey,
603 epoch: u32,
604 lineage: &[u8],
605 log_version: u64,
606 t: u64,
607 plaintext: &[u8],
608) -> Result<Vec<u8>, CryptError> {
609 let mut nonce_bytes = [0_u8; NONCE_LEN];
610 getrandom::fill(&mut nonce_bytes).map_err(|_| CryptError::RandomnessUnavailable)?;
611 let (mut header, aad) = log_header_and_aad(epoch, lineage, log_version, t, &nonce_bytes);
612 let cipher =
613 Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|_| CryptError::InvalidKeyLength)?;
614 let ciphertext = cipher
615 .encrypt(
616 Nonce::from_slice(&nonce_bytes),
617 Payload {
618 msg: plaintext,
619 aad: &aad,
620 },
621 )
622 .map_err(|_| CryptError::EncryptionFailed)?;
623 header.extend_from_slice(&ciphertext);
624 Ok(header)
625}
626
627pub fn decrypt_log_record(
638 key: &SecretKey,
639 lineage: &[u8],
640 log_version: u64,
641 payload: &[u8],
642) -> Result<Vec<u8>, CryptError> {
643 let LogHeader { epoch, t } = parse_log_header(payload)?;
644 let header = &payload[..LOG_HEADER_LEN];
645 let nonce_offset = LOG_HEADER_LEN - NONCE_LEN;
646 let nonce_bytes = <[u8; NONCE_LEN]>::try_from(&header[nonce_offset..])
647 .map_err(|_| CryptError::InvalidLogHeader)?;
648 let (_, aad) = log_header_and_aad(epoch, lineage, log_version, t, &nonce_bytes);
649 let cipher =
650 Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|_| CryptError::InvalidKeyLength)?;
651 cipher
652 .decrypt(
653 Nonce::from_slice(&nonce_bytes),
654 Payload {
655 msg: &payload[LOG_HEADER_LEN..],
656 aad: &aad,
657 },
658 )
659 .map_err(|_| CryptError::AuthenticationFailed)
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use proptest::prelude::*;
666
667 fn key(byte: u8) -> SecretKey {
668 SecretKey::new([byte; 32])
669 }
670
671 proptest! {
672 #[test]
673 fn blobs_are_deterministic_and_round_trip(
674 plaintext in prop::collection::vec(any::<u8>(), 0..4096)
675 ) {
676 let encrypted = encrypt_blob(&key(7), 3, &plaintext).expect("encrypt");
677 let repeated = encrypt_blob(&key(7), 3, &plaintext).expect("repeat");
678 prop_assert_eq!(&encrypted, &repeated);
679 prop_assert_ne!(
680 encrypt_blob(&key(7), 4, &plaintext).expect("different epoch"),
681 encrypted.clone()
682 );
683 prop_assert_eq!(decrypt_blob(&key(7), &encrypted).expect("decrypt"), plaintext);
684 }
685 }
686
687 #[test]
688 fn header_and_ciphertext_are_authenticated() {
689 let encrypted = encrypt_blob(&key(1), 9, b"sentinel").expect("encrypt");
690 assert_eq!(
691 parse_blob_header(&encrypted).expect("header"),
692 BlobHeader {
693 epoch: 9,
694 plaintext_len: 8,
695 }
696 );
697 assert!(!encrypted.windows(8).any(|window| window == b"sentinel"));
698 assert!(decrypt_blob(&key(2), &encrypted).is_err());
699
700 let mut tampered = encrypted;
701 *tampered.last_mut().expect("ciphertext") ^= 1;
702 assert!(decrypt_blob(&key(1), &tampered).is_err());
703
704 let mut tampered_nonce = encrypt_blob(&key(1), 9, b"sentinel").expect("encrypt");
705 tampered_nonce[BLOB_HEADER_LEN] ^= 1;
706 assert!(decrypt_blob(&key(1), &tampered_nonce).is_err());
707 }
708
709 proptest! {
710 #[test]
711 fn log_records_round_trip(payload in prop::collection::vec(any::<u8>(), 0..4096)) {
712 let encrypted = encrypt_log_record(&key(5), 2, b"people", 7, 42, &payload)
713 .expect("encrypt");
714 prop_assert_eq!(
715 parse_log_header(&encrypted).expect("header"),
716 LogHeader { epoch: 2, t: 42 }
717 );
718 prop_assert_eq!(
719 decrypt_log_record(&key(5), b"people", 7, &encrypted).expect("decrypt"),
720 payload
721 );
722 }
723 }
724
725 #[test]
726 fn log_records_are_bound_to_their_position() {
727 let plaintext = b"sentinel payload".as_slice();
728 let encrypted = encrypt_log_record(&key(5), 2, b"people", 7, 42, plaintext).expect("seal");
729 assert!(is_encrypted_log_record(&encrypted));
730 assert!(
731 !encrypted
732 .windows(plaintext.len())
733 .any(|window| window == plaintext)
734 );
735
736 assert!(decrypt_log_record(&key(5), b"other", 7, &encrypted).is_err());
739 assert!(decrypt_log_record(&key(5), b"people", 8, &encrypted).is_err());
740 assert!(decrypt_log_record(&key(6), b"people", 7, &encrypted).is_err());
741
742 let mut moved = encrypted.clone();
743 let t_offset = LOG_MAGIC.len() + 1 + size_of::<u32>();
744 moved[t_offset..t_offset + size_of::<u64>()].copy_from_slice(&43_u64.to_be_bytes());
745 assert_eq!(parse_log_header(&moved).expect("header").t, 43);
746 assert!(decrypt_log_record(&key(5), b"people", 7, &moved).is_err());
747
748 let mut retagged = encrypted;
749 retagged[LOG_MAGIC.len() + 1] ^= 1;
750 assert!(decrypt_log_record(&key(5), b"people", 7, &retagged).is_err());
751 }
752
753 #[test]
754 fn log_records_do_not_reuse_a_nonce() {
755 let first = encrypt_log_record(&key(5), 2, b"people", 7, 42, b"first").expect("first");
758 let second = encrypt_log_record(&key(5), 2, b"people", 7, 42, b"first").expect("second");
759 assert_ne!(first, second);
760 assert_eq!(
761 decrypt_log_record(&key(5), b"people", 7, &second).expect("decrypt"),
762 b"first"
763 );
764 }
765
766 #[test]
767 fn plaintext_records_are_not_mistaken_for_encrypted_ones() {
768 let mut plaintext = 42_u64.to_be_bytes().to_vec();
769 plaintext.extend_from_slice(&[0; 32]);
770 assert!(!is_encrypted_log_record(&plaintext));
771 assert!(matches!(
772 parse_log_header(&plaintext),
773 Err(CryptError::InvalidLogHeader)
774 ));
775 assert!(!is_encrypted_log_record(&[]));
776 assert!(!is_encrypted_log_record(LOG_MAGIC.as_slice().split_at(4).0));
777 }
778
779 #[test]
780 fn generated_keys_are_distinct() {
781 let first = SecretKey::generate().expect("generate");
782 assert_ne!(first, SecretKey::generate().expect("generate"));
783 assert_ne!(first, SecretKey::new([0; 32]));
784 }
785
786 #[test]
787 fn debug_never_reveals_key_material() {
788 let rendered = format!("{:?}", key(0xA5));
789 assert_eq!(rendered, "SecretKey([REDACTED])");
790 assert!(!rendered.contains("165"));
791 }
792
793 #[tokio::test]
794 async fn local_key_sources_accept_raw_and_hex_material() {
795 let dir = std::env::temp_dir().join(format!("corium-crypt-{}", std::process::id()));
796 std::fs::create_dir_all(&dir).expect("temp dir");
797 let raw_path = dir.join("raw.key");
798 std::fs::write(&raw_path, [7_u8; 32]).expect("write raw");
799 let hex_path = dir.join("hex.key");
800 std::fs::write(&hex_path, format!("{}\n", "07".repeat(32))).expect("write hex");
802
803 let raw = KeyId::new(format!("file:{}", raw_path.display())).expect("id");
804 let hex = KeyId::new(format!("file:{}", hex_path.display())).expect("id");
805 assert_eq!(load_key(&raw).expect("raw"), key(7));
806 assert_eq!(load_key(&hex).expect("hex"), key(7));
807 assert!(matches!(
808 load_key(&KeyId::new("env:CORIUM_KEY_THAT_IS_NOT_SET").expect("id")),
809 Err(KeyError::Unreadable { .. })
810 ));
811
812 let keyring = StaticKeyring::resolve([raw.clone(), hex]).expect("resolve");
813 assert_eq!(keyring.key_ids().len(), 2);
814 assert_eq!(
815 keyring.current_epoch(&raw).await.expect("epoch"),
816 STATIC_KEY_EPOCH
817 );
818
819 std::fs::remove_dir_all(&dir).expect("clean up");
820 }
821
822 #[test]
823 fn unresolvable_key_identities_name_what_failed() {
824 let missing = KeyId::new("file:/nonexistent/corium/storage.key").expect("id");
825 assert!(matches!(
826 load_key(&missing),
827 Err(KeyError::Unreadable { .. })
828 ));
829 let kms = KeyId::new("awskms:arn:aws:kms:us-west-2:1:key/2f1c").expect("id");
832 assert!(matches!(
833 load_key(&kms),
834 Err(KeyError::UnsupportedScheme(_))
835 ));
836 assert!(matches!(
837 load_key(&KeyId::new("storage.key").expect("id")),
838 Err(KeyError::UnsupportedScheme(_))
839 ));
840 assert!(decode_key_material(b"too short").is_none());
841 assert!(decode_key_material(&[0; 31]).is_none());
842 assert!(decode_key_material("zz".repeat(32).as_bytes()).is_none());
843 }
844
845 #[tokio::test]
846 async fn static_keyring_resolves_and_wraps_keys() {
847 let id = KeyId::new("file:test-kek").expect("key id");
848 let mut keyring = StaticKeyring::default();
849 keyring.insert(id.clone(), 4, key(4), true);
850
851 assert_eq!(keyring.current_epoch(&id).await.expect("epoch"), 4);
852 assert_eq!(keyring.key_ids(), std::slice::from_ref(&id));
853 let wrapped = keyring.wrap(&id, 4, &key(8)).await.expect("wrap");
854 assert_eq!(
855 keyring.unwrap(&id, 4, &wrapped).await.expect("unwrap"),
856 key(8)
857 );
858 }
859}