use std::fmt;
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use aws_lc_rs::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
use aws_lc_rs::digest::{SHA256, digest};
use aws_lc_rs::hkdf::{HKDF_SHA256, KeyType, Salt};
use uuid::Uuid;
use zeroize::Zeroizing;
use crate::ids::{KeyId, TenantId, VersionId};
mod locked;
use locked::LockedPage;
const KEY_LEN: usize = 32;
const FORMAT: u8 = 1;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
const LABEL: &[u8] = b"vcon-server/v1";
const CATALOG_LABEL: &[u8] = b"vcon-server/v1 catalog";
pub struct Key {
page: LockedPage,
}
impl fmt::Debug for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Key")
.field("id", &format_args!("{}", self.id()))
.finish_non_exhaustive()
}
}
impl Key {
pub fn generate() -> Result<Key, KeyError> {
let mut page = LockedPage::new().map_err(KeyError::Memory)?;
aws_lc_rs::rand::fill(&mut page.bytes_mut()[..KEY_LEN])
.expect("AWS-LC aborts instead of failing");
Ok(Key { page })
}
pub fn load_master(path: &Path) -> Result<Key, KeyError> {
let mut page = LockedPage::new().map_err(KeyError::Memory)?;
let mut file = File::open(path).map_err(KeyError::Read)?;
let buf = page.bytes_mut();
file.read_exact(&mut buf[..KEY_LEN])
.map_err(|e| match e.kind() {
io::ErrorKind::UnexpectedEof => KeyError::WrongSize,
_ => KeyError::Read(e),
})?;
if file
.read(&mut buf[KEY_LEN..=KEY_LEN])
.map_err(KeyError::Read)?
!= 0
{
return Err(KeyError::WrongSize);
}
Ok(Key { page })
}
#[must_use]
pub fn id(&self) -> KeyId {
let hash = digest(&SHA256, self.bytes());
KeyId(hash.as_ref()[..8].try_into().expect("SHA-256 is 32 bytes"))
}
fn bytes(&self) -> &[u8] {
&self.page.bytes()[..KEY_LEN]
}
pub(crate) fn sqlcipher_raw(&self) -> Zeroizing<String> {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut literal = Zeroizing::new(String::with_capacity(3 + 2 * KEY_LEN));
literal.push_str("x'");
for byte in self.bytes() {
literal.push(char::from(HEX[usize::from(byte >> 4)]));
literal.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
literal.push('\'');
literal
}
}
#[derive(Debug)]
pub enum KeyError {
Memory(io::Error),
Read(io::Error),
WrongSize,
Damaged,
}
impl fmt::Display for KeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
KeyError::Memory(_) => "the key's memory could not be locked or kept out of core dumps",
KeyError::Read(_) => "the master key file could not be read",
KeyError::WrongSize => "the master key file does not hold exactly 32 bytes",
KeyError::Damaged => "a wrapped key did not open",
})
}
}
impl std::error::Error for KeyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
KeyError::Memory(e) | KeyError::Read(e) => Some(e),
KeyError::WrongSize | KeyError::Damaged => None,
}
}
}
pub fn lock_all_memory() -> io::Result<()> {
if unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub fn disable_core_dumps() -> io::Result<()> {
let none = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &none) } != 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Context {
Tenant {
tenant: TenantId,
},
Version {
tenant: TenantId,
uuid: Uuid,
version: VersionId,
},
}
impl Context {
fn associated_data(&self, format: u8) -> Vec<u8> {
let mut ad = LABEL.to_vec();
ad.push(format);
match self {
Context::Tenant { tenant } => {
ad.push(1);
ad.extend(tenant.0);
}
Context::Version {
tenant,
uuid,
version,
} => {
ad.push(2);
ad.extend(tenant.0);
ad.extend(uuid.as_bytes());
ad.extend(version.0);
}
}
ad
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct OpenError;
impl fmt::Display for OpenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the blob did not open under this key and context")
}
}
impl std::error::Error for OpenError {}
pub(crate) const SEALED_OVERHEAD: usize = 1 + NONCE_LEN + TAG_LEN;
#[must_use]
pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
let nonce: [u8; NONCE_LEN] = aws_lc_rs::rand::generate(&aws_lc_rs::rand::SystemRandom::new())
.expect("AWS-LC aborts instead of failing")
.expose();
seal_with_nonce(data_key.bytes(), nonce, ctx, plaintext)
}
pub fn open(data_key: &Key, ctx: &Context, blob: &[u8]) -> Result<Vec<u8>, OpenError> {
let mut text = vec![0; blob.len().saturating_sub(1 + NONCE_LEN)];
let len = open_into(data_key.bytes(), ctx, blob, &mut text)?;
text.truncate(len);
Ok(text)
}
#[must_use]
pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
seal(wrapping, ctx, key.bytes())
}
pub fn unwrap(wrapping: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Key, KeyError> {
let mut page = LockedPage::new().map_err(KeyError::Memory)?;
match open_into(wrapping.bytes(), ctx, wrapped, page.bytes_mut()) {
Ok(KEY_LEN) => Ok(Key { page }),
_ => Err(KeyError::Damaged),
}
}
pub fn rewrap(old: &Key, new: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Vec<u8>, KeyError> {
let key = unwrap(old, ctx, wrapped)?;
Ok(wrap(new, ctx, &key))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrapLimit {
Allow,
Warn,
Refuse,
}
#[must_use]
pub fn wrap_limit(wraps: u64) -> WrapLimit {
const REFUSE_AT: u64 = 1 << 32;
const WARN_AT: u64 = 1 << 31;
if wraps >= REFUSE_AT {
WrapLimit::Refuse
} else if wraps >= WARN_AT {
WrapLimit::Warn
} else {
WrapLimit::Allow
}
}
fn open_into(key: &[u8], ctx: &Context, blob: &[u8], out: &mut [u8]) -> Result<usize, OpenError> {
let (&format, rest) = blob.split_first().ok_or(OpenError)?;
let (nonce, sealed) = rest.split_first_chunk::<NONCE_LEN>().ok_or(OpenError)?;
let text = out.get_mut(..sealed.len()).ok_or(OpenError)?;
text.copy_from_slice(sealed);
gcm_open(key, *nonce, &ctx.associated_data(format), text)
}
pub fn catalog_key(parent: &Key) -> Result<Key, KeyError> {
let mut page = LockedPage::new().map_err(KeyError::Memory)?;
hkdf_sha256(
&[],
parent.bytes(),
CATALOG_LABEL,
&mut page.bytes_mut()[..KEY_LEN],
);
Ok(Key { page })
}
fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) {
struct Len(usize);
impl KeyType for Len {
fn len(&self) -> usize {
self.0
}
}
Salt::new(HKDF_SHA256, salt)
.extract(ikm)
.expand(&[info], Len(out.len()))
.and_then(|okm| okm.fill(out))
.expect("within HKDF's output limit");
}
fn seal_with_nonce(key: &[u8], nonce: [u8; NONCE_LEN], ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
let mut blob = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
blob.push(FORMAT);
blob.extend(nonce);
blob.extend(plaintext);
let ad = ctx.associated_data(FORMAT);
let tag = gcm_seal(key, nonce, &ad, &mut blob[1 + NONCE_LEN..]);
blob.extend(tag);
blob
}
fn aead_key(key: &[u8]) -> LessSafeKey {
LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).expect("every key is 32 bytes"))
}
fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
let tag = aead_key(key)
.seal_in_place_separate_tag(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
.expect("within GCM's limits");
tag.as_ref().try_into().expect("a 16-byte tag")
}
fn gcm_open(
key: &[u8],
nonce: [u8; NONCE_LEN],
ad: &[u8],
in_out: &mut [u8],
) -> Result<usize, OpenError> {
aead_key(key)
.open_in_place(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
.map(|plaintext| plaintext.len())
.map_err(|_| OpenError)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_byte_of_a_key_is_random() {
let keys: Vec<Key> = (0..8).map(|_| Key::generate().expect("key")).collect();
for i in 0..KEY_LEN {
let first = keys[0].bytes()[i];
assert!(
keys.iter().any(|k| k.bytes()[i] != first),
"byte {i} is {first} in all eight keys"
);
}
}
fn hex(text: &str) -> Vec<u8> {
(0..text.len())
.step_by(2)
.map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex"))
.collect()
}
#[test]
fn aes_256_gcm_matches_the_published_vector() {
let key = hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
let nonce: [u8; 12] = hex("cafebabefacedbaddecaf888")
.try_into()
.expect("12 bytes");
let ad = hex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
let mut text = hex(concat!(
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72",
"1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"
));
let tag = gcm_seal(&key, nonce, &ad, &mut text);
assert_eq!(
text,
hex(concat!(
"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa",
"8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"
)),
"the ciphertext is Test Case 16's"
);
assert_eq!(
tag.to_vec(),
hex("76fc6ece0f4e1768cddf8853bb2d551b"),
"the tag is Test Case 16's"
);
}
const TENANT: TenantId = TenantId([0x11; 16]);
const VERSION: VersionId = VersionId([0x22; 16]);
const UUID: Uuid = Uuid::from_bytes([0x33; 16]);
fn version_ctx() -> Context {
Context::Version {
tenant: TENANT,
uuid: UUID,
version: VERSION,
}
}
#[test]
fn a_blob_is_the_format_byte_nonce_ciphertext_and_tag() {
let key = [0x44; 32];
let nonce = [0x55; 12];
let mut ad = b"vcon-server/v1".to_vec();
ad.extend([1, 2]);
ad.extend([0x11; 16]);
ad.extend([0x33; 16]);
ad.extend([0x22; 16]);
let mut ciphertext = b"body".to_vec();
let tag = gcm_seal(&key, nonce, &ad, &mut ciphertext);
let mut expected = vec![1];
expected.extend(nonce);
expected.extend(ciphertext);
expected.extend(tag);
assert_eq!(
seal_with_nonce(&key, nonce, &version_ctx(), b"body"),
expected,
"format byte, nonce, ciphertext, tag, under a version's associated data"
);
let mut tenant_ad = b"vcon-server/v1".to_vec();
tenant_ad.extend([1, 1]);
tenant_ad.extend([0x11; 16]);
let mut ciphertext = b"key".to_vec();
let tag = gcm_seal(&key, nonce, &tenant_ad, &mut ciphertext);
let mut expected = vec![1];
expected.extend(nonce);
expected.extend(ciphertext);
expected.extend(tag);
let tenant = Context::Tenant { tenant: TENANT };
assert_eq!(
seal_with_nonce(&key, nonce, &tenant, b"key"),
expected,
"a tenant blob: the same layout under the tenant's associated data"
);
}
#[test]
fn a_sealed_blob_is_its_body_and_the_stated_overhead() {
let key = Key::generate().expect("key");
let ctx = Context::Tenant {
tenant: crate::ids::TenantId([7; 16]),
};
for len in [0, 1, 1000] {
assert_eq!(
seal(&key, &ctx, &vec![0x5a; len]).len(),
len + SEALED_OVERHEAD,
"a body of {len} bytes"
);
}
}
#[test]
fn a_sealed_body_opens_with_the_same_key_and_context() {
let key = Key::generate().expect("key");
for body in [&b""[..], b"x", &[0xa5; 100_000]] {
let blob = seal(&key, &version_ctx(), body);
assert_eq!(
blob.len(),
1 + 12 + body.len() + 16,
"a format byte, a 12-byte nonce, the ciphertext as long as the body, a 16-byte tag"
);
assert_eq!(
open(&key, &version_ctx(), &blob).as_deref(),
Ok(body),
"the same key and context open what they sealed"
);
}
}
#[test]
fn every_byte_of_a_nonce_is_random() {
let key = Key::generate().expect("key");
let nonces: Vec<[u8; 12]> = (0..1000)
.map(|_| {
let blob = seal(&key, &version_ctx(), b"same");
blob[1..13].try_into().expect("12 bytes")
})
.collect();
let distinct: std::collections::HashSet<_> = nonces.iter().collect();
assert_eq!(distinct.len(), nonces.len(), "a nonce repeated");
for i in 0..12 {
assert!(
nonces.iter().any(|n| n[i] != nonces[0][i]),
"byte {i} of the nonce never changes"
);
}
}
#[test]
fn changing_any_byte_of_a_blob_fails() {
let key = Key::generate().expect("key");
let blob = seal(&key, &version_ctx(), b"a body");
assert_eq!(
open(&key, &version_ctx(), &blob).as_deref(),
Ok(&b"a body"[..]),
"unchanged"
);
for i in 0..blob.len() {
let mut bad = blob.clone();
bad[i] ^= 0x01;
assert_eq!(open(&key, &version_ctx(), &bad), Err(OpenError), "byte {i}");
}
for len in 0..blob.len() {
assert_eq!(
open(&key, &version_ctx(), &blob[..len]),
Err(OpenError),
"first {len} bytes"
);
}
let mut longer = blob.clone();
longer.push(0);
assert_eq!(
open(&key, &version_ctx(), &longer),
Err(OpenError),
"one byte more"
);
}
#[test]
fn another_context_or_key_fails() {
let key = Key::generate().expect("key");
let blob = seal(&key, &version_ctx(), b"a body");
assert_eq!(
open(&key, &version_ctx(), &blob).as_deref(),
Ok(&b"a body"[..]),
"unchanged"
);
let others = [
Context::Version {
tenant: TenantId([0x12; 16]),
uuid: UUID,
version: VERSION,
},
Context::Version {
tenant: TENANT,
uuid: Uuid::from_bytes([0x34; 16]),
version: VERSION,
},
Context::Version {
tenant: TENANT,
uuid: UUID,
version: VersionId([0x23; 16]),
},
Context::Tenant { tenant: TENANT },
];
for other in others {
assert_eq!(open(&key, &other, &blob), Err(OpenError), "{other:?}");
}
let other_key = Key::generate().expect("key");
assert_eq!(
open(&other_key, &version_ctx(), &blob),
Err(OpenError),
"the tag fails under any other key"
);
}
const TENANT_CTX: Context = Context::Tenant { tenant: TENANT };
#[test]
fn a_wrapped_key_unwraps_to_the_same_key() {
let wrapping = Key::generate().expect("key");
let key = Key::generate().expect("key");
for ctx in [TENANT_CTX, version_ctx()] {
let wrapped = wrap(&wrapping, &ctx, &key);
assert_eq!(
wrapped.len(),
1 + 12 + KEY_LEN + 16,
"a wrapped key is a sealed blob of the key's 32 bytes"
);
let unwrapped = unwrap(&wrapping, &ctx, &wrapped).expect("unwraps");
assert_eq!(
unwrapped.bytes(),
key.bytes(),
"unwrap gives back the key wrapped"
);
}
}
#[test]
fn changing_any_byte_of_a_wrapped_key_fails() {
let wrapping = Key::generate().expect("key");
let key = Key::generate().expect("key");
let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
assert!(
unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
"unchanged"
);
for i in 0..wrapped.len() {
let mut bad = wrapped.clone();
bad[i] ^= 0x01;
assert!(unwrap(&wrapping, &TENANT_CTX, &bad).is_err(), "byte {i}");
}
for len in 0..wrapped.len() {
assert!(
unwrap(&wrapping, &TENANT_CTX, &wrapped[..len]).is_err(),
"first {len} bytes"
);
}
}
#[test]
fn another_context_or_wrapping_key_fails() {
let wrapping = Key::generate().expect("key");
let key = Key::generate().expect("key");
let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
assert!(
unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
"unchanged"
);
let other_tenant = Context::Tenant {
tenant: TenantId([0x12; 16]),
};
assert!(
unwrap(&wrapping, &other_tenant, &wrapped).is_err(),
"other tenant"
);
assert!(
unwrap(&wrapping, &version_ctx(), &wrapped).is_err(),
"other kind"
);
let other = Key::generate().expect("key");
assert!(
unwrap(&other, &TENANT_CTX, &wrapped).is_err(),
"other wrapping key"
);
}
#[test]
fn only_a_32_byte_key_unwraps() {
let wrapping = Key::generate().expect("key");
for len in [0, 31, 33, 64] {
let sealed = seal(&wrapping, &TENANT_CTX, &vec![7; len]);
assert!(
unwrap(&wrapping, &TENANT_CTX, &sealed).is_err(),
"{len} bytes"
);
}
let sealed = seal(&wrapping, &TENANT_CTX, &[7; KEY_LEN]);
let key = unwrap(&wrapping, &TENANT_CTX, &sealed).expect("32 bytes unwrap");
assert_eq!(key.bytes(), [7; KEY_LEN], "the key is the 32 bytes sealed");
}
fn key_of(bytes: [u8; KEY_LEN]) -> Key {
let mut page = LockedPage::new().expect("page");
page.bytes_mut()[..KEY_LEN].copy_from_slice(&bytes);
Key { page }
}
#[test]
fn hkdf_sha256_matches_rfc_5869() {
let cases = [
(
"000102030405060708090a0b0c",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"f0f1f2f3f4f5f6f7f8f9",
concat!(
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf",
"34007208d5b887185865"
),
),
(
concat!(
"606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
),
concat!(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
"202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
"404142434445464748494a4b4c4d4e4f"
),
concat!(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef",
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
),
concat!(
"b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c",
"59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71",
"cc30c58179ec3e87c14c01d5c1f3434f1d87"
),
),
(
"",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"",
concat!(
"8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d",
"9d201395faa4b61a96c8"
),
),
];
for (n, (salt, ikm, info, okm)) in cases.into_iter().enumerate() {
let expected = hex(okm);
let mut out = vec![0; expected.len()];
hkdf_sha256(&hex(salt), &hex(ikm), &hex(info), &mut out);
assert_eq!(out, expected, "test case {}", n + 1);
}
}
#[test]
fn a_catalog_key_is_hkdf_of_its_parent_with_the_catalog_label() {
let catalog = catalog_key(&key_of([0x44; KEY_LEN])).expect("derives");
assert_eq!(
catalog.bytes(),
hex("c0df4ea4f63e64687ca93c494fff4ca00b3ae13112766d8ea24cca35c6b1031b"),
"the value Python's HKDF gives for this parent and label"
);
}
#[test]
fn distinct_parents_or_labels_give_distinct_keys() {
let parent = Key::generate().expect("key");
let again = catalog_key(&parent).expect("derives");
assert_eq!(
catalog_key(&parent).expect("derives").bytes(),
again.bytes(),
"derivation is deterministic: the same parent gives the same catalog key"
);
let other = catalog_key(&Key::generate().expect("key")).expect("derives");
assert_ne!(
other.bytes(),
again.bytes(),
"another parent gives another catalog key"
);
let mut a = [0; KEY_LEN];
let mut b = [0; KEY_LEN];
hkdf_sha256(&[], parent.bytes(), b"label a", &mut a);
hkdf_sha256(&[], parent.bytes(), b"label b", &mut b);
assert_ne!(a, b, "another label gives another key from the same parent");
}
#[test]
fn a_rewrapped_key_opens_under_the_new_key_only() {
let old = Key::generate().expect("key");
let new = Key::generate().expect("key");
let key = Key::generate().expect("key");
let wrapped = wrap(&old, &version_ctx(), &key);
let rewrapped = rewrap(&old, &new, &version_ctx(), &wrapped).expect("rewraps");
let opened = unwrap(&new, &version_ctx(), &rewrapped).expect("opens under new");
assert_eq!(
opened.bytes(),
key.bytes(),
"rewrapping keeps the wrapped key itself"
);
assert!(unwrap(&old, &version_ctx(), &rewrapped).is_err(), "old key");
assert!(
unwrap(&new, &TENANT_CTX, &rewrapped).is_err(),
"context kept"
);
}
#[test]
fn rewrapping_needs_the_right_old_key_and_context() {
let old = Key::generate().expect("key");
let new = Key::generate().expect("key");
let wrapped = wrap(&old, &TENANT_CTX, &Key::generate().expect("key"));
assert!(
matches!(
rewrap(&new, &new, &TENANT_CTX, &wrapped),
Err(KeyError::Damaged)
),
"the key was not wrapped under new, so new cannot unwrap it to rewrap"
);
assert!(
matches!(
rewrap(&old, &new, &version_ctx(), &wrapped),
Err(KeyError::Damaged)
),
"the key was wrapped for the tenant context, not a version's"
);
}
#[test]
fn wrapping_warns_from_2_to_the_31_and_stops_at_2_to_the_32() {
let cases = [
(0, WrapLimit::Allow),
((1 << 31) - 1, WrapLimit::Allow),
(1 << 31, WrapLimit::Warn),
((1 << 32) - 1, WrapLimit::Warn),
(1 << 32, WrapLimit::Refuse),
(u64::MAX, WrapLimit::Refuse),
];
for (wraps, expected) in cases {
assert_eq!(wrap_limit(wraps), expected, "{wraps} wraps");
}
}
#[test]
fn the_sqlcipher_literal_spells_the_key_in_hex() {
let mut bytes = [0; KEY_LEN];
bytes[0] = 0x0a;
bytes[31] = 0xff;
let literal = key_of(bytes).sqlcipher_raw();
let expected = format!("x'0a{}ff'", "00".repeat(30));
assert_eq!(
*literal, expected,
"SQLCipher's raw key literal is x'' around two lower-case hex digits per byte"
);
assert_eq!(literal.capacity(), literal.len(), "built at its final size");
}
#[test]
fn a_keys_debug_shows_its_id_and_none_of_its_bytes() {
let bytes: [u8; KEY_LEN] = std::array::from_fn(|i| 0xa0 ^ (5 * i as u8));
let key = key_of(bytes);
let shown = format!("{key:?}");
assert!(
shown.contains(&key.id().to_string()),
"the id tells keys apart in a log: {shown}"
);
for run in bytes.windows(4) {
let lower: String = run.iter().map(|b| format!("{b:02x}")).collect();
let upper = lower.to_uppercase();
let decimal = run.iter().map(u8::to_string).collect::<Vec<_>>().join(", ");
for rendering in [lower, upper, decimal] {
assert!(
!shown.contains(&rendering),
"no run of the key's bytes may be printed, found {rendering:?} in {shown}"
);
}
}
}
#[test]
fn keys_can_be_shared_between_threads() {
fn shared<T: Send + Sync>() {}
shared::<Key>();
}
}