Skip to main content

vcon_server/
envelope.rs

1//! Keys and the encryption that protects every stored body.
2
3use std::fs::File;
4use std::io::{self, Read};
5use std::path::Path;
6
7use aws_lc_rs::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
8use aws_lc_rs::digest::{SHA256, digest};
9use aws_lc_rs::hkdf::{HKDF_SHA256, KeyType, Salt};
10use uuid::Uuid;
11use zeroize::Zeroizing;
12
13use crate::ids::{KeyId, TenantId, VersionId};
14
15mod locked;
16
17use locked::LockedPage;
18
19/// The length of every key, in bytes.
20const KEY_LEN: usize = 32;
21
22/// The first byte of every blob, naming its layout and algorithm:
23/// AES-256-GCM with a 96-bit nonce and a 128-bit tag.
24const FORMAT: u8 = 1;
25const NONCE_LEN: usize = 12;
26const TAG_LEN: usize = 16;
27
28/// The start of every associated data.
29const LABEL: &[u8] = b"vcon-server/v1";
30
31/// The HKDF info that makes a catalog key.
32const CATALOG_LABEL: &[u8] = b"vcon-server/v1 catalog";
33
34/// A 256-bit key. Its bytes live only in a page of their own that is locked
35/// in memory, so they never reach swap, and that core dumps leave out; they
36/// are zeroed when the key is dropped. A key cannot be copied or printed.
37pub struct Key {
38    page: LockedPage,
39}
40
41impl Key {
42    /// Makes a new random key, refusing if its memory cannot be locked.
43    pub fn generate() -> Result<Key, KeyError> {
44        let mut page = LockedPage::new().map_err(KeyError::Memory)?;
45        // AWS-LC's RAND_bytes aborts the process when it cannot get enough
46        // randomness, so `fill` never returns its error.
47        aws_lc_rs::rand::fill(&mut page.bytes_mut()[..KEY_LEN])
48            .expect("AWS-LC aborts instead of failing");
49        Ok(Key { page })
50    }
51
52    /// Loads the master key: the file must hold exactly 32 bytes. They are
53    /// read straight into the key's own locked page.
54    pub fn load_master(path: &Path) -> Result<Key, KeyError> {
55        let mut page = LockedPage::new().map_err(KeyError::Memory)?;
56        let mut file = File::open(path).map_err(KeyError::Read)?;
57        let buf = page.bytes_mut();
58        file.read_exact(&mut buf[..KEY_LEN])
59            .map_err(|e| match e.kind() {
60                io::ErrorKind::UnexpectedEof => KeyError::WrongSize,
61                _ => KeyError::Read(e),
62            })?;
63        if file
64            .read(&mut buf[KEY_LEN..=KEY_LEN])
65            .map_err(KeyError::Read)?
66            != 0
67        {
68            return Err(KeyError::WrongSize);
69        }
70        Ok(Key { page })
71    }
72
73    /// The key's id: the first 8 bytes of its SHA-256 hash.
74    pub fn id(&self) -> KeyId {
75        let hash = digest(&SHA256, self.bytes());
76        KeyId(hash.as_ref()[..8].try_into().expect("SHA-256 is 32 bytes"))
77    }
78
79    fn bytes(&self) -> &[u8] {
80        &self.page.bytes()[..KEY_LEN]
81    }
82
83    /// The key as SQLCipher's raw key literal, `x'` and 64 hex digits and
84    /// `'`, in a buffer that is zeroed when dropped. It is built at its
85    /// final size, so no shorter copy is left behind by a reallocation.
86    pub(crate) fn sqlcipher_raw(&self) -> Zeroizing<String> {
87        const HEX: &[u8; 16] = b"0123456789abcdef";
88        let mut literal = Zeroizing::new(String::with_capacity(3 + 2 * KEY_LEN));
89        literal.push_str("x'");
90        for byte in self.bytes() {
91            literal.push(char::from(HEX[usize::from(byte >> 4)]));
92            literal.push(char::from(HEX[usize::from(byte & 0x0f)]));
93        }
94        literal.push('\'');
95        literal
96    }
97}
98
99/// Why a key could not be made.
100#[derive(Debug)]
101pub enum KeyError {
102    /// The key's memory could not be locked or kept out of core dumps.
103    Memory(io::Error),
104    /// The master key file could not be opened or read.
105    Read(io::Error),
106    /// The master key file does not hold exactly 32 bytes.
107    WrongSize,
108    /// A wrapped key did not open. Like [`OpenError`], it does not say which
109    /// part was wrong.
110    Damaged,
111}
112
113/// Locks every page of this process in memory, now and as it grows, so
114/// nothing it holds can reach swap: not AWS-LC's expanded keys, which live
115/// in its own allocations for the length of one call, and not plaintext
116/// bodies. It fails if the lock limit is too low; the operator then raises
117/// it (for example LimitMEMLOCK=infinity under systemd).
118pub fn lock_all_memory() -> io::Result<()> {
119    // SAFETY: mlockall takes only flags.
120    if unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) } != 0 {
121        return Err(io::Error::last_os_error());
122    }
123    Ok(())
124}
125
126/// Stops this process from writing core dumps: the core size limit is set
127/// to zero and the process is marked not dumpable, which also stops other
128/// processes of the same user from reading its memory. Neither is enough on
129/// its own: the limit is not enforced when core dumps are piped to a
130/// program, and a host with `suid_dumpable` set to 2 still dumps processes
131/// that are not dumpable. Each key's page is left out of core dumps as well.
132pub fn disable_core_dumps() -> io::Result<()> {
133    let none = libc::rlimit {
134        rlim_cur: 0,
135        rlim_max: 0,
136    };
137    // SAFETY: `none` is a valid rlimit.
138    if unsafe { libc::setrlimit(libc::RLIMIT_CORE, &none) } != 0 {
139        return Err(io::Error::last_os_error());
140    }
141    // SAFETY: PR_SET_DUMPABLE takes one integer argument.
142    if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0) } != 0 {
143        return Err(io::Error::last_os_error());
144    }
145    Ok(())
146}
147
148/// What a sealed or wrapped value belongs to. It is bound into the
149/// encryption, so a value moved to another tenant, uuid or version does
150/// not open.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Context {
153    /// A tenant key, wrapped under the master key.
154    Tenant { tenant: TenantId },
155    /// One version's data key, wrapped under its tenant key, and the body
156    /// sealed under that data key.
157    Version {
158        tenant: TenantId,
159        uuid: Uuid,
160        version: VersionId,
161    },
162}
163
164impl Context {
165    /// The label, the blob's format byte, then the context in fixed-length
166    /// fields after a byte naming its kind, so no two contexts give the
167    /// same bytes.
168    fn associated_data(&self, format: u8) -> Vec<u8> {
169        let mut ad = LABEL.to_vec();
170        ad.push(format);
171        match self {
172            Context::Tenant { tenant } => {
173                ad.push(1);
174                ad.extend(tenant.0);
175            }
176            Context::Version {
177                tenant,
178                uuid,
179                version,
180            } => {
181                ad.push(2);
182                ad.extend(tenant.0);
183                ad.extend(uuid.as_bytes());
184                ad.extend(version.0);
185            }
186        }
187        ad
188    }
189}
190
191/// A sealed body or wrapped key did not open. It carries no detail, so a
192/// caller cannot tell which part was wrong.
193#[derive(Debug, PartialEq, Eq)]
194pub struct OpenError;
195
196/// Encrypts `plaintext` under `data_key`, bound to `ctx`, with a random
197/// nonce. The blob is the format byte, the nonce, the ciphertext and the
198/// tag.
199pub fn seal(data_key: &Key, ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
200    // Drawn whole from the system generator, so no fixed value is ever a
201    // nonce, even for a moment.
202    let nonce: [u8; NONCE_LEN] = aws_lc_rs::rand::generate(&aws_lc_rs::rand::SystemRandom::new())
203        .expect("AWS-LC aborts instead of failing")
204        .expose();
205    seal_with_nonce(data_key.bytes(), nonce, ctx, plaintext)
206}
207
208/// Decrypts a blob made by [`seal`] with the same key and context. Any
209/// change to the blob, the context or the key is an error, never partial
210/// plaintext.
211pub fn open(data_key: &Key, ctx: &Context, blob: &[u8]) -> Result<Vec<u8>, OpenError> {
212    let mut text = vec![0; blob.len().saturating_sub(1 + NONCE_LEN)];
213    let len = open_into(data_key.bytes(), ctx, blob, &mut text)?;
214    text.truncate(len);
215    Ok(text)
216}
217
218/// Wraps `key` under `wrapping`, bound to `ctx`: a blob like [`seal`]'s,
219/// whose plaintext is the key.
220pub fn wrap(wrapping: &Key, ctx: &Context, key: &Key) -> Vec<u8> {
221    seal(wrapping, ctx, key.bytes())
222}
223
224/// Unwraps a key made by [`wrap`] with the same wrapping key and context.
225/// It is opened straight into the new key's own locked page, so the plain
226/// key is never anywhere else; on any error that page is dropped, and so
227/// zeroed.
228pub fn unwrap(wrapping: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Key, KeyError> {
229    let mut page = LockedPage::new().map_err(KeyError::Memory)?;
230    match open_into(wrapping.bytes(), ctx, wrapped, page.bytes_mut()) {
231        Ok(KEY_LEN) => Ok(Key { page }),
232        _ => Err(KeyError::Damaged),
233    }
234}
235
236/// Moves a wrapped key from `old` to `new`, keeping its context: the
237/// rotation step. The plain key exists only in its own locked page in
238/// between, and what it protects is never re-encrypted.
239pub fn rewrap(old: &Key, new: &Key, ctx: &Context, wrapped: &[u8]) -> Result<Vec<u8>, KeyError> {
240    let key = unwrap(old, ctx, wrapped)?;
241    Ok(wrap(new, ctx, &key))
242}
243
244/// Whether a key that has already wrapped `wraps` keys may wrap another.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum WrapLimit {
247    /// Wrap.
248    Allow,
249    /// Wrap, and warn that the key needs rotating.
250    Warn,
251    /// Refuse the write until the key is rotated.
252    Refuse,
253}
254
255/// The wrap-count limit for one wrapping key. Each wrap uses a random
256/// nonce, and NIST SP 800-38D 8.3 allows at most 2^32 such encryptions
257/// under one key, so a key that has made 2^32 wraps makes no more. The
258/// warning starts at half that.
259pub fn wrap_limit(wraps: u64) -> WrapLimit {
260    const REFUSE_AT: u64 = 1 << 32;
261    const WARN_AT: u64 = 1 << 31;
262    if wraps >= REFUSE_AT {
263        WrapLimit::Refuse
264    } else if wraps >= WARN_AT {
265        WrapLimit::Warn
266    } else {
267        WrapLimit::Allow
268    }
269}
270
271/// Decrypts `blob` into the start of `out` and returns the plaintext's
272/// length. `out` must hold the blob's ciphertext and tag.
273fn open_into(key: &[u8], ctx: &Context, blob: &[u8], out: &mut [u8]) -> Result<usize, OpenError> {
274    let (&format, rest) = blob.split_first().ok_or(OpenError)?;
275    let (nonce, sealed) = rest.split_first_chunk::<NONCE_LEN>().ok_or(OpenError)?;
276    let text = out.get_mut(..sealed.len()).ok_or(OpenError)?;
277    text.copy_from_slice(sealed);
278    gcm_open(key, *nonce, &ctx.associated_data(format), text)
279}
280
281/// The key of the catalog that `parent` protects: the master key's for
282/// server.sqlite, a tenant key's for that tenant's catalog. It is HKDF-SHA256
283/// of the parent with no salt and the info `vcon-server/v1 catalog`, made in
284/// a locked page of its own and never stored.
285pub fn catalog_key(parent: &Key) -> Result<Key, KeyError> {
286    let mut page = LockedPage::new().map_err(KeyError::Memory)?;
287    hkdf_sha256(
288        &[],
289        parent.bytes(),
290        CATALOG_LABEL,
291        &mut page.bytes_mut()[..KEY_LEN],
292    );
293    Ok(Key { page })
294}
295
296/// HKDF (RFC 5869) with SHA-256, filling `out`.
297fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) {
298    struct Len(usize);
299    impl KeyType for Len {
300        fn len(&self) -> usize {
301            self.0
302        }
303    }
304    // HKDF refuses only outputs over 255 hashes (8160 bytes); every caller
305    // asks for far less.
306    Salt::new(HKDF_SHA256, salt)
307        .extract(ikm)
308        .expand(&[info], Len(out.len()))
309        .and_then(|okm| okm.fill(out))
310        .expect("within HKDF's output limit");
311}
312
313fn seal_with_nonce(key: &[u8], nonce: [u8; NONCE_LEN], ctx: &Context, plaintext: &[u8]) -> Vec<u8> {
314    let mut blob = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
315    blob.push(FORMAT);
316    blob.extend(nonce);
317    blob.extend(plaintext);
318    let ad = ctx.associated_data(FORMAT);
319    let tag = gcm_seal(key, nonce, &ad, &mut blob[1 + NONCE_LEN..]);
320    blob.extend(tag);
321    blob
322}
323
324fn aead_key(key: &[u8]) -> LessSafeKey {
325    LessSafeKey::new(UnboundKey::new(&AES_256_GCM, key).expect("every key is 32 bytes"))
326}
327
328/// Encrypts `in_out` in place and returns the tag.
329fn gcm_seal(key: &[u8], nonce: [u8; NONCE_LEN], ad: &[u8], in_out: &mut [u8]) -> [u8; TAG_LEN] {
330    // AWS-LC refuses only inputs over 2^36 - 32 bytes, short output buffers
331    // and nonces of another size. Keys are 32 bytes, and the request size
332    // limit must keep bodies far below 64 GiB.
333    let tag = aead_key(key)
334        .seal_in_place_separate_tag(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
335        .expect("within GCM's limits");
336    tag.as_ref().try_into().expect("a 16-byte tag")
337}
338
339/// Decrypts ciphertext followed by its tag in place and returns the length
340/// of the plaintext now at the start of `in_out`.
341fn gcm_open(
342    key: &[u8],
343    nonce: [u8; NONCE_LEN],
344    ad: &[u8],
345    in_out: &mut [u8],
346) -> Result<usize, OpenError> {
347    aead_key(key)
348        .open_in_place(Nonce::assume_unique_for_key(nonce), Aad::from(ad), in_out)
349        .map(|plaintext| plaintext.len())
350        .map_err(|_| OpenError)
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn every_byte_of_a_key_is_random() {
359        let keys: Vec<Key> = (0..8).map(|_| Key::generate().expect("key")).collect();
360        for i in 0..KEY_LEN {
361            let first = keys[0].bytes()[i];
362            assert!(
363                keys.iter().any(|k| k.bytes()[i] != first),
364                "byte {i} is {first} in all eight keys"
365            );
366        }
367    }
368
369    fn hex(text: &str) -> Vec<u8> {
370        (0..text.len())
371            .step_by(2)
372            .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex"))
373            .collect()
374    }
375
376    /// Test Case 16 of McGrew and Viega, "The Galois/Counter Mode of
377    /// Operation (GCM)": AES-256, a 96-bit IV and additional data. The same
378    /// values are in AWS-LC's gcm_tests.txt.
379    #[test]
380    fn aes_256_gcm_matches_the_published_vector() {
381        let key = hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
382        let nonce: [u8; 12] = hex("cafebabefacedbaddecaf888")
383            .try_into()
384            .expect("12 bytes");
385        let ad = hex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
386        let mut text = hex(concat!(
387            "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72",
388            "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39"
389        ));
390        let tag = gcm_seal(&key, nonce, &ad, &mut text);
391        assert_eq!(
392            text,
393            hex(concat!(
394                "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa",
395                "8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662"
396            ))
397        );
398        assert_eq!(tag.to_vec(), hex("76fc6ece0f4e1768cddf8853bb2d551b"));
399    }
400
401    const TENANT: TenantId = TenantId([0x11; 16]);
402    const VERSION: VersionId = VersionId([0x22; 16]);
403    const UUID: Uuid = Uuid::from_bytes([0x33; 16]);
404
405    fn version_ctx() -> Context {
406        Context::Version {
407            tenant: TENANT,
408            uuid: UUID,
409            version: VERSION,
410        }
411    }
412
413    /// The blob layout and the associated data are written out here byte by
414    /// byte, so a change to either, which would make stored bodies
415    /// unreadable, fails this test.
416    #[test]
417    fn a_blob_is_the_format_byte_nonce_ciphertext_and_tag() {
418        let key = [0x44; 32];
419        let nonce = [0x55; 12];
420        let mut ad = b"vcon-server/v1".to_vec();
421        ad.extend([1, 2]);
422        ad.extend([0x11; 16]);
423        ad.extend([0x33; 16]);
424        ad.extend([0x22; 16]);
425        let mut ciphertext = b"body".to_vec();
426        let tag = gcm_seal(&key, nonce, &ad, &mut ciphertext);
427        let mut expected = vec![1];
428        expected.extend(nonce);
429        expected.extend(ciphertext);
430        expected.extend(tag);
431        assert_eq!(
432            seal_with_nonce(&key, nonce, &version_ctx(), b"body"),
433            expected
434        );
435
436        let mut tenant_ad = b"vcon-server/v1".to_vec();
437        tenant_ad.extend([1, 1]);
438        tenant_ad.extend([0x11; 16]);
439        let mut ciphertext = b"key".to_vec();
440        let tag = gcm_seal(&key, nonce, &tenant_ad, &mut ciphertext);
441        let mut expected = vec![1];
442        expected.extend(nonce);
443        expected.extend(ciphertext);
444        expected.extend(tag);
445        let tenant = Context::Tenant { tenant: TENANT };
446        assert_eq!(seal_with_nonce(&key, nonce, &tenant, b"key"), expected);
447    }
448
449    #[test]
450    fn a_sealed_body_opens_with_the_same_key_and_context() {
451        let key = Key::generate().expect("key");
452        for body in [&b""[..], b"x", &[0xa5; 100_000]] {
453            let blob = seal(&key, &version_ctx(), body);
454            assert_eq!(blob.len(), 1 + 12 + body.len() + 16);
455            assert_eq!(open(&key, &version_ctx(), &blob).as_deref(), Ok(body));
456        }
457    }
458
459    #[test]
460    fn each_seal_uses_a_new_nonce() {
461        let key = Key::generate().expect("key");
462        let a = seal(&key, &version_ctx(), b"same");
463        let b = seal(&key, &version_ctx(), b"same");
464        assert_ne!(a[1..13], b[1..13]);
465    }
466
467    /// Every byte of the nonce is random: over 1000 seals no nonce repeats
468    /// and no byte position keeps one value. For 96 random bits either
469    /// event has a probability below 2^-76.
470    #[test]
471    fn every_byte_of_a_nonce_is_random() {
472        let key = Key::generate().expect("key");
473        let nonces: Vec<[u8; 12]> = (0..1000)
474            .map(|_| {
475                let blob = seal(&key, &version_ctx(), b"same");
476                blob[1..13].try_into().expect("12 bytes")
477            })
478            .collect();
479        let distinct: std::collections::HashSet<_> = nonces.iter().collect();
480        assert_eq!(distinct.len(), nonces.len(), "a nonce repeated");
481        for i in 0..12 {
482            assert!(
483                nonces.iter().any(|n| n[i] != nonces[0][i]),
484                "byte {i} of the nonce never changes"
485            );
486        }
487    }
488
489    #[test]
490    fn changing_any_byte_of_a_blob_fails() {
491        let key = Key::generate().expect("key");
492        let blob = seal(&key, &version_ctx(), b"a body");
493        assert_eq!(
494            open(&key, &version_ctx(), &blob).as_deref(),
495            Ok(&b"a body"[..]),
496            "unchanged"
497        );
498        for i in 0..blob.len() {
499            let mut bad = blob.clone();
500            bad[i] ^= 0x01;
501            assert_eq!(open(&key, &version_ctx(), &bad), Err(OpenError), "byte {i}");
502        }
503        for len in 0..blob.len() {
504            assert_eq!(
505                open(&key, &version_ctx(), &blob[..len]),
506                Err(OpenError),
507                "first {len} bytes"
508            );
509        }
510        let mut longer = blob.clone();
511        longer.push(0);
512        assert_eq!(
513            open(&key, &version_ctx(), &longer),
514            Err(OpenError),
515            "one byte more"
516        );
517    }
518
519    #[test]
520    fn another_context_or_key_fails() {
521        let key = Key::generate().expect("key");
522        let blob = seal(&key, &version_ctx(), b"a body");
523        assert_eq!(
524            open(&key, &version_ctx(), &blob).as_deref(),
525            Ok(&b"a body"[..]),
526            "unchanged"
527        );
528        let others = [
529            Context::Version {
530                tenant: TenantId([0x12; 16]),
531                uuid: UUID,
532                version: VERSION,
533            },
534            Context::Version {
535                tenant: TENANT,
536                uuid: Uuid::from_bytes([0x34; 16]),
537                version: VERSION,
538            },
539            Context::Version {
540                tenant: TENANT,
541                uuid: UUID,
542                version: VersionId([0x23; 16]),
543            },
544            Context::Tenant { tenant: TENANT },
545        ];
546        for other in others {
547            assert_eq!(open(&key, &other, &blob), Err(OpenError), "{other:?}");
548        }
549        let other_key = Key::generate().expect("key");
550        assert_eq!(open(&other_key, &version_ctx(), &blob), Err(OpenError));
551    }
552
553    const TENANT_CTX: Context = Context::Tenant { tenant: TENANT };
554
555    #[test]
556    fn a_wrapped_key_unwraps_to_the_same_key() {
557        let wrapping = Key::generate().expect("key");
558        let key = Key::generate().expect("key");
559        for ctx in [TENANT_CTX, version_ctx()] {
560            let wrapped = wrap(&wrapping, &ctx, &key);
561            assert_eq!(wrapped.len(), 1 + 12 + KEY_LEN + 16);
562            let unwrapped = unwrap(&wrapping, &ctx, &wrapped).expect("unwraps");
563            assert_eq!(unwrapped.bytes(), key.bytes());
564        }
565    }
566
567    #[test]
568    fn changing_any_byte_of_a_wrapped_key_fails() {
569        let wrapping = Key::generate().expect("key");
570        let key = Key::generate().expect("key");
571        let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
572        assert!(
573            unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
574            "unchanged"
575        );
576        for i in 0..wrapped.len() {
577            let mut bad = wrapped.clone();
578            bad[i] ^= 0x01;
579            assert!(unwrap(&wrapping, &TENANT_CTX, &bad).is_err(), "byte {i}");
580        }
581        for len in 0..wrapped.len() {
582            assert!(
583                unwrap(&wrapping, &TENANT_CTX, &wrapped[..len]).is_err(),
584                "first {len} bytes"
585            );
586        }
587    }
588
589    #[test]
590    fn another_context_or_wrapping_key_fails() {
591        let wrapping = Key::generate().expect("key");
592        let key = Key::generate().expect("key");
593        let wrapped = wrap(&wrapping, &TENANT_CTX, &key);
594        assert!(
595            unwrap(&wrapping, &TENANT_CTX, &wrapped).is_ok(),
596            "unchanged"
597        );
598        let other_tenant = Context::Tenant {
599            tenant: TenantId([0x12; 16]),
600        };
601        assert!(
602            unwrap(&wrapping, &other_tenant, &wrapped).is_err(),
603            "other tenant"
604        );
605        assert!(
606            unwrap(&wrapping, &version_ctx(), &wrapped).is_err(),
607            "other kind"
608        );
609        let other = Key::generate().expect("key");
610        assert!(
611            unwrap(&other, &TENANT_CTX, &wrapped).is_err(),
612            "other wrapping key"
613        );
614    }
615
616    #[test]
617    fn only_a_32_byte_key_unwraps() {
618        let wrapping = Key::generate().expect("key");
619        for len in [0, 31, 33, 64] {
620            let sealed = seal(&wrapping, &TENANT_CTX, &vec![7; len]);
621            assert!(
622                unwrap(&wrapping, &TENANT_CTX, &sealed).is_err(),
623                "{len} bytes"
624            );
625        }
626        let sealed = seal(&wrapping, &TENANT_CTX, &[7; KEY_LEN]);
627        let key = unwrap(&wrapping, &TENANT_CTX, &sealed).expect("32 bytes unwrap");
628        assert_eq!(key.bytes(), [7; KEY_LEN]);
629    }
630
631    /// A key with the given bytes, for tests that need a known key.
632    fn key_of(bytes: [u8; KEY_LEN]) -> Key {
633        let mut page = LockedPage::new().expect("page");
634        page.bytes_mut()[..KEY_LEN].copy_from_slice(&bytes);
635        Key { page }
636    }
637
638    /// RFC 5869, appendix A, test cases 1 to 3 (SHA-256).
639    #[test]
640    fn hkdf_sha256_matches_rfc_5869() {
641        let cases = [
642            (
643                "000102030405060708090a0b0c",
644                "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
645                "f0f1f2f3f4f5f6f7f8f9",
646                concat!(
647                    "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf",
648                    "34007208d5b887185865"
649                ),
650            ),
651            (
652                concat!(
653                    "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f",
654                    "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
655                    "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
656                ),
657                concat!(
658                    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
659                    "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
660                    "404142434445464748494a4b4c4d4e4f"
661                ),
662                concat!(
663                    "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
664                    "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef",
665                    "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
666                ),
667                concat!(
668                    "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c",
669                    "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71",
670                    "cc30c58179ec3e87c14c01d5c1f3434f1d87"
671                ),
672            ),
673            (
674                "",
675                "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
676                "",
677                concat!(
678                    "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d",
679                    "9d201395faa4b61a96c8"
680                ),
681            ),
682        ];
683        for (n, (salt, ikm, info, okm)) in cases.into_iter().enumerate() {
684            let expected = hex(okm);
685            let mut out = vec![0; expected.len()];
686            hkdf_sha256(&hex(salt), &hex(ikm), &hex(info), &mut out);
687            assert_eq!(out, expected, "test case {}", n + 1);
688        }
689    }
690
691    /// The expected key was computed with Python's hmac and hashlib: HKDF
692    /// with no salt and the info "vcon-server/v1 catalog". Changing the
693    /// label or the salt would lock every catalog out, so this pins both.
694    #[test]
695    fn a_catalog_key_is_hkdf_of_its_parent_with_the_catalog_label() {
696        let catalog = catalog_key(&key_of([0x44; KEY_LEN])).expect("derives");
697        assert_eq!(
698            catalog.bytes(),
699            hex("c0df4ea4f63e64687ca93c494fff4ca00b3ae13112766d8ea24cca35c6b1031b")
700        );
701    }
702
703    #[test]
704    fn distinct_parents_or_labels_give_distinct_keys() {
705        let parent = Key::generate().expect("key");
706        let again = catalog_key(&parent).expect("derives");
707        assert_eq!(
708            catalog_key(&parent).expect("derives").bytes(),
709            again.bytes()
710        );
711        let other = catalog_key(&Key::generate().expect("key")).expect("derives");
712        assert_ne!(other.bytes(), again.bytes());
713        let mut a = [0; KEY_LEN];
714        let mut b = [0; KEY_LEN];
715        hkdf_sha256(&[], parent.bytes(), b"label a", &mut a);
716        hkdf_sha256(&[], parent.bytes(), b"label b", &mut b);
717        assert_ne!(a, b);
718    }
719
720    #[test]
721    fn a_rewrapped_key_opens_under_the_new_key_only() {
722        let old = Key::generate().expect("key");
723        let new = Key::generate().expect("key");
724        let key = Key::generate().expect("key");
725        let wrapped = wrap(&old, &version_ctx(), &key);
726        let rewrapped = rewrap(&old, &new, &version_ctx(), &wrapped).expect("rewraps");
727        let opened = unwrap(&new, &version_ctx(), &rewrapped).expect("opens under new");
728        assert_eq!(opened.bytes(), key.bytes());
729        assert!(unwrap(&old, &version_ctx(), &rewrapped).is_err(), "old key");
730        assert!(
731            unwrap(&new, &TENANT_CTX, &rewrapped).is_err(),
732            "context kept"
733        );
734    }
735
736    #[test]
737    fn rewrapping_needs_the_right_old_key_and_context() {
738        let old = Key::generate().expect("key");
739        let new = Key::generate().expect("key");
740        let wrapped = wrap(&old, &TENANT_CTX, &Key::generate().expect("key"));
741        assert!(matches!(
742            rewrap(&new, &new, &TENANT_CTX, &wrapped),
743            Err(KeyError::Damaged)
744        ));
745        assert!(matches!(
746            rewrap(&old, &new, &version_ctx(), &wrapped),
747            Err(KeyError::Damaged)
748        ));
749    }
750
751    #[test]
752    fn wrapping_warns_from_2_to_the_31_and_stops_at_2_to_the_32() {
753        let cases = [
754            (0, WrapLimit::Allow),
755            ((1 << 31) - 1, WrapLimit::Allow),
756            (1 << 31, WrapLimit::Warn),
757            ((1 << 32) - 1, WrapLimit::Warn),
758            (1 << 32, WrapLimit::Refuse),
759            (u64::MAX, WrapLimit::Refuse),
760        ];
761        for (wraps, expected) in cases {
762            assert_eq!(wrap_limit(wraps), expected, "{wraps} wraps");
763        }
764    }
765
766    #[test]
767    fn the_sqlcipher_literal_spells_the_key_in_hex() {
768        let mut bytes = [0; KEY_LEN];
769        bytes[0] = 0x0a;
770        bytes[31] = 0xff;
771        let literal = key_of(bytes).sqlcipher_raw();
772        let expected = format!("x'0a{}ff'", "00".repeat(30));
773        assert_eq!(*literal, expected);
774        assert_eq!(literal.capacity(), literal.len(), "built at its final size");
775    }
776
777    #[test]
778    fn keys_can_be_shared_between_threads() {
779        fn shared<T: Send + Sync>() {}
780        shared::<Key>();
781    }
782}