Skip to main content

vcon_server/
envelope.rs

1//! Keys and the encryption that protects every stored body.
2
3use 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
20/// The length of every key, in bytes.
21const KEY_LEN: usize = 32;
22
23/// The first byte of every blob, naming its layout and algorithm:
24/// AES-256-GCM with a 96-bit nonce and a 128-bit tag.
25const FORMAT: u8 = 1;
26const NONCE_LEN: usize = 12;
27const TAG_LEN: usize = 16;
28
29/// The start of every associated data.
30const LABEL: &[u8] = b"vcon-server/v1";
31
32/// The HKDF info that makes a catalog key.
33const CATALOG_LABEL: &[u8] = b"vcon-server/v1 catalog";
34
35/// A 256-bit key. Its bytes live only in a page of their own that is locked
36/// in memory, so they never reach swap, and that core dumps leave out; they
37/// are zeroed when the key is dropped. A key cannot be copied, and Debug
38/// shows only its id.
39pub 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    /// Makes a new random key, refusing if its memory cannot be locked.
53    ///
54    /// # Errors
55    ///
56    /// [`KeyError::Memory`] if the key's page cannot be locked or kept out
57    /// of core dumps.
58    ///
59    /// # Panics
60    ///
61    /// Never: AWS-LC aborts the process rather than fail to give random
62    /// bytes.
63    pub fn generate() -> Result<Key, KeyError> {
64        let mut page = LockedPage::new().map_err(KeyError::Memory)?;
65        // AWS-LC's RAND_bytes aborts the process when it cannot get enough
66        // randomness, so `fill` never returns its error.
67        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    /// Loads the master key: the file must hold exactly 32 bytes. They are
73    /// read straight into the key's own locked page.
74    ///
75    /// # Errors
76    ///
77    /// [`KeyError::Read`] if the file cannot be opened or read,
78    /// [`KeyError::WrongSize`] if it holds other than 32 bytes, and
79    /// [`KeyError::Memory`] if the key's page cannot be locked.
80    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    /// The key's id: the first 8 bytes of its SHA-256 hash.
100    ///
101    /// # Panics
102    ///
103    /// Never: a SHA-256 hash is 32 bytes.
104    #[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    /// The key as SQLCipher's raw key literal, `x'` and 64 hex digits and
115    /// `'`, in a buffer that is zeroed when dropped. It is built at its
116    /// final size, so no shorter copy is left behind by a reallocation.
117    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/// Why a key could not be made.
131#[derive(Debug)]
132pub enum KeyError {
133    /// The key's memory could not be locked or kept out of core dumps.
134    Memory(io::Error),
135    /// The master key file could not be opened or read.
136    Read(io::Error),
137    /// The master key file does not hold exactly 32 bytes.
138    WrongSize,
139    /// A wrapped key did not open. Like [`OpenError`], it does not say which
140    /// part was wrong.
141    Damaged,
142}
143
144/// The I/O error itself is given by `source`, not repeated here.
145impl 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
165/// Locks every page of this process in memory, now and as it grows, so
166/// nothing it holds can reach swap: not AWS-LC's expanded keys, which live
167/// in its own allocations for the length of one call, and not plaintext
168/// bodies. It fails if the lock limit is too low; the operator then raises
169/// it (for example LimitMEMLOCK=infinity under systemd).
170///
171/// # Errors
172///
173/// The error mlockall(2) gives: ENOMEM when the limit is too low, EPERM
174/// when it is zero and the process may not lock memory.
175pub fn lock_all_memory() -> io::Result<()> {
176    // SAFETY: mlockall takes only flags.
177    if unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) } != 0 {
178        return Err(io::Error::last_os_error());
179    }
180    Ok(())
181}
182
183/// Stops this process from writing core dumps: the core size limit is set
184/// to zero and the process is marked not dumpable, which also stops other
185/// processes of the same user from reading its memory. Neither is enough on
186/// its own: the limit is not enforced when core dumps are piped to a
187/// program, and a host with `suid_dumpable` set to 2 still dumps processes
188/// that are not dumpable. Each key's page is left out of core dumps as well.
189///
190/// # Errors
191///
192/// The error setrlimit(2) or prctl(2) gives.
193pub fn disable_core_dumps() -> io::Result<()> {
194    let none = libc::rlimit {
195        rlim_cur: 0,
196        rlim_max: 0,
197    };
198    // SAFETY: `none` is a valid rlimit.
199    if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &none) } != 0 {
200        return Err(io::Error::last_os_error());
201    }
202    // SAFETY: PR_SET_DUMPABLE takes one integer argument.
203    if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0) } != 0 {
204        return Err(io::Error::last_os_error());
205    }
206    Ok(())
207}
208
209/// What a sealed or wrapped value belongs to. It is bound into the
210/// encryption, so a value moved to another tenant, uuid or version does
211/// not open.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Context {
214    /// A tenant key, wrapped under the master key.
215    Tenant {
216        /// The tenant the key belongs to.
217        tenant: TenantId,
218    },
219    /// One version's data key, wrapped under its tenant key, and the body
220    /// sealed under that data key.
221    Version {
222        /// The tenant the vCon belongs to.
223        tenant: TenantId,
224        /// The vCon's uuid.
225        uuid: Uuid,
226        /// The version.
227        version: VersionId,
228    },
229}
230
231impl Context {
232    /// The label, the blob's format byte, then the context in fixed-length
233    /// fields after a byte naming its kind, so no two contexts give the
234    /// same bytes.
235    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/// A sealed body or wrapped key did not open. It carries no detail, so a
259/// caller cannot tell which part was wrong.
260#[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/// Encrypts `plaintext` under `data_key`, bound to `ctx`, with a random
272/// nonce. The blob is the format byte, the nonce, the ciphertext and the
273/// tag.
274///
275/// # Panics
276///
277/// Never: AWS-LC aborts the process rather than fail to give random
278/// bytes.
279#[must_use]
280pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
281    // Drawn whole from the system generator, so no fixed value is ever a
282    // nonce, even for a moment.
283    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
289/// Decrypts a blob made by [`seal`] with the same key and context. Any
290/// change to the blob, the context or the key is an error, never partial
291/// plaintext.
292///
293/// # Errors
294///
295/// [`OpenError`] for a blob that does not open under this key and context.
296pub 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/// Wraps `key` under `wrapping`, bound to `ctx`: a blob like [`seal`]'s,
304/// whose plaintext is the key.
305#[must_use]
306pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
307    seal(wrapping, ctx, key.bytes())
308}
309
310/// Unwraps a key made by [`wrap`] with the same wrapping key and context.
311/// It is opened straight into the new key's own locked page, so the plain
312/// key is never anywhere else; on any error that page is dropped, and so
313/// zeroed.
314///
315/// # Errors
316///
317/// [`KeyError::Damaged`] for a blob that does not open under this key and
318/// context to exactly 32 bytes, and [`KeyError::Memory`] if the new key's
319/// page cannot be locked.
320pub 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
328/// Moves a wrapped key from `old` to `new`, keeping its context: the
329/// rotation step. The plain key exists only in its own locked page in
330/// between, and what it protects is never re-encrypted.
331///
332/// # Errors
333///
334/// Any error [`unwrap`] gives for `wrapped` under `old`.
335pub 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/// Whether a key that has already wrapped `wraps` keys may wrap another.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub enum WrapLimit {
343    /// Wrap.
344    Allow,
345    /// Wrap, and warn that the key needs rotating.
346    Warn,
347    /// Refuse the write until the key is rotated.
348    Refuse,
349}
350
351/// The wrap-count limit for one wrapping key. Each wrap uses a random
352/// nonce, and NIST SP 800-38D 8.3 allows at most 2^32 such encryptions
353/// under one key, so a key that has made 2^32 wraps makes no more. The
354/// warning starts at half that.
355#[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
368/// Decrypts `blob` into the start of `out` and returns the plaintext's
369/// length. `out` must hold the blob's ciphertext and tag.
370fn 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
378/// The key of the catalog that `parent` protects: the master key's for
379/// server.sqlite, a tenant key's for that tenant's catalog. It is HKDF-SHA256
380/// of the parent with no salt and the info `vcon-server/v1 catalog`, made in
381/// a locked page of its own and never stored.
382///
383/// # Errors
384///
385/// [`KeyError::Memory`] if the key's page cannot be locked or kept out
386/// of core dumps.
387pub 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
398/// HKDF (RFC 5869) with SHA-256, filling `out`.
399fn 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    // HKDF refuses only outputs over 255 hashes (8160 bytes); every caller
407    // asks for far less.
408    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
430/// Encrypts `in_out` in place and returns the tag.
431fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
432    // AWS-LC refuses only inputs over 2^36 - 32 bytes, short output buffers
433    // and nonces of another size. Keys are 32 bytes, and the request size
434    // limit must keep bodies far below 64 GiB.
435    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
441/// Decrypts ciphertext followed by its tag in place and returns the length
442/// of the plaintext now at the start of `in_out`.
443fn 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 Case 16 of McGrew and Viega, "The Galois/Counter Mode of
479    /// Operation (GCM)": AES-256, a 96-bit IV and additional data. The same
480    /// values are in AWS-LC's gcm_tests.txt.
481    #[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    /// The blob layout and the associated data are written out here byte by
521    /// byte, so a change to either, which would make stored bodies
522    /// unreadable, fails this test.
523    #[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    /// Every byte of the nonce is random: over 1000 seals no nonce repeats
580    /// and no byte position keeps one value. For 96 random bits either
581    /// event has a probability below 2^-76.
582    #[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    /// A key with the given bytes, for tests that need a known key.
756    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    /// RFC 5869, appendix A, test cases 1 to 3 (SHA-256).
763    #[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    /// The expected key was computed with Python's hmac and hashlib: HKDF
816    /// with no salt and the info "vcon-server/v1 catalog". Changing the
817    /// label or the salt would lock every catalog out, so this pins both.
818    #[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    /// Debug is how a key reaches a log line or a panic message, so it must
921    /// name the key without any rendering of its bytes.
922    #[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}