1use std::fmt;
4use std::fs::File;
5use std::io::{self, Read};
6use std::path::Path;
7
8use aws_lc_rs::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
9use aws_lc_rs::digest::{SHA256, digest};
10use aws_lc_rs::hkdf::{HKDF_SHA256, KeyType, Salt};
11use uuid::Uuid;
12use zeroize::Zeroizing;
13
14use crate::ids::{KeyId, TenantId, VersionId};
15
16mod locked;
17
18use locked::LockedPage;
19
20const KEY_LEN: usize = 32;
22
23const FORMAT: u8 = 1;
26const NONCE_LEN: usize = 12;
27const TAG_LEN: usize = 16;
28
29const LABEL: &[u8] = b"vcon-server/v1";
31
32const CATALOG_LABEL: &[u8] = b"vcon-server/v1 catalog";
34
35pub struct Key {
40 page: LockedPage,
41}
42
43impl fmt::Debug for Key {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.debug_struct("Key")
46 .field("id", &format_args!("{}", self.id()))
47 .finish_non_exhaustive()
48 }
49}
50
51impl Key {
52 pub fn generate() -> Result<Key, KeyError> {
64 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
65 aws_lc_rs::rand::fill(&mut page.bytes_mut()[..KEY_LEN])
68 .expect("AWS-LC aborts instead of failing");
69 Ok(Key { page })
70 }
71
72 pub fn load_master(path: &Path) -> Result<Key, KeyError> {
81 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
82 let mut file = File::open(path).map_err(KeyError::Read)?;
83 let buf = page.bytes_mut();
84 file.read_exact(&mut buf[..KEY_LEN])
85 .map_err(|e| match e.kind() {
86 io::ErrorKind::UnexpectedEof => KeyError::WrongSize,
87 _ => KeyError::Read(e),
88 })?;
89 if file
90 .read(&mut buf[KEY_LEN..=KEY_LEN])
91 .map_err(KeyError::Read)?
92 != 0
93 {
94 return Err(KeyError::WrongSize);
95 }
96 Ok(Key { page })
97 }
98
99 #[must_use]
105 pub fn id(&self) -> KeyId {
106 let hash = digest(&SHA256, self.bytes());
107 KeyId(hash.as_ref()[..8].try_into().expect("SHA-256 is 32 bytes"))
108 }
109
110 fn bytes(&self) -> &[u8] {
111 &self.page.bytes()[..KEY_LEN]
112 }
113
114 pub(crate) fn sqlcipher_raw(&self) -> Zeroizing<String> {
118 const HEX: &[u8; 16] = b"0123456789abcdef";
119 let mut literal = Zeroizing::new(String::with_capacity(3 + 2 * KEY_LEN));
120 literal.push_str("x'");
121 for byte in self.bytes() {
122 literal.push(char::from(HEX[usize::from(byte >> 4)]));
123 literal.push(char::from(HEX[usize::from(byte & 0x0f)]));
124 }
125 literal.push('\'');
126 literal
127 }
128}
129
130#[derive(Debug)]
132pub enum KeyError {
133 Memory(io::Error),
135 Read(io::Error),
137 WrongSize,
139 Damaged,
142}
143
144impl fmt::Display for KeyError {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.write_str(match self {
148 KeyError::Memory(_) => "the key's memory could not be locked or kept out of core dumps",
149 KeyError::Read(_) => "the master key file could not be read",
150 KeyError::WrongSize => "the master key file does not hold exactly 32 bytes",
151 KeyError::Damaged => "a wrapped key did not open",
152 })
153 }
154}
155
156impl std::error::Error for KeyError {
157 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
158 match self {
159 KeyError::Memory(e) | KeyError::Read(e) => Some(e),
160 KeyError::WrongSize | KeyError::Damaged => None,
161 }
162 }
163}
164
165pub fn lock_all_memory() -> io::Result<()> {
176 if unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) } != 0 {
178 return Err(io::Error::last_os_error());
179 }
180 Ok(())
181}
182
183pub fn disable_core_dumps() -> io::Result<()> {
194 let none = libc::rlimit {
195 rlim_cur: 0,
196 rlim_max: 0,
197 };
198 if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &none) } != 0 {
200 return Err(io::Error::last_os_error());
201 }
202 if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0) } != 0 {
204 return Err(io::Error::last_os_error());
205 }
206 Ok(())
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Context {
214 Tenant {
216 tenant: TenantId,
218 },
219 Version {
222 tenant: TenantId,
224 uuid: Uuid,
226 version: VersionId,
228 },
229}
230
231impl Context {
232 fn associated_data(&self, format: u8) -> Vec<u8> {
236 let mut ad = LABEL.to_vec();
237 ad.push(format);
238 match self {
239 Context::Tenant { tenant } => {
240 ad.push(1);
241 ad.extend(tenant.0);
242 }
243 Context::Version {
244 tenant,
245 uuid,
246 version,
247 } => {
248 ad.push(2);
249 ad.extend(tenant.0);
250 ad.extend(uuid.as_bytes());
251 ad.extend(version.0);
252 }
253 }
254 ad
255 }
256}
257
258#[derive(Debug, PartialEq, Eq)]
261pub struct OpenError;
262
263impl fmt::Display for OpenError {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.write_str("the blob did not open under this key and context")
266 }
267}
268
269impl std::error::Error for OpenError {}
270
271#[must_use]
280pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
281 let nonce: [u8; NONCE_LEN] = aws_lc_rs::rand::generate(&aws_lc_rs::rand::SystemRandom::new())
284 .expect("AWS-LC aborts instead of failing")
285 .expose();
286 seal_with_nonce(data_key.bytes(), nonce, ctx, plaintext)
287}
288
289pub fn open(data_key: &Key, ctx: &Context, blob: &[u8]) -> Result<Vec<u8>, OpenError> {
297 let mut text = vec![0; blob.len().saturating_sub(1 + NONCE_LEN)];
298 let len = open_into(data_key.bytes(), ctx, blob, &mut text)?;
299 text.truncate(len);
300 Ok(text)
301}
302
303#[must_use]
306pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
307 seal(wrapping, ctx, key.bytes())
308}
309
310pub fn unwrap(wrapping: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Key, KeyError> {
321 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
322 match open_into(wrapping.bytes(), ctx, wrapped, page.bytes_mut()) {
323 Ok(KEY_LEN) => Ok(Key { page }),
324 _ => Err(KeyError::Damaged),
325 }
326}
327
328pub fn rewrap(old: &Key, new: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Vec<u8>, KeyError> {
336 let key = unwrap(old, ctx, wrapped)?;
337 Ok(wrap(new, ctx, &key))
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub enum WrapLimit {
343 Allow,
345 Warn,
347 Refuse,
349}
350
351#[must_use]
356pub fn wrap_limit(wraps: u64) -> WrapLimit {
357 const REFUSE_AT: u64 = 1 << 32;
358 const WARN_AT: u64 = 1 << 31;
359 if wraps >= REFUSE_AT {
360 WrapLimit::Refuse
361 } else if wraps >= WARN_AT {
362 WrapLimit::Warn
363 } else {
364 WrapLimit::Allow
365 }
366}
367
368fn open_into(key: &[u8], ctx: &Context, blob: &[u8], out: &mut [u8]) -> Result<usize, OpenError> {
371 let (&format, rest) = blob.split_first().ok_or(OpenError)?;
372 let (nonce, sealed) = rest.split_first_chunk::<NONCE_LEN>().ok_or(OpenError)?;
373 let text = out.get_mut(..sealed.len()).ok_or(OpenError)?;
374 text.copy_from_slice(sealed);
375 gcm_open(key, *nonce, &ctx.associated_data(format), text)
376}
377
378pub fn catalog_key(parent: &Key) -> Result<Key, KeyError> {
388 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
389 hkdf_sha256(
390 &[],
391 parent.bytes(),
392 CATALOG_LABEL,
393 &mut page.bytes_mut()[..KEY_LEN],
394 );
395 Ok(Key { page })
396}
397
398fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) {
400 struct Len(usize);
401 impl KeyType for Len {
402 fn len(&self) -> usize {
403 self.0
404 }
405 }
406 Salt::new(HKDF_SHA256, salt)
409 .extract(ikm)
410 .expand(&[info], Len(out.len()))
411 .and_then(|okm| okm.fill(out))
412 .expect("within HKDF's output limit");
413}
414
415fn seal_with_nonce(key: &[u8], nonce: [u8; NONCE_LEN], ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
416 let mut blob = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
417 blob.push(FORMAT);
418 blob.extend(nonce);
419 blob.extend(plaintext);
420 let ad = ctx.associated_data(FORMAT);
421 let tag = gcm_seal(key, nonce, &ad, &mut blob[1 + NONCE_LEN..]);
422 blob.extend(tag);
423 blob
424}
425
426fn aead_key(key: &[u8]) -> LessSafeKey {
427 LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).expect("every key is 32 bytes"))
428}
429
430fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
432 let tag = aead_key(key)
436 .seal_in_place_separate_tag(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
437 .expect("within GCM's limits");
438 tag.as_ref().try_into().expect("a 16-byte tag")
439}
440
441fn gcm_open(
444 key: &[u8],
445 nonce: [u8; NONCE_LEN],
446 ad: &[u8],
447 in_out: &mut [u8],
448) -> Result<usize, OpenError> {
449 aead_key(key)
450 .open_in_place(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
451 .map(|plaintext| plaintext.len())
452 .map_err(|_| OpenError)
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn every_byte_of_a_key_is_random() {
461 let keys: Vec<Key> = (0..8).map(|_| Key::generate().expect("key")).collect();
462 for i in 0..KEY_LEN {
463 let first = keys[0].bytes()[i];
464 assert!(
465 keys.iter().any(|k| k.bytes()[i] != first),
466 "byte {i} is {first} in all eight keys"
467 );
468 }
469 }
470
471 fn hex(text: &str) -> Vec<u8> {
472 (0..text.len())
473 .step_by(2)
474 .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex"))
475 .collect()
476 }
477
478 #[test]
482 fn aes_256_gcm_matches_the_published_vector() {
483 let key = hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
484 let nonce: [u8; 12] = hex("cafebabefacedbaddecaf888")
485 .try_into()
486 .expect("12 bytes");
487 let ad = hex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
488 let mut text = hex(concat!(
489 "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72",
490 "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"
491 ));
492 let tag = gcm_seal(&key, nonce, &ad, &mut text);
493 assert_eq!(
494 text,
495 hex(concat!(
496 "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa",
497 "8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"
498 )),
499 "the ciphertext is Test Case 16's"
500 );
501 assert_eq!(
502 tag.to_vec(),
503 hex("76fc6ece0f4e1768cddf8853bb2d551b"),
504 "the tag is Test Case 16's"
505 );
506 }
507
508 const TENANT: TenantId = TenantId([0x11; 16]);
509 const VERSION: VersionId = VersionId([0x22; 16]);
510 const UUID: Uuid = Uuid::from_bytes([0x33; 16]);
511
512 fn version_ctx() -> Context {
513 Context::Version {
514 tenant: TENANT,
515 uuid: UUID,
516 version: VERSION,
517 }
518 }
519
520 #[test]
524 fn a_blob_is_the_format_byte_nonce_ciphertext_and_tag() {
525 let key = [0x44; 32];
526 let nonce = [0x55; 12];
527 let mut ad = b"vcon-server/v1".to_vec();
528 ad.extend([1, 2]);
529 ad.extend([0x11; 16]);
530 ad.extend([0x33; 16]);
531 ad.extend([0x22; 16]);
532 let mut ciphertext = b"body".to_vec();
533 let tag = gcm_seal(&key, nonce, &ad, &mut ciphertext);
534 let mut expected = vec![1];
535 expected.extend(nonce);
536 expected.extend(ciphertext);
537 expected.extend(tag);
538 assert_eq!(
539 seal_with_nonce(&key, nonce, &version_ctx(), b"body"),
540 expected,
541 "format byte, nonce, ciphertext, tag, under a version's associated data"
542 );
543
544 let mut tenant_ad = b"vcon-server/v1".to_vec();
545 tenant_ad.extend([1, 1]);
546 tenant_ad.extend([0x11; 16]);
547 let mut ciphertext = b"key".to_vec();
548 let tag = gcm_seal(&key, nonce, &tenant_ad, &mut ciphertext);
549 let mut expected = vec![1];
550 expected.extend(nonce);
551 expected.extend(ciphertext);
552 expected.extend(tag);
553 let tenant = Context::Tenant { tenant: TENANT };
554 assert_eq!(
555 seal_with_nonce(&key, nonce, &tenant, b"key"),
556 expected,
557 "a tenant blob: the same layout under the tenant's associated data"
558 );
559 }
560
561 #[test]
562 fn a_sealed_body_opens_with_the_same_key_and_context() {
563 let key = Key::generate().expect("key");
564 for body in [&b""[..], b"x", &[0xa5; 100_000]] {
565 let blob = seal(&key, &version_ctx(), body);
566 assert_eq!(
567 blob.len(),
568 1 + 12 + body.len() + 16,
569 "a format byte, a 12-byte nonce, the ciphertext as long as the body, a 16-byte tag"
570 );
571 assert_eq!(
572 open(&key, &version_ctx(), &blob).as_deref(),
573 Ok(body),
574 "the same key and context open what they sealed"
575 );
576 }
577 }
578
579 #[test]
583 fn every_byte_of_a_nonce_is_random() {
584 let key = Key::generate().expect("key");
585 let nonces: Vec<[u8; 12]> = (0..1000)
586 .map(|_| {
587 let blob = seal(&key, &version_ctx(), b"same");
588 blob[1..13].try_into().expect("12 bytes")
589 })
590 .collect();
591 let distinct: std::collections::HashSet<_> = nonces.iter().collect();
592 assert_eq!(distinct.len(), nonces.len(), "a nonce repeated");
593 for i in 0..12 {
594 assert!(
595 nonces.iter().any(|n| n[i] != nonces[0][i]),
596 "byte {i} of the nonce never changes"
597 );
598 }
599 }
600
601 #[test]
602 fn changing_any_byte_of_a_blob_fails() {
603 let key = Key::generate().expect("key");
604 let blob = seal(&key, &version_ctx(), b"a body");
605 assert_eq!(
606 open(&key, &version_ctx(), &blob).as_deref(),
607 Ok(&b"a body"[..]),
608 "unchanged"
609 );
610 for i in 0..blob.len() {
611 let mut bad = blob.clone();
612 bad[i] ^= 0x01;
613 assert_eq!(open(&key, &version_ctx(), &bad), Err(OpenError), "byte {i}");
614 }
615 for len in 0..blob.len() {
616 assert_eq!(
617 open(&key, &version_ctx(), &blob[..len]),
618 Err(OpenError),
619 "first {len} bytes"
620 );
621 }
622 let mut longer = blob.clone();
623 longer.push(0);
624 assert_eq!(
625 open(&key, &version_ctx(), &longer),
626 Err(OpenError),
627 "one byte more"
628 );
629 }
630
631 #[test]
632 fn another_context_or_key_fails() {
633 let key = Key::generate().expect("key");
634 let blob = seal(&key, &version_ctx(), b"a body");
635 assert_eq!(
636 open(&key, &version_ctx(), &blob).as_deref(),
637 Ok(&b"a body"[..]),
638 "unchanged"
639 );
640 let others = [
641 Context::Version {
642 tenant: TenantId([0x12; 16]),
643 uuid: UUID,
644 version: VERSION,
645 },
646 Context::Version {
647 tenant: TENANT,
648 uuid: Uuid::from_bytes([0x34; 16]),
649 version: VERSION,
650 },
651 Context::Version {
652 tenant: TENANT,
653 uuid: UUID,
654 version: VersionId([0x23; 16]),
655 },
656 Context::Tenant { tenant: TENANT },
657 ];
658 for other in others {
659 assert_eq!(open(&key, &other, &blob), Err(OpenError), "{other:?}");
660 }
661 let other_key = Key::generate().expect("key");
662 assert_eq!(
663 open(&other_key, &version_ctx(), &blob),
664 Err(OpenError),
665 "the tag fails under any other key"
666 );
667 }
668
669 const TENANT_CTX: Context = Context::Tenant { tenant: TENANT };
670
671 #[test]
672 fn a_wrapped_key_unwraps_to_the_same_key() {
673 let wrapping = Key::generate().expect("key");
674 let key = Key::generate().expect("key");
675 for ctx in [TENANT_CTX, version_ctx()] {
676 let wrapped = wrap(&wrapping, &ctx, &key);
677 assert_eq!(
678 wrapped.len(),
679 1 + 12 + KEY_LEN + 16,
680 "a wrapped key is a sealed blob of the key's 32 bytes"
681 );
682 let unwrapped = unwrap(&wrapping, &ctx, &wrapped).expect("unwraps");
683 assert_eq!(
684 unwrapped.bytes(),
685 key.bytes(),
686 "unwrap gives back the key wrapped"
687 );
688 }
689 }
690
691 #[test]
692 fn changing_any_byte_of_a_wrapped_key_fails() {
693 let wrapping = Key::generate().expect("key");
694 let key = Key::generate().expect("key");
695 let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
696 assert!(
697 unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
698 "unchanged"
699 );
700 for i in 0..wrapped.len() {
701 let mut bad = wrapped.clone();
702 bad[i] ^= 0x01;
703 assert!(unwrap(&wrapping, &TENANT_CTX, &bad).is_err(), "byte {i}");
704 }
705 for len in 0..wrapped.len() {
706 assert!(
707 unwrap(&wrapping, &TENANT_CTX, &wrapped[..len]).is_err(),
708 "first {len} bytes"
709 );
710 }
711 }
712
713 #[test]
714 fn another_context_or_wrapping_key_fails() {
715 let wrapping = Key::generate().expect("key");
716 let key = Key::generate().expect("key");
717 let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
718 assert!(
719 unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
720 "unchanged"
721 );
722 let other_tenant = Context::Tenant {
723 tenant: TenantId([0x12; 16]),
724 };
725 assert!(
726 unwrap(&wrapping, &other_tenant, &wrapped).is_err(),
727 "other tenant"
728 );
729 assert!(
730 unwrap(&wrapping, &version_ctx(), &wrapped).is_err(),
731 "other kind"
732 );
733 let other = Key::generate().expect("key");
734 assert!(
735 unwrap(&other, &TENANT_CTX, &wrapped).is_err(),
736 "other wrapping key"
737 );
738 }
739
740 #[test]
741 fn only_a_32_byte_key_unwraps() {
742 let wrapping = Key::generate().expect("key");
743 for len in [0, 31, 33, 64] {
744 let sealed = seal(&wrapping, &TENANT_CTX, &vec![7; len]);
745 assert!(
746 unwrap(&wrapping, &TENANT_CTX, &sealed).is_err(),
747 "{len} bytes"
748 );
749 }
750 let sealed = seal(&wrapping, &TENANT_CTX, &[7; KEY_LEN]);
751 let key = unwrap(&wrapping, &TENANT_CTX, &sealed).expect("32 bytes unwrap");
752 assert_eq!(key.bytes(), [7; KEY_LEN], "the key is the 32 bytes sealed");
753 }
754
755 fn key_of(bytes: [u8; KEY_LEN]) -> Key {
757 let mut page = LockedPage::new().expect("page");
758 page.bytes_mut()[..KEY_LEN].copy_from_slice(&bytes);
759 Key { page }
760 }
761
762 #[test]
764 fn hkdf_sha256_matches_rfc_5869() {
765 let cases = [
766 (
767 "000102030405060708090a0b0c",
768 "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
769 "f0f1f2f3f4f5f6f7f8f9",
770 concat!(
771 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf",
772 "34007208d5b887185865"
773 ),
774 ),
775 (
776 concat!(
777 "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
778 "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
779 "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
780 ),
781 concat!(
782 "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
783 "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
784 "404142434445464748494a4b4c4d4e4f"
785 ),
786 concat!(
787 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
788 "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef",
789 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
790 ),
791 concat!(
792 "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c",
793 "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71",
794 "cc30c58179ec3e87c14c01d5c1f3434f1d87"
795 ),
796 ),
797 (
798 "",
799 "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
800 "",
801 concat!(
802 "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d",
803 "9d201395faa4b61a96c8"
804 ),
805 ),
806 ];
807 for (n, (salt, ikm, info, okm)) in cases.into_iter().enumerate() {
808 let expected = hex(okm);
809 let mut out = vec![0; expected.len()];
810 hkdf_sha256(&hex(salt), &hex(ikm), &hex(info), &mut out);
811 assert_eq!(out, expected, "test case {}", n + 1);
812 }
813 }
814
815 #[test]
819 fn a_catalog_key_is_hkdf_of_its_parent_with_the_catalog_label() {
820 let catalog = catalog_key(&key_of([0x44; KEY_LEN])).expect("derives");
821 assert_eq!(
822 catalog.bytes(),
823 hex("c0df4ea4f63e64687ca93c494fff4ca00b3ae13112766d8ea24cca35c6b1031b"),
824 "the value Python's HKDF gives for this parent and label"
825 );
826 }
827
828 #[test]
829 fn distinct_parents_or_labels_give_distinct_keys() {
830 let parent = Key::generate().expect("key");
831 let again = catalog_key(&parent).expect("derives");
832 assert_eq!(
833 catalog_key(&parent).expect("derives").bytes(),
834 again.bytes(),
835 "derivation is deterministic: the same parent gives the same catalog key"
836 );
837 let other = catalog_key(&Key::generate().expect("key")).expect("derives");
838 assert_ne!(
839 other.bytes(),
840 again.bytes(),
841 "another parent gives another catalog key"
842 );
843 let mut a = [0; KEY_LEN];
844 let mut b = [0; KEY_LEN];
845 hkdf_sha256(&[], parent.bytes(), b"label a", &mut a);
846 hkdf_sha256(&[], parent.bytes(), b"label b", &mut b);
847 assert_ne!(a, b, "another label gives another key from the same parent");
848 }
849
850 #[test]
851 fn a_rewrapped_key_opens_under_the_new_key_only() {
852 let old = Key::generate().expect("key");
853 let new = Key::generate().expect("key");
854 let key = Key::generate().expect("key");
855 let wrapped = wrap(&old, &version_ctx(), &key);
856 let rewrapped = rewrap(&old, &new, &version_ctx(), &wrapped).expect("rewraps");
857 let opened = unwrap(&new, &version_ctx(), &rewrapped).expect("opens under new");
858 assert_eq!(
859 opened.bytes(),
860 key.bytes(),
861 "rewrapping keeps the wrapped key itself"
862 );
863 assert!(unwrap(&old, &version_ctx(), &rewrapped).is_err(), "old key");
864 assert!(
865 unwrap(&new, &TENANT_CTX, &rewrapped).is_err(),
866 "context kept"
867 );
868 }
869
870 #[test]
871 fn rewrapping_needs_the_right_old_key_and_context() {
872 let old = Key::generate().expect("key");
873 let new = Key::generate().expect("key");
874 let wrapped = wrap(&old, &TENANT_CTX, &Key::generate().expect("key"));
875 assert!(
876 matches!(
877 rewrap(&new, &new, &TENANT_CTX, &wrapped),
878 Err(KeyError::Damaged)
879 ),
880 "the key was not wrapped under new, so new cannot unwrap it to rewrap"
881 );
882 assert!(
883 matches!(
884 rewrap(&old, &new, &version_ctx(), &wrapped),
885 Err(KeyError::Damaged)
886 ),
887 "the key was wrapped for the tenant context, not a version's"
888 );
889 }
890
891 #[test]
892 fn wrapping_warns_from_2_to_the_31_and_stops_at_2_to_the_32() {
893 let cases = [
894 (0, WrapLimit::Allow),
895 ((1 << 31) - 1, WrapLimit::Allow),
896 (1 << 31, WrapLimit::Warn),
897 ((1 << 32) - 1, WrapLimit::Warn),
898 (1 << 32, WrapLimit::Refuse),
899 (u64::MAX, WrapLimit::Refuse),
900 ];
901 for (wraps, expected) in cases {
902 assert_eq!(wrap_limit(wraps), expected, "{wraps} wraps");
903 }
904 }
905
906 #[test]
907 fn the_sqlcipher_literal_spells_the_key_in_hex() {
908 let mut bytes = [0; KEY_LEN];
909 bytes[0] = 0x0a;
910 bytes[31] = 0xff;
911 let literal = key_of(bytes).sqlcipher_raw();
912 let expected = format!("x'0a{}ff'", "00".repeat(30));
913 assert_eq!(
914 *literal, expected,
915 "SQLCipher's raw key literal is x'' around two lower-case hex digits per byte"
916 );
917 assert_eq!(literal.capacity(), literal.len(), "built at its final size");
918 }
919
920 #[test]
923 fn a_keys_debug_shows_its_id_and_none_of_its_bytes() {
924 let bytes: [u8; KEY_LEN] = std::array::from_fn(|i| 0xa0 ^ (5 * i as u8));
925 let key = key_of(bytes);
926 let shown = format!("{key:?}");
927 assert!(
928 shown.contains(&key.id().to_string()),
929 "the id tells keys apart in a log: {shown}"
930 );
931 for run in bytes.windows(4) {
932 let lower: String = run.iter().map(|b| format!("{b:02x}")).collect();
933 let upper = lower.to_uppercase();
934 let decimal = run.iter().map(u8::to_string).collect::<Vec<_>>().join(", ");
935 for rendering in [lower, upper, decimal] {
936 assert!(
937 !shown.contains(&rendering),
938 "no run of the key's bytes may be printed, found {rendering:?} in {shown}"
939 );
940 }
941 }
942 }
943
944 #[test]
945 fn keys_can_be_shared_between_threads() {
946 fn shared<T: Send + Sync>() {}
947 shared::<Key>();
948 }
949}