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
271pub(crate) const SEALED_OVERHEAD: usize = 1 + NONCE_LEN + TAG_LEN;
274
275#[must_use]
284pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
285 let nonce: [u8; NONCE_LEN] = aws_lc_rs::rand::generate(&aws_lc_rs::rand::SystemRandom::new())
288 .expect("AWS-LC aborts instead of failing")
289 .expose();
290 seal_with_nonce(data_key.bytes(), nonce, ctx, plaintext)
291}
292
293pub fn open(data_key: &Key, ctx: &Context, blob: &[u8]) -> Result<Vec<u8>, OpenError> {
301 let mut text = vec![0; blob.len().saturating_sub(1 + NONCE_LEN)];
302 let len = open_into(data_key.bytes(), ctx, blob, &mut text)?;
303 text.truncate(len);
304 Ok(text)
305}
306
307#[must_use]
310pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
311 seal(wrapping, ctx, key.bytes())
312}
313
314pub fn unwrap(wrapping: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Key, KeyError> {
325 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
326 match open_into(wrapping.bytes(), ctx, wrapped, page.bytes_mut()) {
327 Ok(KEY_LEN) => Ok(Key { page }),
328 _ => Err(KeyError::Damaged),
329 }
330}
331
332pub fn rewrap(old: &Key, new: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Vec<u8>, KeyError> {
340 let key = unwrap(old, ctx, wrapped)?;
341 Ok(wrap(new, ctx, &key))
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum WrapLimit {
347 Allow,
349 Warn,
351 Refuse,
353}
354
355#[must_use]
360pub fn wrap_limit(wraps: u64) -> WrapLimit {
361 const REFUSE_AT: u64 = 1 << 32;
362 const WARN_AT: u64 = 1 << 31;
363 if wraps >= REFUSE_AT {
364 WrapLimit::Refuse
365 } else if wraps >= WARN_AT {
366 WrapLimit::Warn
367 } else {
368 WrapLimit::Allow
369 }
370}
371
372fn open_into(key: &[u8], ctx: &Context, blob: &[u8], out: &mut [u8]) -> Result<usize, OpenError> {
375 let (&format, rest) = blob.split_first().ok_or(OpenError)?;
376 let (nonce, sealed) = rest.split_first_chunk::<NONCE_LEN>().ok_or(OpenError)?;
377 let text = out.get_mut(..sealed.len()).ok_or(OpenError)?;
378 text.copy_from_slice(sealed);
379 gcm_open(key, *nonce, &ctx.associated_data(format), text)
380}
381
382pub fn catalog_key(parent: &Key) -> Result<Key, KeyError> {
392 let mut page = LockedPage::new().map_err(KeyError::Memory)?;
393 hkdf_sha256(
394 &[],
395 parent.bytes(),
396 CATALOG_LABEL,
397 &mut page.bytes_mut()[..KEY_LEN],
398 );
399 Ok(Key { page })
400}
401
402fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) {
404 struct Len(usize);
405 impl KeyType for Len {
406 fn len(&self) -> usize {
407 self.0
408 }
409 }
410 Salt::new(HKDF_SHA256, salt)
413 .extract(ikm)
414 .expand(&[info], Len(out.len()))
415 .and_then(|okm| okm.fill(out))
416 .expect("within HKDF's output limit");
417}
418
419fn seal_with_nonce(key: &[u8], nonce: [u8; NONCE_LEN], ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
420 let mut blob = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
421 blob.push(FORMAT);
422 blob.extend(nonce);
423 blob.extend(plaintext);
424 let ad = ctx.associated_data(FORMAT);
425 let tag = gcm_seal(key, nonce, &ad, &mut blob[1 + NONCE_LEN..]);
426 blob.extend(tag);
427 blob
428}
429
430fn aead_key(key: &[u8]) -> LessSafeKey {
431 LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).expect("every key is 32 bytes"))
432}
433
434fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
436 let tag = aead_key(key)
440 .seal_in_place_separate_tag(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
441 .expect("within GCM's limits");
442 tag.as_ref().try_into().expect("a 16-byte tag")
443}
444
445fn gcm_open(
448 key: &[u8],
449 nonce: [u8; NONCE_LEN],
450 ad: &[u8],
451 in_out: &mut [u8],
452) -> Result<usize, OpenError> {
453 aead_key(key)
454 .open_in_place(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
455 .map(|plaintext| plaintext.len())
456 .map_err(|_| OpenError)
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn every_byte_of_a_key_is_random() {
465 let keys: Vec<Key> = (0..8).map(|_| Key::generate().expect("key")).collect();
466 for i in 0..KEY_LEN {
467 let first = keys[0].bytes()[i];
468 assert!(
469 keys.iter().any(|k| k.bytes()[i] != first),
470 "byte {i} is {first} in all eight keys"
471 );
472 }
473 }
474
475 fn hex(text: &str) -> Vec<u8> {
476 (0..text.len())
477 .step_by(2)
478 .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex"))
479 .collect()
480 }
481
482 #[test]
486 fn aes_256_gcm_matches_the_published_vector() {
487 let key = hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
488 let nonce: [u8; 12] = hex("cafebabefacedbaddecaf888")
489 .try_into()
490 .expect("12 bytes");
491 let ad = hex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
492 let mut text = hex(concat!(
493 "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72",
494 "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"
495 ));
496 let tag = gcm_seal(&key, nonce, &ad, &mut text);
497 assert_eq!(
498 text,
499 hex(concat!(
500 "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa",
501 "8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"
502 )),
503 "the ciphertext is Test Case 16's"
504 );
505 assert_eq!(
506 tag.to_vec(),
507 hex("76fc6ece0f4e1768cddf8853bb2d551b"),
508 "the tag is Test Case 16's"
509 );
510 }
511
512 const TENANT: TenantId = TenantId([0x11; 16]);
513 const VERSION: VersionId = VersionId([0x22; 16]);
514 const UUID: Uuid = Uuid::from_bytes([0x33; 16]);
515
516 fn version_ctx() -> Context {
517 Context::Version {
518 tenant: TENANT,
519 uuid: UUID,
520 version: VERSION,
521 }
522 }
523
524 #[test]
528 fn a_blob_is_the_format_byte_nonce_ciphertext_and_tag() {
529 let key = [0x44; 32];
530 let nonce = [0x55; 12];
531 let mut ad = b"vcon-server/v1".to_vec();
532 ad.extend([1, 2]);
533 ad.extend([0x11; 16]);
534 ad.extend([0x33; 16]);
535 ad.extend([0x22; 16]);
536 let mut ciphertext = b"body".to_vec();
537 let tag = gcm_seal(&key, nonce, &ad, &mut ciphertext);
538 let mut expected = vec![1];
539 expected.extend(nonce);
540 expected.extend(ciphertext);
541 expected.extend(tag);
542 assert_eq!(
543 seal_with_nonce(&key, nonce, &version_ctx(), b"body"),
544 expected,
545 "format byte, nonce, ciphertext, tag, under a version's associated data"
546 );
547
548 let mut tenant_ad = b"vcon-server/v1".to_vec();
549 tenant_ad.extend([1, 1]);
550 tenant_ad.extend([0x11; 16]);
551 let mut ciphertext = b"key".to_vec();
552 let tag = gcm_seal(&key, nonce, &tenant_ad, &mut ciphertext);
553 let mut expected = vec![1];
554 expected.extend(nonce);
555 expected.extend(ciphertext);
556 expected.extend(tag);
557 let tenant = Context::Tenant { tenant: TENANT };
558 assert_eq!(
559 seal_with_nonce(&key, nonce, &tenant, b"key"),
560 expected,
561 "a tenant blob: the same layout under the tenant's associated data"
562 );
563 }
564
565 #[test]
566 fn a_sealed_blob_is_its_body_and_the_stated_overhead() {
567 let key = Key::generate().expect("key");
568 let ctx = Context::Tenant {
569 tenant: crate::ids::TenantId([7; 16]),
570 };
571 for len in [0, 1, 1000] {
572 assert_eq!(
573 seal(&key, &ctx, &vec![0x5a; len]).len(),
574 len + SEALED_OVERHEAD,
575 "a body of {len} bytes"
576 );
577 }
578 }
579
580 #[test]
581 fn a_sealed_body_opens_with_the_same_key_and_context() {
582 let key = Key::generate().expect("key");
583 for body in [&b""[..], b"x", &[0xa5; 100_000]] {
584 let blob = seal(&key, &version_ctx(), body);
585 assert_eq!(
586 blob.len(),
587 1 + 12 + body.len() + 16,
588 "a format byte, a 12-byte nonce, the ciphertext as long as the body, a 16-byte tag"
589 );
590 assert_eq!(
591 open(&key, &version_ctx(), &blob).as_deref(),
592 Ok(body),
593 "the same key and context open what they sealed"
594 );
595 }
596 }
597
598 #[test]
602 fn every_byte_of_a_nonce_is_random() {
603 let key = Key::generate().expect("key");
604 let nonces: Vec<[u8; 12]> = (0..1000)
605 .map(|_| {
606 let blob = seal(&key, &version_ctx(), b"same");
607 blob[1..13].try_into().expect("12 bytes")
608 })
609 .collect();
610 let distinct: std::collections::HashSet<_> = nonces.iter().collect();
611 assert_eq!(distinct.len(), nonces.len(), "a nonce repeated");
612 for i in 0..12 {
613 assert!(
614 nonces.iter().any(|n| n[i] != nonces[0][i]),
615 "byte {i} of the nonce never changes"
616 );
617 }
618 }
619
620 #[test]
621 fn changing_any_byte_of_a_blob_fails() {
622 let key = Key::generate().expect("key");
623 let blob = seal(&key, &version_ctx(), b"a body");
624 assert_eq!(
625 open(&key, &version_ctx(), &blob).as_deref(),
626 Ok(&b"a body"[..]),
627 "unchanged"
628 );
629 for i in 0..blob.len() {
630 let mut bad = blob.clone();
631 bad[i] ^= 0x01;
632 assert_eq!(open(&key, &version_ctx(), &bad), Err(OpenError), "byte {i}");
633 }
634 for len in 0..blob.len() {
635 assert_eq!(
636 open(&key, &version_ctx(), &blob[..len]),
637 Err(OpenError),
638 "first {len} bytes"
639 );
640 }
641 let mut longer = blob.clone();
642 longer.push(0);
643 assert_eq!(
644 open(&key, &version_ctx(), &longer),
645 Err(OpenError),
646 "one byte more"
647 );
648 }
649
650 #[test]
651 fn another_context_or_key_fails() {
652 let key = Key::generate().expect("key");
653 let blob = seal(&key, &version_ctx(), b"a body");
654 assert_eq!(
655 open(&key, &version_ctx(), &blob).as_deref(),
656 Ok(&b"a body"[..]),
657 "unchanged"
658 );
659 let others = [
660 Context::Version {
661 tenant: TenantId([0x12; 16]),
662 uuid: UUID,
663 version: VERSION,
664 },
665 Context::Version {
666 tenant: TENANT,
667 uuid: Uuid::from_bytes([0x34; 16]),
668 version: VERSION,
669 },
670 Context::Version {
671 tenant: TENANT,
672 uuid: UUID,
673 version: VersionId([0x23; 16]),
674 },
675 Context::Tenant { tenant: TENANT },
676 ];
677 for other in others {
678 assert_eq!(open(&key, &other, &blob), Err(OpenError), "{other:?}");
679 }
680 let other_key = Key::generate().expect("key");
681 assert_eq!(
682 open(&other_key, &version_ctx(), &blob),
683 Err(OpenError),
684 "the tag fails under any other key"
685 );
686 }
687
688 const TENANT_CTX: Context = Context::Tenant { tenant: TENANT };
689
690 #[test]
691 fn a_wrapped_key_unwraps_to_the_same_key() {
692 let wrapping = Key::generate().expect("key");
693 let key = Key::generate().expect("key");
694 for ctx in [TENANT_CTX, version_ctx()] {
695 let wrapped = wrap(&wrapping, &ctx, &key);
696 assert_eq!(
697 wrapped.len(),
698 1 + 12 + KEY_LEN + 16,
699 "a wrapped key is a sealed blob of the key's 32 bytes"
700 );
701 let unwrapped = unwrap(&wrapping, &ctx, &wrapped).expect("unwraps");
702 assert_eq!(
703 unwrapped.bytes(),
704 key.bytes(),
705 "unwrap gives back the key wrapped"
706 );
707 }
708 }
709
710 #[test]
711 fn changing_any_byte_of_a_wrapped_key_fails() {
712 let wrapping = Key::generate().expect("key");
713 let key = Key::generate().expect("key");
714 let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
715 assert!(
716 unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
717 "unchanged"
718 );
719 for i in 0..wrapped.len() {
720 let mut bad = wrapped.clone();
721 bad[i] ^= 0x01;
722 assert!(unwrap(&wrapping, &TENANT_CTX, &bad).is_err(), "byte {i}");
723 }
724 for len in 0..wrapped.len() {
725 assert!(
726 unwrap(&wrapping, &TENANT_CTX, &wrapped[..len]).is_err(),
727 "first {len} bytes"
728 );
729 }
730 }
731
732 #[test]
733 fn another_context_or_wrapping_key_fails() {
734 let wrapping = Key::generate().expect("key");
735 let key = Key::generate().expect("key");
736 let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
737 assert!(
738 unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
739 "unchanged"
740 );
741 let other_tenant = Context::Tenant {
742 tenant: TenantId([0x12; 16]),
743 };
744 assert!(
745 unwrap(&wrapping, &other_tenant, &wrapped).is_err(),
746 "other tenant"
747 );
748 assert!(
749 unwrap(&wrapping, &version_ctx(), &wrapped).is_err(),
750 "other kind"
751 );
752 let other = Key::generate().expect("key");
753 assert!(
754 unwrap(&other, &TENANT_CTX, &wrapped).is_err(),
755 "other wrapping key"
756 );
757 }
758
759 #[test]
760 fn only_a_32_byte_key_unwraps() {
761 let wrapping = Key::generate().expect("key");
762 for len in [0, 31, 33, 64] {
763 let sealed = seal(&wrapping, &TENANT_CTX, &vec![7; len]);
764 assert!(
765 unwrap(&wrapping, &TENANT_CTX, &sealed).is_err(),
766 "{len} bytes"
767 );
768 }
769 let sealed = seal(&wrapping, &TENANT_CTX, &[7; KEY_LEN]);
770 let key = unwrap(&wrapping, &TENANT_CTX, &sealed).expect("32 bytes unwrap");
771 assert_eq!(key.bytes(), [7; KEY_LEN], "the key is the 32 bytes sealed");
772 }
773
774 fn key_of(bytes: [u8; KEY_LEN]) -> Key {
776 let mut page = LockedPage::new().expect("page");
777 page.bytes_mut()[..KEY_LEN].copy_from_slice(&bytes);
778 Key { page }
779 }
780
781 #[test]
783 fn hkdf_sha256_matches_rfc_5869() {
784 let cases = [
785 (
786 "000102030405060708090a0b0c",
787 "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
788 "f0f1f2f3f4f5f6f7f8f9",
789 concat!(
790 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf",
791 "34007208d5b887185865"
792 ),
793 ),
794 (
795 concat!(
796 "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
797 "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
798 "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
799 ),
800 concat!(
801 "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
802 "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
803 "404142434445464748494a4b4c4d4e4f"
804 ),
805 concat!(
806 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
807 "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef",
808 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
809 ),
810 concat!(
811 "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c",
812 "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71",
813 "cc30c58179ec3e87c14c01d5c1f3434f1d87"
814 ),
815 ),
816 (
817 "",
818 "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
819 "",
820 concat!(
821 "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d",
822 "9d201395faa4b61a96c8"
823 ),
824 ),
825 ];
826 for (n, (salt, ikm, info, okm)) in cases.into_iter().enumerate() {
827 let expected = hex(okm);
828 let mut out = vec![0; expected.len()];
829 hkdf_sha256(&hex(salt), &hex(ikm), &hex(info), &mut out);
830 assert_eq!(out, expected, "test case {}", n + 1);
831 }
832 }
833
834 #[test]
838 fn a_catalog_key_is_hkdf_of_its_parent_with_the_catalog_label() {
839 let catalog = catalog_key(&key_of([0x44; KEY_LEN])).expect("derives");
840 assert_eq!(
841 catalog.bytes(),
842 hex("c0df4ea4f63e64687ca93c494fff4ca00b3ae13112766d8ea24cca35c6b1031b"),
843 "the value Python's HKDF gives for this parent and label"
844 );
845 }
846
847 #[test]
848 fn distinct_parents_or_labels_give_distinct_keys() {
849 let parent = Key::generate().expect("key");
850 let again = catalog_key(&parent).expect("derives");
851 assert_eq!(
852 catalog_key(&parent).expect("derives").bytes(),
853 again.bytes(),
854 "derivation is deterministic: the same parent gives the same catalog key"
855 );
856 let other = catalog_key(&Key::generate().expect("key")).expect("derives");
857 assert_ne!(
858 other.bytes(),
859 again.bytes(),
860 "another parent gives another catalog key"
861 );
862 let mut a = [0; KEY_LEN];
863 let mut b = [0; KEY_LEN];
864 hkdf_sha256(&[], parent.bytes(), b"label a", &mut a);
865 hkdf_sha256(&[], parent.bytes(), b"label b", &mut b);
866 assert_ne!(a, b, "another label gives another key from the same parent");
867 }
868
869 #[test]
870 fn a_rewrapped_key_opens_under_the_new_key_only() {
871 let old = Key::generate().expect("key");
872 let new = Key::generate().expect("key");
873 let key = Key::generate().expect("key");
874 let wrapped = wrap(&old, &version_ctx(), &key);
875 let rewrapped = rewrap(&old, &new, &version_ctx(), &wrapped).expect("rewraps");
876 let opened = unwrap(&new, &version_ctx(), &rewrapped).expect("opens under new");
877 assert_eq!(
878 opened.bytes(),
879 key.bytes(),
880 "rewrapping keeps the wrapped key itself"
881 );
882 assert!(unwrap(&old, &version_ctx(), &rewrapped).is_err(), "old key");
883 assert!(
884 unwrap(&new, &TENANT_CTX, &rewrapped).is_err(),
885 "context kept"
886 );
887 }
888
889 #[test]
890 fn rewrapping_needs_the_right_old_key_and_context() {
891 let old = Key::generate().expect("key");
892 let new = Key::generate().expect("key");
893 let wrapped = wrap(&old, &TENANT_CTX, &Key::generate().expect("key"));
894 assert!(
895 matches!(
896 rewrap(&new, &new, &TENANT_CTX, &wrapped),
897 Err(KeyError::Damaged)
898 ),
899 "the key was not wrapped under new, so new cannot unwrap it to rewrap"
900 );
901 assert!(
902 matches!(
903 rewrap(&old, &new, &version_ctx(), &wrapped),
904 Err(KeyError::Damaged)
905 ),
906 "the key was wrapped for the tenant context, not a version's"
907 );
908 }
909
910 #[test]
911 fn wrapping_warns_from_2_to_the_31_and_stops_at_2_to_the_32() {
912 let cases = [
913 (0, WrapLimit::Allow),
914 ((1 << 31) - 1, WrapLimit::Allow),
915 (1 << 31, WrapLimit::Warn),
916 ((1 << 32) - 1, WrapLimit::Warn),
917 (1 << 32, WrapLimit::Refuse),
918 (u64::MAX, WrapLimit::Refuse),
919 ];
920 for (wraps, expected) in cases {
921 assert_eq!(wrap_limit(wraps), expected, "{wraps} wraps");
922 }
923 }
924
925 #[test]
926 fn the_sqlcipher_literal_spells_the_key_in_hex() {
927 let mut bytes = [0; KEY_LEN];
928 bytes[0] = 0x0a;
929 bytes[31] = 0xff;
930 let literal = key_of(bytes).sqlcipher_raw();
931 let expected = format!("x'0a{}ff'", "00".repeat(30));
932 assert_eq!(
933 *literal, expected,
934 "SQLCipher's raw key literal is x'' around two lower-case hex digits per byte"
935 );
936 assert_eq!(literal.capacity(), literal.len(), "built at its final size");
937 }
938
939 #[test]
942 fn a_keys_debug_shows_its_id_and_none_of_its_bytes() {
943 let bytes: [u8; KEY_LEN] = std::array::from_fn(|i| 0xa0 ^ (5 * i as u8));
944 let key = key_of(bytes);
945 let shown = format!("{key:?}");
946 assert!(
947 shown.contains(&key.id().to_string()),
948 "the id tells keys apart in a log: {shown}"
949 );
950 for run in bytes.windows(4) {
951 let lower: String = run.iter().map(|b| format!("{b:02x}")).collect();
952 let upper = lower.to_uppercase();
953 let decimal = run.iter().map(u8::to_string).collect::<Vec<_>>().join(", ");
954 for rendering in [lower, upper, decimal] {
955 assert!(
956 !shown.contains(&rendering),
957 "no run of the key's bytes may be printed, found {rendering:?} in {shown}"
958 );
959 }
960 }
961 }
962
963 #[test]
964 fn keys_can_be_shared_between_threads() {
965 fn shared<T: Send + Sync>() {}
966 shared::<Key>();
967 }
968}