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/// How many bytes a sealed blob holds beyond its plaintext: the format
272/// byte, the nonce and the tag.
273pub(crate) const SEALED_OVERHEAD: usize = 1 + NONCE_LEN + TAG_LEN;
274
275/// Encrypts `plaintext` under `data_key`, bound to `ctx`, with a random
276/// nonce. The blob is the format byte, the nonce, the ciphertext and the
277/// tag.
278///
279/// # Panics
280///
281/// Never: AWS-LC aborts the process rather than fail to give random
282/// bytes.
283#[must_use]
284pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
285    // Drawn whole from the system generator, so no fixed value is ever a
286    // nonce, even for a moment.
287    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
293/// Decrypts a blob made by [`seal`] with the same key and context. Any
294/// change to the blob, the context or the key is an error, never partial
295/// plaintext.
296///
297/// # Errors
298///
299/// [`OpenError`] for a blob that does not open under this key and context.
300pub 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/// Wraps `key` under `wrapping`, bound to `ctx`: a blob like [`seal`]'s,
308/// whose plaintext is the key.
309#[must_use]
310pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
311    seal(wrapping, ctx, key.bytes())
312}
313
314/// Unwraps a key made by [`wrap`] with the same wrapping key and context.
315/// It is opened straight into the new key's own locked page, so the plain
316/// key is never anywhere else; on any error that page is dropped, and so
317/// zeroed.
318///
319/// # Errors
320///
321/// [`KeyError::Damaged`] for a blob that does not open under this key and
322/// context to exactly 32 bytes, and [`KeyError::Memory`] if the new key's
323/// page cannot be locked.
324pub 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
332/// Moves a wrapped key from `old` to `new`, keeping its context: the
333/// rotation step. The plain key exists only in its own locked page in
334/// between, and what it protects is never re-encrypted.
335///
336/// # Errors
337///
338/// Any error [`unwrap`] gives for `wrapped` under `old`.
339pub 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/// Whether a key that has already wrapped `wraps` keys may wrap another.
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum WrapLimit {
347    /// Wrap.
348    Allow,
349    /// Wrap, and warn that the key needs rotating.
350    Warn,
351    /// Refuse the write until the key is rotated.
352    Refuse,
353}
354
355/// The wrap-count limit for one wrapping key. Each wrap uses a random
356/// nonce, and NIST SP 800-38D 8.3 allows at most 2^32 such encryptions
357/// under one key, so a key that has made 2^32 wraps makes no more. The
358/// warning starts at half that.
359#[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
372/// Decrypts `blob` into the start of `out` and returns the plaintext's
373/// length. `out` must hold the blob's ciphertext and tag.
374fn 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
382/// The key of the catalog that `parent` protects: the master key's for
383/// server.sqlite, a tenant key's for that tenant's catalog. It is HKDF-SHA256
384/// of the parent with no salt and the info `vcon-server/v1 catalog`, made in
385/// a locked page of its own and never stored.
386///
387/// # Errors
388///
389/// [`KeyError::Memory`] if the key's page cannot be locked or kept out
390/// of core dumps.
391pub 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
402/// HKDF (RFC 5869) with SHA-256, filling `out`.
403fn 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    // HKDF refuses only outputs over 255 hashes (8160 bytes); every caller
411    // asks for far less.
412    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
434/// Encrypts `in_out` in place and returns the tag.
435fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
436    // AWS-LC refuses only inputs over 2^36 - 32 bytes, short output buffers
437    // and nonces of another size. Keys are 32 bytes, and the request size
438    // limit must keep bodies far below 64 GiB.
439    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
445/// Decrypts ciphertext followed by its tag in place and returns the length
446/// of the plaintext now at the start of `in_out`.
447fn 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 Case 16 of McGrew and Viega, "The Galois/Counter Mode of
483    /// Operation (GCM)": AES-256, a 96-bit IV and additional data. The same
484    /// values are in AWS-LC's gcm_tests.txt.
485    #[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    /// The blob layout and the associated data are written out here byte by
525    /// byte, so a change to either, which would make stored bodies
526    /// unreadable, fails this test.
527    #[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    /// Every byte of the nonce is random: over 1000 seals no nonce repeats
599    /// and no byte position keeps one value. For 96 random bits either
600    /// event has a probability below 2^-76.
601    #[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    /// A key with the given bytes, for tests that need a known key.
775    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    /// RFC 5869, appendix A, test cases 1 to 3 (SHA-256).
782    #[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    /// The expected key was computed with Python's hmac and hashlib: HKDF
835    /// with no salt and the info "vcon-server/v1 catalog". Changing the
836    /// label or the salt would lock every catalog out, so this pins both.
837    #[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    /// Debug is how a key reaches a log line or a panic message, so it must
940    /// name the key without any rendering of its bytes.
941    #[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}