Skip to main content

tzap_core/
crypto.rs

1use aes_gcm::Aes256Gcm;
2use aes_gcm_siv::Aes256GcmSiv;
3use argon2::{Algorithm, Argon2, Params, Version};
4use chacha20poly1305::XChaCha20Poly1305;
5use hkdf::Hkdf;
6use hmac::{Hmac, Mac};
7use sha2::{Digest, Sha256};
8use unicode_normalization::UnicodeNormalization;
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use aes_gcm_siv::aead::{Aead, KeyInit as AeadKeyInit, Payload};
12
13use crate::format::{
14    AeadAlgo, FormatError, KdfAlgo, MASTER_KEY_LEN, READER_MAX_ARGON2ID_M_COST_KIB,
15    READER_MAX_ARGON2ID_PARALLELISM, READER_MAX_ARGON2ID_T_COST, READER_MAX_KEY_WRAP_TABLE_LEN,
16    READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS, SUBKEY_LEN, VOLUME_FORMAT_REV_44,
17};
18use crate::padding::{depad_suffix_padding, suffix_pad_for_aead};
19
20type HmacSha256 = Hmac<Sha256>;
21
22const HKDF_SALT_DOMAIN: &[u8] = b"tzap-v1-subkeys";
23const CRYPTO_HEADER_HMAC_DOMAIN: &[u8] = b"tzap-v1-crypto-header";
24const MANIFEST_FOOTER_HMAC_DOMAIN: &[u8] = b"tzap-v1-manifest-footer";
25const VOLUME_TRAILER_HMAC_DOMAIN: &[u8] = b"tzap-v1-volume-trailer";
26const BOOTSTRAP_SIDECAR_HMAC_DOMAIN: &[u8] = b"tzap-v1-sidecar";
27const CRYPTO_HEADER_DIGEST_DOMAIN_V43: &[u8] = b"tzap-header-v43";
28const MANIFEST_FOOTER_DIGEST_DOMAIN_V43: &[u8] = b"tzap-manifest-v43";
29const VOLUME_TRAILER_DIGEST_DOMAIN_V43: &[u8] = b"tzap-trailer-v43";
30const BOOTSTRAP_SIDECAR_DIGEST_DOMAIN_V43: &[u8] = b"tzap-sidecar-v43";
31const CRYPTO_HEADER_DIGEST_DOMAIN_V44: &[u8] = b"tzap-header-v44";
32const MANIFEST_FOOTER_DIGEST_DOMAIN_V44: &[u8] = b"tzap-manifest-v44";
33const VOLUME_TRAILER_DIGEST_DOMAIN_V44: &[u8] = b"tzap-trailer-v44";
34const BOOTSTRAP_SIDECAR_DIGEST_DOMAIN_V44: &[u8] = b"tzap-sidecar-v44";
35
36const RAW_KDF_PARAMS_LEN: usize = 2;
37const NONE_KDF_PARAMS_LEN: usize = 2;
38const ARGON2ID_FIXED_PARAMS_LEN: usize = 16;
39const RECIPIENT_WRAP_KDF_PARAMS_LEN: usize = 46;
40const ARGON2ID_MIN_SALT_LEN: u16 = 8;
41const ARGON2ID_MAX_SALT_LEN: u16 = 64;
42const RECIPIENT_WRAP_TABLE_VERSION: u16 = 1;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum KdfParams {
46    None,
47    Raw,
48    Argon2id {
49        t_cost: u32,
50        m_cost_kib: u32,
51        parallelism: u32,
52        salt: Vec<u8>,
53    },
54    RecipientWrap {
55        key_wrap_table_length: u32,
56        key_wrap_table_record_count: u32,
57        key_wrap_table_version: u16,
58        key_wrap_table_digest: [u8; 32],
59    },
60}
61
62impl KdfParams {
63    pub fn parse(algo: KdfAlgo, bytes: &[u8]) -> Result<(Self, usize), FormatError> {
64        match algo {
65            KdfAlgo::Raw => parse_raw_kdf_params(bytes),
66            KdfAlgo::Argon2id => parse_argon2id_kdf_params(bytes),
67            KdfAlgo::None => parse_none_kdf_params(bytes),
68            KdfAlgo::RecipientWrap => parse_recipient_wrap_kdf_params(bytes),
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
74pub struct MasterKey(pub [u8; MASTER_KEY_LEN]);
75
76impl MasterKey {
77    pub fn from_raw_key(raw_key: &[u8]) -> Result<Self, FormatError> {
78        if raw_key.len() != MASTER_KEY_LEN {
79            return Err(FormatError::InvalidRawMasterKeyLength);
80        }
81        let mut key = [0u8; MASTER_KEY_LEN];
82        key.copy_from_slice(raw_key);
83        Ok(Self(key))
84    }
85
86    pub fn derive_from_passphrase(
87        params: &KdfParams,
88        passphrase: &str,
89    ) -> Result<Self, FormatError> {
90        let KdfParams::Argon2id {
91            t_cost,
92            m_cost_kib,
93            parallelism,
94            salt,
95        } = params
96        else {
97            return Err(FormatError::KeyMaterialMismatch);
98        };
99
100        let salt_length = u16::try_from(salt.len()).map_err(|_| {
101            FormatError::InvalidKdfParams("argon2id salt length must be 8..64 bytes")
102        })?;
103        validate_argon2id_bounds(*t_cost, *m_cost_kib, *parallelism, salt_length)?;
104        let params = Params::new(*m_cost_kib, *t_cost, *parallelism, Some(MASTER_KEY_LEN))
105            .map_err(|_| FormatError::InvalidKdfParams("argon2 params rejected"))?;
106        let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
107        let mut output = [0u8; MASTER_KEY_LEN];
108        let mut passphrase_bytes = normalize_passphrase_nfc(passphrase);
109        let result = argon2.hash_password_into(&passphrase_bytes, salt, &mut output);
110        passphrase_bytes.zeroize();
111        result.map_err(|_| FormatError::Argon2idFailure)?;
112        Ok(Self(output))
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
117pub struct Subkeys {
118    pub enc_key: [u8; SUBKEY_LEN],
119    pub mac_key: [u8; SUBKEY_LEN],
120    pub nonce_seed: [u8; SUBKEY_LEN],
121    pub index_root_key: [u8; SUBKEY_LEN],
122    pub index_shard_key: [u8; SUBKEY_LEN],
123    pub dictionary_key: [u8; SUBKEY_LEN],
124    pub dir_hint_key: [u8; SUBKEY_LEN],
125    pub index_nonce_seed: [u8; SUBKEY_LEN],
126}
127
128impl Subkeys {
129    pub(crate) fn unencrypted_placeholder() -> Self {
130        Self {
131            enc_key: [0; SUBKEY_LEN],
132            mac_key: [0; SUBKEY_LEN],
133            nonce_seed: [0; SUBKEY_LEN],
134            index_root_key: [0; SUBKEY_LEN],
135            index_shard_key: [0; SUBKEY_LEN],
136            dictionary_key: [0; SUBKEY_LEN],
137            dir_hint_key: [0; SUBKEY_LEN],
138            index_nonce_seed: [0; SUBKEY_LEN],
139        }
140    }
141
142    pub fn derive(
143        master_key: &MasterKey,
144        archive_uuid: &[u8; 16],
145        session_id: &[u8; 16],
146    ) -> Result<Self, FormatError> {
147        let mut salt = Vec::with_capacity(HKDF_SALT_DOMAIN.len() + 32);
148        salt.extend_from_slice(HKDF_SALT_DOMAIN);
149        salt.extend_from_slice(archive_uuid);
150        salt.extend_from_slice(session_id);
151        let hk = Hkdf::<Sha256>::new(Some(&salt), &master_key.0);
152        salt.zeroize();
153
154        Ok(Self {
155            enc_key: expand_subkey(&hk, b"tzap-v1-enc")?,
156            mac_key: expand_subkey(&hk, b"tzap-v1-mac")?,
157            nonce_seed: expand_subkey(&hk, b"tzap-v1-nonce")?,
158            index_root_key: expand_subkey(&hk, b"tzap-v1-idxroot")?,
159            index_shard_key: expand_subkey(&hk, b"tzap-v1-idxshard")?,
160            dictionary_key: expand_subkey(&hk, b"tzap-v1-dict")?,
161            dir_hint_key: expand_subkey(&hk, b"tzap-v1-dirhint")?,
162            index_nonce_seed: expand_subkey(&hk, b"tzap-v1-idxnonce")?,
163        })
164    }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum HmacDomain {
169    CryptoHeader,
170    ManifestFooter,
171    VolumeTrailer,
172    BootstrapSidecar,
173}
174
175impl HmacDomain {
176    pub fn structure_name(self) -> &'static str {
177        match self {
178            Self::CryptoHeader => "CryptoHeader",
179            Self::ManifestFooter => "ManifestFooter",
180            Self::VolumeTrailer => "VolumeTrailer",
181            Self::BootstrapSidecar => "BootstrapSidecarHeader",
182        }
183    }
184
185    fn domain_bytes(self) -> &'static [u8] {
186        match self {
187            Self::CryptoHeader => CRYPTO_HEADER_HMAC_DOMAIN,
188            Self::ManifestFooter => MANIFEST_FOOTER_HMAC_DOMAIN,
189            Self::VolumeTrailer => VOLUME_TRAILER_HMAC_DOMAIN,
190            Self::BootstrapSidecar => BOOTSTRAP_SIDECAR_HMAC_DOMAIN,
191        }
192    }
193
194    fn digest_domain_bytes(self, volume_format_rev: u16) -> &'static [u8] {
195        let (crypto_header_domain, manifest_footer_domain, trailer_domain, sidecar_domain) =
196            if volume_format_rev == VOLUME_FORMAT_REV_44 {
197                (
198                    CRYPTO_HEADER_DIGEST_DOMAIN_V44,
199                    MANIFEST_FOOTER_DIGEST_DOMAIN_V44,
200                    VOLUME_TRAILER_DIGEST_DOMAIN_V44,
201                    BOOTSTRAP_SIDECAR_DIGEST_DOMAIN_V44,
202                )
203            } else {
204                (
205                    CRYPTO_HEADER_DIGEST_DOMAIN_V43,
206                    MANIFEST_FOOTER_DIGEST_DOMAIN_V43,
207                    VOLUME_TRAILER_DIGEST_DOMAIN_V43,
208                    BOOTSTRAP_SIDECAR_DIGEST_DOMAIN_V43,
209                )
210            };
211        match self {
212            Self::CryptoHeader => crypto_header_domain,
213            Self::ManifestFooter => manifest_footer_domain,
214            Self::VolumeTrailer => trailer_domain,
215            Self::BootstrapSidecar => sidecar_domain,
216        }
217    }
218}
219
220pub fn compute_hmac(
221    domain: HmacDomain,
222    mac_key: &[u8; SUBKEY_LEN],
223    archive_uuid: &[u8; 16],
224    session_id: &[u8; 16],
225    covered_bytes: &[u8],
226) -> [u8; SUBKEY_LEN] {
227    let mut mac =
228        <HmacSha256 as Mac>::new_from_slice(mac_key).expect("HMAC accepts any key length");
229    mac.update(domain.domain_bytes());
230    mac.update(archive_uuid);
231    mac.update(session_id);
232    mac.update(covered_bytes);
233    let digest = mac.finalize().into_bytes();
234    let mut output = [0u8; SUBKEY_LEN];
235    output.copy_from_slice(&digest);
236    output
237}
238
239pub fn verify_hmac(
240    domain: HmacDomain,
241    mac_key: &[u8; SUBKEY_LEN],
242    archive_uuid: &[u8; 16],
243    session_id: &[u8; 16],
244    covered_bytes: &[u8],
245    expected_hmac: &[u8],
246) -> Result<(), FormatError> {
247    let mut mac =
248        <HmacSha256 as Mac>::new_from_slice(mac_key).expect("HMAC accepts any key length");
249    mac.update(domain.domain_bytes());
250    mac.update(archive_uuid);
251    mac.update(session_id);
252    mac.update(covered_bytes);
253    mac.verify_slice(expected_hmac)
254        .map_err(|_| FormatError::HmacMismatch {
255            structure: domain.structure_name(),
256        })
257}
258
259pub fn compute_integrity_tag(
260    domain: HmacDomain,
261    aead_algo: AeadAlgo,
262    volume_format_rev: u16,
263    mac_key: Option<&[u8; SUBKEY_LEN]>,
264    archive_uuid: &[u8; 16],
265    session_id: &[u8; 16],
266    covered_bytes: &[u8],
267) -> Result<[u8; SUBKEY_LEN], FormatError> {
268    if aead_algo.is_encrypted() {
269        return Ok(compute_hmac(
270            domain,
271            mac_key.ok_or(FormatError::KeyMaterialMismatch)?,
272            archive_uuid,
273            session_id,
274            covered_bytes,
275        ));
276    }
277
278    let mut hasher = Sha256::new();
279    hasher.update(domain.digest_domain_bytes(volume_format_rev));
280    hasher.update(archive_uuid);
281    hasher.update(session_id);
282    hasher.update(covered_bytes);
283    let digest = hasher.finalize();
284    let mut output = [0u8; SUBKEY_LEN];
285    output.copy_from_slice(&digest);
286    Ok(output)
287}
288
289#[allow(clippy::too_many_arguments)]
290pub fn verify_integrity_tag(
291    domain: HmacDomain,
292    aead_algo: AeadAlgo,
293    volume_format_rev: u16,
294    mac_key: Option<&[u8; SUBKEY_LEN]>,
295    archive_uuid: &[u8; 16],
296    session_id: &[u8; 16],
297    covered_bytes: &[u8],
298    expected_tag: &[u8],
299) -> Result<(), FormatError> {
300    if aead_algo.is_encrypted() {
301        return verify_hmac(
302            domain,
303            mac_key.ok_or(FormatError::KeyMaterialMismatch)?,
304            archive_uuid,
305            session_id,
306            covered_bytes,
307            expected_tag,
308        );
309    }
310
311    let actual = compute_integrity_tag(
312        domain,
313        aead_algo,
314        volume_format_rev,
315        None,
316        archive_uuid,
317        session_id,
318        covered_bytes,
319    )?;
320    if expected_tag == actual {
321        Ok(())
322    } else {
323        Err(FormatError::IntegrityDigestMismatch {
324            structure: domain.structure_name(),
325        })
326    }
327}
328
329pub fn normalize_passphrase_nfc(passphrase: &str) -> Vec<u8> {
330    passphrase.nfc().collect::<String>().into_bytes()
331}
332
333pub fn derive_nonce(
334    seed: &[u8; SUBKEY_LEN],
335    domain: &[u8],
336    archive_uuid: &[u8; 16],
337    session_id: &[u8; 16],
338    counter: u64,
339    len: usize,
340) -> Result<Vec<u8>, FormatError> {
341    let info = nonce_or_aad_info(b"tzap-v1-nonce", domain, archive_uuid, session_id, counter)?;
342    let hk = Hkdf::<Sha256>::from_prk(seed)
343        .map_err(|_| FormatError::InvalidKdfParams("bad nonce seed"))?;
344    let mut nonce = vec![0u8; len];
345    hk.expand(&info, &mut nonce)
346        .map_err(|_| FormatError::HkdfExpandFailure)?;
347    Ok(nonce)
348}
349
350pub fn build_aad(
351    domain: &[u8],
352    archive_uuid: &[u8; 16],
353    session_id: &[u8; 16],
354    counter: u64,
355) -> Result<Vec<u8>, FormatError> {
356    nonce_or_aad_info(b"tzap-v1-aad", domain, archive_uuid, session_id, counter)
357}
358
359pub fn aead_encrypt(
360    algo: AeadAlgo,
361    key: &[u8; SUBKEY_LEN],
362    nonce: &[u8],
363    aad: &[u8],
364    plaintext: &[u8],
365) -> Result<Vec<u8>, FormatError> {
366    validate_nonce_len(algo, nonce)?;
367    match algo {
368        AeadAlgo::None => Ok(plaintext.to_vec()),
369        AeadAlgo::AesGcmSiv256 => {
370            let cipher =
371                Aes256GcmSiv::new_from_slice(key).map_err(|_| FormatError::InvalidAeadKeyLength)?;
372            cipher
373                .encrypt(
374                    aes_gcm_siv::Nonce::from_slice(nonce),
375                    Payload {
376                        msg: plaintext,
377                        aad,
378                    },
379                )
380                .map_err(|_| FormatError::AeadFailure)
381        }
382        AeadAlgo::XChaCha20Poly1305 => {
383            let cipher = XChaCha20Poly1305::new_from_slice(key)
384                .map_err(|_| FormatError::InvalidAeadKeyLength)?;
385            cipher
386                .encrypt(
387                    chacha20poly1305::XNonce::from_slice(nonce),
388                    Payload {
389                        msg: plaintext,
390                        aad,
391                    },
392                )
393                .map_err(|_| FormatError::AeadFailure)
394        }
395        AeadAlgo::AesGcm256 => {
396            let cipher =
397                Aes256Gcm::new_from_slice(key).map_err(|_| FormatError::InvalidAeadKeyLength)?;
398            cipher
399                .encrypt(
400                    aes_gcm::Nonce::from_slice(nonce),
401                    Payload {
402                        msg: plaintext,
403                        aad,
404                    },
405                )
406                .map_err(|_| FormatError::AeadFailure)
407        }
408    }
409}
410
411pub fn aead_decrypt(
412    algo: AeadAlgo,
413    key: &[u8; SUBKEY_LEN],
414    nonce: &[u8],
415    aad: &[u8],
416    ciphertext_and_tag: &[u8],
417) -> Result<Vec<u8>, FormatError> {
418    validate_nonce_len(algo, nonce)?;
419    match algo {
420        AeadAlgo::None => Ok(ciphertext_and_tag.to_vec()),
421        AeadAlgo::AesGcmSiv256 => {
422            let cipher =
423                Aes256GcmSiv::new_from_slice(key).map_err(|_| FormatError::InvalidAeadKeyLength)?;
424            cipher
425                .decrypt(
426                    aes_gcm_siv::Nonce::from_slice(nonce),
427                    Payload {
428                        msg: ciphertext_and_tag,
429                        aad,
430                    },
431                )
432                .map_err(|_| FormatError::AeadFailure)
433        }
434        AeadAlgo::XChaCha20Poly1305 => {
435            let cipher = XChaCha20Poly1305::new_from_slice(key)
436                .map_err(|_| FormatError::InvalidAeadKeyLength)?;
437            cipher
438                .decrypt(
439                    chacha20poly1305::XNonce::from_slice(nonce),
440                    Payload {
441                        msg: ciphertext_and_tag,
442                        aad,
443                    },
444                )
445                .map_err(|_| FormatError::AeadFailure)
446        }
447        AeadAlgo::AesGcm256 => {
448            let cipher =
449                Aes256Gcm::new_from_slice(key).map_err(|_| FormatError::InvalidAeadKeyLength)?;
450            cipher
451                .decrypt(
452                    aes_gcm::Nonce::from_slice(nonce),
453                    Payload {
454                        msg: ciphertext_and_tag,
455                        aad,
456                    },
457                )
458                .map_err(|_| FormatError::AeadFailure)
459        }
460    }
461}
462
463#[derive(Debug, Clone, Copy)]
464pub struct AeadObjectContext<'a> {
465    pub algo: AeadAlgo,
466    pub key: &'a [u8; SUBKEY_LEN],
467    pub nonce_seed: &'a [u8; SUBKEY_LEN],
468    pub domain: &'a [u8],
469    pub archive_uuid: &'a [u8; 16],
470    pub session_id: &'a [u8; 16],
471    pub counter: u64,
472}
473
474pub fn encrypt_padded_aead_object(
475    context: AeadObjectContext<'_>,
476    block_size: usize,
477    payload: &[u8],
478) -> Result<Vec<u8>, FormatError> {
479    let nonce = derive_nonce(
480        context.nonce_seed,
481        context.domain,
482        context.archive_uuid,
483        context.session_id,
484        context.counter,
485        context.algo.nonce_len(),
486    )?;
487    let aad = build_aad(
488        context.domain,
489        context.archive_uuid,
490        context.session_id,
491        context.counter,
492    )?;
493    let padded = suffix_pad_for_aead(payload, context.algo.tag_len(), block_size)?;
494    aead_encrypt(context.algo, context.key, &nonce, &aad, &padded)
495}
496
497pub fn decrypt_padded_aead_object(
498    context: AeadObjectContext<'_>,
499    ciphertext_and_tag: &[u8],
500) -> Result<Vec<u8>, FormatError> {
501    let nonce = derive_nonce(
502        context.nonce_seed,
503        context.domain,
504        context.archive_uuid,
505        context.session_id,
506        context.counter,
507        context.algo.nonce_len(),
508    )?;
509    let aad = build_aad(
510        context.domain,
511        context.archive_uuid,
512        context.session_id,
513        context.counter,
514    )?;
515    let padded = aead_decrypt(context.algo, context.key, &nonce, &aad, ciphertext_and_tag)?;
516    Ok(depad_suffix_padding(&padded)?.to_vec())
517}
518
519fn parse_raw_kdf_params(bytes: &[u8]) -> Result<(KdfParams, usize), FormatError> {
520    if bytes.len() < RAW_KDF_PARAMS_LEN {
521        return Err(FormatError::TruncatedKdfParams);
522    }
523    let algo_tag = read_u16(bytes, 0)?;
524    if algo_tag != KdfAlgo::Raw as u16 {
525        return Err(FormatError::KdfAlgoTagMismatch {
526            expected: KdfAlgo::Raw as u16,
527            actual: algo_tag,
528        });
529    }
530    Ok((KdfParams::Raw, RAW_KDF_PARAMS_LEN))
531}
532
533fn parse_none_kdf_params(bytes: &[u8]) -> Result<(KdfParams, usize), FormatError> {
534    if bytes.len() < NONE_KDF_PARAMS_LEN {
535        return Err(FormatError::TruncatedKdfParams);
536    }
537    let algo_tag = read_u16(bytes, 0)?;
538    if algo_tag != KdfAlgo::None as u16 {
539        return Err(FormatError::KdfAlgoTagMismatch {
540            expected: KdfAlgo::None as u16,
541            actual: algo_tag,
542        });
543    }
544    Ok((KdfParams::None, NONE_KDF_PARAMS_LEN))
545}
546
547fn parse_argon2id_kdf_params(bytes: &[u8]) -> Result<(KdfParams, usize), FormatError> {
548    if bytes.len() < ARGON2ID_FIXED_PARAMS_LEN {
549        return Err(FormatError::TruncatedKdfParams);
550    }
551    let algo_tag = read_u16(bytes, 0)?;
552    if algo_tag != KdfAlgo::Argon2id as u16 {
553        return Err(FormatError::KdfAlgoTagMismatch {
554            expected: KdfAlgo::Argon2id as u16,
555            actual: algo_tag,
556        });
557    }
558    let t_cost = read_u32(bytes, 2)?;
559    let m_cost_kib = read_u32(bytes, 6)?;
560    let parallelism = read_u32(bytes, 10)?;
561    let salt_length = read_u16(bytes, 14)?;
562    if !(ARGON2ID_MIN_SALT_LEN..=ARGON2ID_MAX_SALT_LEN).contains(&salt_length) {
563        return Err(FormatError::InvalidKdfParams(
564            "argon2id salt length must be 8..64 bytes",
565        ));
566    }
567    if t_cost == 0 {
568        return Err(FormatError::InvalidKdfParams(
569            "argon2id t_cost must be non-zero",
570        ));
571    }
572    if parallelism == 0 {
573        return Err(FormatError::InvalidKdfParams(
574            "argon2id parallelism must be non-zero",
575        ));
576    }
577    validate_argon2id_bounds(t_cost, m_cost_kib, parallelism, salt_length)?;
578
579    let total_len = ARGON2ID_FIXED_PARAMS_LEN + salt_length as usize;
580    if bytes.len() < total_len {
581        return Err(FormatError::TruncatedKdfParams);
582    }
583    Ok((
584        KdfParams::Argon2id {
585            t_cost,
586            m_cost_kib,
587            parallelism,
588            salt: bytes[ARGON2ID_FIXED_PARAMS_LEN..total_len].to_vec(),
589        },
590        total_len,
591    ))
592}
593
594fn parse_recipient_wrap_kdf_params(bytes: &[u8]) -> Result<(KdfParams, usize), FormatError> {
595    if bytes.len() < RECIPIENT_WRAP_KDF_PARAMS_LEN {
596        return Err(FormatError::TruncatedKdfParams);
597    }
598    let algo_tag = read_u16(bytes, 0)?;
599    if algo_tag != KdfAlgo::RecipientWrap as u16 {
600        return Err(FormatError::KdfAlgoTagMismatch {
601            expected: KdfAlgo::RecipientWrap as u16,
602            actual: algo_tag,
603        });
604    }
605    let key_wrap_table_length = read_u32(bytes, 2)?;
606    let key_wrap_table_record_count = read_u32(bytes, 6)?;
607    let table_version = read_u16(bytes, 10)?;
608    if key_wrap_table_length == 0 {
609        return Err(FormatError::InvalidKdfParams(
610            "recipient-wrap key_wrap_table_length must be non-zero",
611        ));
612    }
613    if key_wrap_table_length > READER_MAX_KEY_WRAP_TABLE_LEN {
614        return Err(FormatError::ReaderResourceLimitExceeded {
615            field: "KeyWrapTableV1 length",
616            cap: READER_MAX_KEY_WRAP_TABLE_LEN as u64,
617            actual: key_wrap_table_length as u64,
618        });
619    }
620    if key_wrap_table_record_count > READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS {
621        return Err(FormatError::ReaderResourceLimitExceeded {
622            field: "KeyWrapTableV1 recipient_record_count",
623            cap: READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS as u64,
624            actual: key_wrap_table_record_count as u64,
625        });
626    }
627    if table_version != RECIPIENT_WRAP_TABLE_VERSION {
628        return Err(FormatError::InvalidKdfParams(
629            "recipient-wrap table version must be 1",
630        ));
631    }
632    let reserved = read_u16(bytes, 12)?;
633    if reserved != 0 {
634        return Err(FormatError::InvalidKdfParams(
635            "recipient-wrap reserved bytes must be zero",
636        ));
637    }
638    let mut key_wrap_table_digest = [0u8; 32];
639    key_wrap_table_digest.copy_from_slice(&bytes[14..RECIPIENT_WRAP_KDF_PARAMS_LEN]);
640    Ok((
641        KdfParams::RecipientWrap {
642            key_wrap_table_length,
643            key_wrap_table_record_count,
644            key_wrap_table_version: table_version,
645            key_wrap_table_digest,
646        },
647        RECIPIENT_WRAP_KDF_PARAMS_LEN,
648    ))
649}
650
651fn validate_argon2id_bounds(
652    t_cost: u32,
653    m_cost_kib: u32,
654    parallelism: u32,
655    salt_length: u16,
656) -> Result<(), FormatError> {
657    if !(ARGON2ID_MIN_SALT_LEN..=ARGON2ID_MAX_SALT_LEN).contains(&salt_length) {
658        return Err(FormatError::InvalidKdfParams(
659            "argon2id salt length must be 8..64 bytes",
660        ));
661    }
662    if t_cost == 0 {
663        return Err(FormatError::InvalidKdfParams(
664            "argon2id t_cost must be non-zero",
665        ));
666    }
667    if t_cost > READER_MAX_ARGON2ID_T_COST {
668        return Err(FormatError::ReaderResourceLimitExceeded {
669            field: "argon2id t_cost",
670            cap: READER_MAX_ARGON2ID_T_COST as u64,
671            actual: t_cost as u64,
672        });
673    }
674    if parallelism == 0 {
675        return Err(FormatError::InvalidKdfParams(
676            "argon2id parallelism must be non-zero",
677        ));
678    }
679    if parallelism > READER_MAX_ARGON2ID_PARALLELISM {
680        return Err(FormatError::ReaderResourceLimitExceeded {
681            field: "argon2id parallelism",
682            cap: READER_MAX_ARGON2ID_PARALLELISM as u64,
683            actual: parallelism as u64,
684        });
685    }
686    if m_cost_kib > READER_MAX_ARGON2ID_M_COST_KIB {
687        return Err(FormatError::ReaderResourceLimitExceeded {
688            field: "argon2id m_cost_kib",
689            cap: READER_MAX_ARGON2ID_M_COST_KIB as u64,
690            actual: m_cost_kib as u64,
691        });
692    }
693    let min_memory = parallelism
694        .checked_mul(8)
695        .ok_or(FormatError::InvalidKdfParams(
696            "argon2id memory requirement overflow",
697        ))?;
698    if m_cost_kib < min_memory {
699        return Err(FormatError::InvalidKdfParams(
700            "argon2id memory must be at least 8 KiB per lane",
701        ));
702    }
703    Ok(())
704}
705
706fn expand_subkey(hk: &Hkdf<Sha256>, info: &[u8]) -> Result<[u8; SUBKEY_LEN], FormatError> {
707    let mut output = [0u8; SUBKEY_LEN];
708    hk.expand(info, &mut output)
709        .map_err(|_| FormatError::HkdfExpandFailure)?;
710    Ok(output)
711}
712
713fn nonce_or_aad_info(
714    prefix: &[u8],
715    domain: &[u8],
716    archive_uuid: &[u8; 16],
717    session_id: &[u8; 16],
718    counter: u64,
719) -> Result<Vec<u8>, FormatError> {
720    let domain_len = u16::try_from(domain.len()).map_err(|_| FormatError::DomainTooLong)?;
721    let mut info = Vec::with_capacity(prefix.len() + 2 + domain.len() + 16 + 16 + 8);
722    info.extend_from_slice(prefix);
723    info.extend_from_slice(&domain_len.to_le_bytes());
724    info.extend_from_slice(domain);
725    info.extend_from_slice(archive_uuid);
726    info.extend_from_slice(session_id);
727    info.extend_from_slice(&counter.to_le_bytes());
728    Ok(info)
729}
730
731fn validate_nonce_len(algo: AeadAlgo, nonce: &[u8]) -> Result<(), FormatError> {
732    let expected = algo.nonce_len();
733    if nonce.len() != expected {
734        return Err(FormatError::InvalidNonceLength {
735            algo,
736            expected,
737            actual: nonce.len(),
738        });
739    }
740    Ok(())
741}
742
743fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
744    let array: [u8; 2] = bytes
745        .get(offset..offset + 2)
746        .ok_or(FormatError::InvalidLength {
747            structure: "u16",
748            expected: offset + 2,
749            actual: bytes.len(),
750        })?
751        .try_into()
752        .expect("slice length checked");
753    Ok(u16::from_le_bytes(array))
754}
755
756fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
757    let array: [u8; 4] = bytes
758        .get(offset..offset + 4)
759        .ok_or(FormatError::InvalidLength {
760            structure: "u32",
761            expected: offset + 4,
762            actual: bytes.len(),
763        })?
764        .try_into()
765        .expect("slice length checked");
766    Ok(u32::from_le_bytes(array))
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use crate::format::VOLUME_FORMAT_REV_43;
773
774    fn uuid() -> [u8; 16] {
775        [0x11; 16]
776    }
777
778    fn session() -> [u8; 16] {
779        [0x22; 16]
780    }
781
782    fn legacy_nonce_info(
783        domain: &[u8],
784        archive_uuid: &[u8; 16],
785        session_id: &[u8; 16],
786        counter: u64,
787    ) -> Vec<u8> {
788        let mut info = Vec::with_capacity(b"tzap-v1-nonce".len() + domain.len() + 16 + 16 + 8);
789        info.extend_from_slice(b"tzap-v1-nonce");
790        info.extend_from_slice(domain);
791        info.extend_from_slice(archive_uuid);
792        info.extend_from_slice(session_id);
793        info.extend_from_slice(&counter.to_le_bytes());
794        info
795    }
796
797    #[test]
798    fn parses_raw_kdf_params() {
799        let (params, consumed) = KdfParams::parse(KdfAlgo::Raw, &0u16.to_le_bytes()).unwrap();
800        assert_eq!(params, KdfParams::Raw);
801        assert_eq!(consumed, 2);
802    }
803
804    #[test]
805    fn parses_none_kdf_params() {
806        let (params, consumed) =
807            KdfParams::parse(KdfAlgo::None, &(KdfAlgo::None as u16).to_le_bytes()).unwrap();
808        assert_eq!(params, KdfParams::None);
809        assert_eq!(consumed, 2);
810
811        assert_eq!(
812            KdfParams::parse(KdfAlgo::None, &(KdfAlgo::Raw as u16).to_le_bytes()).unwrap_err(),
813            FormatError::KdfAlgoTagMismatch {
814                expected: KdfAlgo::None as u16,
815                actual: KdfAlgo::Raw as u16,
816            }
817        );
818    }
819
820    #[test]
821    fn parses_argon2id_kdf_params() {
822        let mut bytes = Vec::new();
823        bytes.extend_from_slice(&(KdfAlgo::Argon2id as u16).to_le_bytes());
824        bytes.extend_from_slice(&1u32.to_le_bytes());
825        bytes.extend_from_slice(&8u32.to_le_bytes());
826        bytes.extend_from_slice(&1u32.to_le_bytes());
827        bytes.extend_from_slice(&8u16.to_le_bytes());
828        bytes.extend_from_slice(b"12345678");
829
830        let (params, consumed) = KdfParams::parse(KdfAlgo::Argon2id, &bytes).unwrap();
831        assert_eq!(consumed, 24);
832        assert_eq!(
833            params,
834            KdfParams::Argon2id {
835                t_cost: 1,
836                m_cost_kib: 8,
837                parallelism: 1,
838                salt: b"12345678".to_vec()
839            }
840        );
841    }
842
843    #[test]
844    fn parses_recipient_wrap_kdf_params() {
845        let mut bytes = Vec::new();
846        bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
847        bytes.extend_from_slice(&16u32.to_le_bytes());
848        bytes.extend_from_slice(&4u32.to_le_bytes());
849        bytes.extend_from_slice(&1u16.to_le_bytes());
850        bytes.extend_from_slice(&0u16.to_le_bytes());
851        bytes.extend_from_slice(&[0xaau8; 32]);
852
853        let (params, consumed) = KdfParams::parse(KdfAlgo::RecipientWrap, &bytes).unwrap();
854        assert_eq!(consumed, 46);
855        assert_eq!(
856            params,
857            KdfParams::RecipientWrap {
858                key_wrap_table_length: 16,
859                key_wrap_table_record_count: 4,
860                key_wrap_table_version: 1,
861                key_wrap_table_digest: [0xaau8; 32]
862            }
863        );
864    }
865
866    #[test]
867    fn rejects_invalid_recipient_wrap_kdf_params_fields() {
868        let mut bytes = Vec::new();
869        bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
870        bytes.extend_from_slice(&16u32.to_le_bytes());
871        bytes.extend_from_slice(&4u32.to_le_bytes());
872        bytes.extend_from_slice(&2u16.to_le_bytes());
873        bytes.extend_from_slice(&0u16.to_le_bytes());
874        bytes.extend_from_slice(&[0xaau8; 32]);
875        assert_eq!(
876            KdfParams::parse(KdfAlgo::RecipientWrap, &bytes).unwrap_err(),
877            FormatError::InvalidKdfParams("recipient-wrap table version must be 1")
878        );
879
880        let mut bytes = Vec::new();
881        bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
882        bytes.extend_from_slice(&0u32.to_le_bytes());
883        bytes.extend_from_slice(&1u32.to_le_bytes());
884        bytes.extend_from_slice(&1u16.to_le_bytes());
885        bytes.extend_from_slice(&0u16.to_le_bytes());
886        bytes.extend_from_slice(&[0u8; 32]);
887        assert_eq!(
888            KdfParams::parse(KdfAlgo::RecipientWrap, &bytes).unwrap_err(),
889            FormatError::InvalidKdfParams("recipient-wrap key_wrap_table_length must be non-zero"),
890        );
891
892        let mut bytes = Vec::new();
893        bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
894        bytes.extend_from_slice(&(READER_MAX_KEY_WRAP_TABLE_LEN + 1).to_le_bytes());
895        bytes.extend_from_slice(&0u32.to_le_bytes());
896        bytes.extend_from_slice(&1u16.to_le_bytes());
897        bytes.extend_from_slice(&0u16.to_le_bytes());
898        bytes.extend_from_slice(&[0u8; 32]);
899        assert_eq!(
900            KdfParams::parse(KdfAlgo::RecipientWrap, &bytes).unwrap_err(),
901            FormatError::ReaderResourceLimitExceeded {
902                field: "KeyWrapTableV1 length",
903                cap: READER_MAX_KEY_WRAP_TABLE_LEN as u64,
904                actual: (READER_MAX_KEY_WRAP_TABLE_LEN + 1) as u64,
905            },
906        );
907
908        let mut bytes = Vec::new();
909        bytes.extend_from_slice(&(KdfAlgo::RecipientWrap as u16).to_le_bytes());
910        bytes.extend_from_slice(&16u32.to_le_bytes());
911        bytes.extend_from_slice(&(READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS + 1).to_le_bytes());
912        bytes.extend_from_slice(&1u16.to_le_bytes());
913        bytes.extend_from_slice(&0u16.to_le_bytes());
914        bytes.extend_from_slice(&[0u8; 32]);
915        assert_eq!(
916            KdfParams::parse(KdfAlgo::RecipientWrap, &bytes).unwrap_err(),
917            FormatError::ReaderResourceLimitExceeded {
918                field: "KeyWrapTableV1 recipient_record_count",
919                cap: READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS as u64,
920                actual: (READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS + 1) as u64,
921            },
922        );
923    }
924
925    #[test]
926    fn rejects_argon2id_params_above_reader_caps() {
927        let mut bytes = Vec::new();
928        bytes.extend_from_slice(&(KdfAlgo::Argon2id as u16).to_le_bytes());
929        bytes.extend_from_slice(&(READER_MAX_ARGON2ID_T_COST + 1).to_le_bytes());
930        bytes.extend_from_slice(&8u32.to_le_bytes());
931        bytes.extend_from_slice(&1u32.to_le_bytes());
932        bytes.extend_from_slice(&8u16.to_le_bytes());
933        bytes.extend_from_slice(b"12345678");
934
935        assert_eq!(
936            KdfParams::parse(KdfAlgo::Argon2id, &bytes).unwrap_err(),
937            FormatError::ReaderResourceLimitExceeded {
938                field: "argon2id t_cost",
939                cap: READER_MAX_ARGON2ID_T_COST as u64,
940                actual: (READER_MAX_ARGON2ID_T_COST + 1) as u64,
941            }
942        );
943
944        let err = MasterKey::derive_from_passphrase(
945            &KdfParams::Argon2id {
946                t_cost: 1,
947                m_cost_kib: READER_MAX_ARGON2ID_M_COST_KIB + 1,
948                parallelism: 1,
949                salt: b"12345678".to_vec(),
950            },
951            "passphrase",
952        )
953        .unwrap_err();
954        assert_eq!(
955            err,
956            FormatError::ReaderResourceLimitExceeded {
957                field: "argon2id m_cost_kib",
958                cap: READER_MAX_ARGON2ID_M_COST_KIB as u64,
959                actual: (READER_MAX_ARGON2ID_M_COST_KIB + 1) as u64,
960            }
961        );
962    }
963
964    #[test]
965    fn rejects_argon2id_salt_bounds_and_raw_kdf_truncation() {
966        fn argon_bytes(salt_len: u16, actual_salt: &[u8]) -> Vec<u8> {
967            let mut bytes = Vec::new();
968            bytes.extend_from_slice(&(KdfAlgo::Argon2id as u16).to_le_bytes());
969            bytes.extend_from_slice(&1u32.to_le_bytes());
970            bytes.extend_from_slice(&8u32.to_le_bytes());
971            bytes.extend_from_slice(&1u32.to_le_bytes());
972            bytes.extend_from_slice(&salt_len.to_le_bytes());
973            bytes.extend_from_slice(actual_salt);
974            bytes
975        }
976
977        assert_eq!(
978            KdfParams::parse(KdfAlgo::Raw, &[]).unwrap_err(),
979            FormatError::TruncatedKdfParams
980        );
981        assert_eq!(
982            KdfParams::parse(KdfAlgo::Argon2id, &argon_bytes(7, b"1234567")).unwrap_err(),
983            FormatError::InvalidKdfParams("argon2id salt length must be 8..64 bytes")
984        );
985        assert!(matches!(
986            KdfParams::parse(KdfAlgo::Argon2id, &argon_bytes(8, b"12345678")).unwrap(),
987            (KdfParams::Argon2id { .. }, 24)
988        ));
989        assert!(matches!(
990            KdfParams::parse(KdfAlgo::Argon2id, &argon_bytes(64, &[0x5a; 64])).unwrap(),
991            (KdfParams::Argon2id { .. }, 80)
992        ));
993        assert_eq!(
994            KdfParams::parse(KdfAlgo::Argon2id, &argon_bytes(65, &[0x5a; 65])).unwrap_err(),
995            FormatError::InvalidKdfParams("argon2id salt length must be 8..64 bytes")
996        );
997        assert_eq!(
998            KdfParams::parse(KdfAlgo::Argon2id, &argon_bytes(64, &[0x5a; 63])).unwrap_err(),
999            FormatError::TruncatedKdfParams
1000        );
1001    }
1002
1003    #[test]
1004    fn rejects_kdf_algo_tag_mismatch() {
1005        assert_eq!(
1006            KdfParams::parse(KdfAlgo::Raw, &(KdfAlgo::Argon2id as u16).to_le_bytes()).unwrap_err(),
1007            FormatError::KdfAlgoTagMismatch {
1008                expected: 0,
1009                actual: 1
1010            }
1011        );
1012    }
1013
1014    #[test]
1015    fn passphrase_normalization_preserves_archive_semantics() {
1016        assert_eq!(normalize_passphrase_nfc("e\u{301}\n\0"), "é\n\0".as_bytes());
1017    }
1018
1019    #[test]
1020    fn argon2id_passphrase_edge_vectors_are_literal() {
1021        let params = KdfParams::Argon2id {
1022            t_cost: 1,
1023            m_cost_kib: 8,
1024            parallelism: 1,
1025            salt: b"12345678".to_vec(),
1026        };
1027        let cases = [
1028            (
1029                "trailing newline",
1030                "pass\n",
1031                "f63027356e6da90a4f6c81af70b9e6f1b1967ab684ecda8257cb7d21de760623",
1032            ),
1033            (
1034                "embedded nul",
1035                "pass\0word",
1036                "23db596ddbaa8f3f36d653f456dd9819e342aad4e30224008a22f1fb7648780e",
1037            ),
1038            (
1039                "leading bom",
1040                "\u{feff}pass",
1041                "d493645da269dce9b0ab6d39367d94c1896b0f4a2c3ca486c775d7275b8558da",
1042            ),
1043        ];
1044
1045        for (name, passphrase, expected_hex) in cases {
1046            let master = MasterKey::derive_from_passphrase(&params, passphrase).unwrap();
1047            assert_eq!(hex::encode(master.0), expected_hex, "{name}");
1048        }
1049
1050        let without_newline = MasterKey::derive_from_passphrase(&params, "pass").unwrap();
1051        let with_newline = MasterKey::derive_from_passphrase(&params, "pass\n").unwrap();
1052        assert_ne!(without_newline, with_newline);
1053    }
1054
1055    #[test]
1056    fn argon2id_profile_rejects_alternate_version_vector() {
1057        let params = KdfParams::Argon2id {
1058            t_cost: 1,
1059            m_cost_kib: 8,
1060            parallelism: 1,
1061            salt: b"12345678".to_vec(),
1062        };
1063        let current = MasterKey::derive_from_passphrase(&params, "e\u{301}").unwrap();
1064
1065        let argon_params = Params::new(8, 1, 1, Some(MASTER_KEY_LEN)).unwrap();
1066        let old_argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x10, argon_params);
1067        let mut old_output = [0u8; MASTER_KEY_LEN];
1068        let passphrase = normalize_passphrase_nfc("e\u{301}");
1069        old_argon2
1070            .hash_password_into(&passphrase, b"12345678", &mut old_output)
1071            .unwrap();
1072
1073        assert_eq!(
1074            hex::encode(current.0),
1075            "24709642204c04bf88fb36550c478769eb10a0400c0493c9695d30fbf7082241"
1076        );
1077        assert_ne!(old_output, current.0);
1078    }
1079
1080    #[test]
1081    fn derives_argon2id_master_key_from_nfc_passphrase() {
1082        let params = KdfParams::Argon2id {
1083            t_cost: 1,
1084            m_cost_kib: 8,
1085            parallelism: 1,
1086            salt: b"12345678".to_vec(),
1087        };
1088        let one = MasterKey::derive_from_passphrase(&params, "e\u{301}").unwrap();
1089        let two = MasterKey::derive_from_passphrase(&params, "é").unwrap();
1090        assert_eq!(one.0, two.0);
1091        assert_ne!(one.0, [0u8; MASTER_KEY_LEN]);
1092    }
1093
1094    #[test]
1095    fn derives_stable_distinct_subkeys() {
1096        let master = MasterKey::from_raw_key(&[0x33; MASTER_KEY_LEN]).unwrap();
1097        let subkeys = Subkeys::derive(&master, &uuid(), &session()).unwrap();
1098        assert_ne!(subkeys.enc_key, subkeys.mac_key);
1099        assert_ne!(subkeys.index_root_key, subkeys.index_shard_key);
1100
1101        let repeat = Subkeys::derive(&master, &uuid(), &session()).unwrap();
1102        assert_eq!(subkeys, repeat);
1103    }
1104
1105    #[test]
1106    fn hkdf_passphrase_and_identity_vectors_are_literal() {
1107        let params = KdfParams::Argon2id {
1108            t_cost: 1,
1109            m_cost_kib: 8,
1110            parallelism: 1,
1111            salt: b"saltsalt".to_vec(),
1112        };
1113        let archive_uuid = core::array::from_fn::<_, 16, _>(|idx| 0x30 + idx as u8);
1114        let session_id = core::array::from_fn::<_, 16, _>(|idx| 0xc0 + idx as u8);
1115        let master = MasterKey::derive_from_passphrase(&params, "correct horse\n").unwrap();
1116        let subkeys = Subkeys::derive(&master, &archive_uuid, &session_id).unwrap();
1117
1118        assert_eq!(
1119            hex::encode(master.0),
1120            "c58d65c836c8a590c0d34fcc0907d876e969d72c51a267cad2518cfee8eb2a21"
1121        );
1122        assert_eq!(
1123            hex::encode(subkeys.enc_key),
1124            "786001f513f99062c7c7ef72c978847a7c2daa452f363177839ce2ed3ecfd5df"
1125        );
1126        assert_eq!(
1127            hex::encode(subkeys.mac_key),
1128            "024f2737f6db8aa03d3ce241d25c26fcc18bbcf4af242614c3d703224cd82b74"
1129        );
1130        assert_eq!(
1131            hex::encode(subkeys.index_nonce_seed),
1132            "5d51a19bf7f6d77ce7945517ce95837a089f8d1cd20aea43cbcb8d745c0668ee"
1133        );
1134
1135        let different_session = Subkeys::derive(&master, &archive_uuid, &[0xc1; 16]).unwrap();
1136        let different_archive = Subkeys::derive(&master, &[0x31; 16], &session_id).unwrap();
1137        assert_ne!(subkeys.enc_key, different_session.enc_key);
1138        assert_ne!(subkeys.enc_key, different_archive.enc_key);
1139    }
1140
1141    #[test]
1142    fn computes_and_verifies_hmac_domains() {
1143        let key = [0x44; SUBKEY_LEN];
1144        let covered = b"covered bytes";
1145        let tag = compute_hmac(HmacDomain::CryptoHeader, &key, &uuid(), &session(), covered);
1146        verify_hmac(
1147            HmacDomain::CryptoHeader,
1148            &key,
1149            &uuid(),
1150            &session(),
1151            covered,
1152            &tag,
1153        )
1154        .unwrap();
1155
1156        assert_eq!(
1157            verify_hmac(
1158                HmacDomain::ManifestFooter,
1159                &key,
1160                &uuid(),
1161                &session(),
1162                covered,
1163                &tag,
1164            )
1165            .unwrap_err(),
1166            FormatError::HmacMismatch {
1167                structure: "ManifestFooter"
1168            }
1169        );
1170    }
1171
1172    #[test]
1173    fn computes_and_verifies_unkeyed_integrity_domains() {
1174        let covered = b"covered bytes";
1175        let tag_v43 = compute_integrity_tag(
1176            HmacDomain::CryptoHeader,
1177            AeadAlgo::None,
1178            VOLUME_FORMAT_REV_43,
1179            None,
1180            &uuid(),
1181            &session(),
1182            covered,
1183        )
1184        .unwrap();
1185        let tag_v44 = compute_integrity_tag(
1186            HmacDomain::CryptoHeader,
1187            AeadAlgo::None,
1188            VOLUME_FORMAT_REV_44,
1189            None,
1190            &uuid(),
1191            &session(),
1192            covered,
1193        )
1194        .unwrap();
1195
1196        verify_integrity_tag(
1197            HmacDomain::CryptoHeader,
1198            AeadAlgo::None,
1199            VOLUME_FORMAT_REV_43,
1200            None,
1201            &uuid(),
1202            &session(),
1203            covered,
1204            &tag_v43,
1205        )
1206        .unwrap();
1207        verify_integrity_tag(
1208            HmacDomain::CryptoHeader,
1209            AeadAlgo::None,
1210            VOLUME_FORMAT_REV_44,
1211            None,
1212            &uuid(),
1213            &session(),
1214            covered,
1215            &tag_v44,
1216        )
1217        .unwrap();
1218        assert_eq!(
1219            verify_integrity_tag(
1220                HmacDomain::CryptoHeader,
1221                AeadAlgo::None,
1222                VOLUME_FORMAT_REV_44,
1223                None,
1224                &uuid(),
1225                &session(),
1226                covered,
1227                &tag_v43,
1228            )
1229            .unwrap_err(),
1230            FormatError::IntegrityDigestMismatch {
1231                structure: "CryptoHeader"
1232            }
1233        );
1234
1235        assert_eq!(
1236            verify_integrity_tag(
1237                HmacDomain::ManifestFooter,
1238                AeadAlgo::None,
1239                VOLUME_FORMAT_REV_43,
1240                None,
1241                &uuid(),
1242                &session(),
1243                covered,
1244                &tag_v43,
1245            )
1246            .unwrap_err(),
1247            FormatError::IntegrityDigestMismatch {
1248                structure: "ManifestFooter"
1249            }
1250        );
1251
1252        let manifest_tag_v43 = compute_integrity_tag(
1253            HmacDomain::ManifestFooter,
1254            AeadAlgo::None,
1255            VOLUME_FORMAT_REV_43,
1256            None,
1257            &uuid(),
1258            &session(),
1259            covered,
1260        )
1261        .unwrap();
1262        let manifest_tag_v44 = compute_integrity_tag(
1263            HmacDomain::ManifestFooter,
1264            AeadAlgo::None,
1265            VOLUME_FORMAT_REV_44,
1266            None,
1267            &uuid(),
1268            &session(),
1269            covered,
1270        )
1271        .unwrap();
1272        assert_eq!(
1273            verify_integrity_tag(
1274                HmacDomain::ManifestFooter,
1275                AeadAlgo::None,
1276                VOLUME_FORMAT_REV_43,
1277                None,
1278                &uuid(),
1279                &session(),
1280                covered,
1281                &manifest_tag_v43,
1282            )
1283            .unwrap(),
1284            ()
1285        );
1286        assert_eq!(
1287            verify_integrity_tag(
1288                HmacDomain::ManifestFooter,
1289                AeadAlgo::None,
1290                VOLUME_FORMAT_REV_44,
1291                None,
1292                &uuid(),
1293                &session(),
1294                covered,
1295                &manifest_tag_v44,
1296            )
1297            .unwrap(),
1298            ()
1299        );
1300        assert_ne!(
1301            tag_v43,
1302            compute_integrity_tag(
1303                HmacDomain::ManifestFooter,
1304                AeadAlgo::None,
1305                VOLUME_FORMAT_REV_43,
1306                None,
1307                &uuid(),
1308                &session(),
1309                covered,
1310            )
1311            .unwrap()
1312        );
1313        assert_ne!(tag_v43, tag_v44);
1314        assert_ne!(manifest_tag_v43, manifest_tag_v44);
1315    }
1316
1317    #[test]
1318    fn hmac_sidecar_domain_vector_and_boundary_bytes_are_literal() {
1319        let key = [0x44; SUBKEY_LEN];
1320        let covered = b"covered bytes";
1321        let tag = compute_hmac(
1322            HmacDomain::BootstrapSidecar,
1323            &key,
1324            &uuid(),
1325            &session(),
1326            covered,
1327        );
1328        assert_eq!(
1329            hex::encode(tag),
1330            "1ecc9e0c5c9079b6824e16c4468ac9df22ca50fa2a924d21a91aab33c3721d51"
1331        );
1332        verify_hmac(
1333            HmacDomain::BootstrapSidecar,
1334            &key,
1335            &uuid(),
1336            &session(),
1337            covered,
1338            &tag,
1339        )
1340        .unwrap();
1341
1342        for mutate_index in [0, covered.len() - 1] {
1343            let mut mutated = covered.to_vec();
1344            mutated[mutate_index] ^= 0x01;
1345            assert_eq!(
1346                verify_hmac(
1347                    HmacDomain::BootstrapSidecar,
1348                    &key,
1349                    &uuid(),
1350                    &session(),
1351                    &mutated,
1352                    &tag,
1353                )
1354                .unwrap_err(),
1355                FormatError::HmacMismatch {
1356                    structure: "BootstrapSidecarHeader"
1357                }
1358            );
1359        }
1360
1361        for mutate_index in [0, tag.len() - 1] {
1362            let mut mutated_tag = tag;
1363            mutated_tag[mutate_index] ^= 0x01;
1364            assert_eq!(
1365                verify_hmac(
1366                    HmacDomain::BootstrapSidecar,
1367                    &key,
1368                    &uuid(),
1369                    &session(),
1370                    covered,
1371                    &mutated_tag,
1372                )
1373                .unwrap_err(),
1374                FormatError::HmacMismatch {
1375                    structure: "BootstrapSidecarHeader"
1376                }
1377            );
1378        }
1379    }
1380
1381    #[test]
1382    fn derives_nonce_and_aad_with_domain_separation() {
1383        let seed = [0x55; SUBKEY_LEN];
1384        let nonce = derive_nonce(&seed, b"envelope", &uuid(), &session(), 7, 12).unwrap();
1385        let other = derive_nonce(&seed, b"idxroot", &uuid(), &session(), 7, 12).unwrap();
1386        assert_eq!(nonce.len(), 12);
1387        assert_ne!(nonce, other);
1388
1389        let aad = build_aad(b"envelope", &uuid(), &session(), 7).unwrap();
1390        assert!(aad.starts_with(b"tzap-v1-aad"));
1391        assert_ne!(aad, nonce);
1392    }
1393
1394    #[test]
1395    fn rejects_old_nonce_info_without_domain_length() {
1396        let key = [0x66; SUBKEY_LEN];
1397        let nonce_seed = [0x77; SUBKEY_LEN];
1398        let uuid = uuid();
1399        let session = session();
1400        let counter = 7u64;
1401        let domain = b"idxroot";
1402
1403        let ciphertext = encrypt_padded_aead_object(
1404            AeadObjectContext {
1405                algo: AeadAlgo::AesGcmSiv256,
1406                key: &key,
1407                nonce_seed: &nonce_seed,
1408                domain,
1409                archive_uuid: &uuid,
1410                session_id: &session,
1411                counter,
1412            },
1413            4096,
1414            b"index-root",
1415        )
1416        .unwrap();
1417        let mut legacy_nonce = vec![0u8; AeadAlgo::AesGcmSiv256.nonce_len()];
1418        Hkdf::<Sha256>::from_prk(&nonce_seed)
1419            .unwrap()
1420            .expand(
1421                &legacy_nonce_info(domain, &uuid, &session, counter),
1422                &mut legacy_nonce,
1423            )
1424            .unwrap();
1425        let aad = build_aad(domain, &uuid, &session, counter).unwrap();
1426
1427        assert_ne!(
1428            legacy_nonce,
1429            derive_nonce(
1430                &nonce_seed,
1431                domain,
1432                &uuid,
1433                &session,
1434                counter,
1435                AeadAlgo::AesGcmSiv256.nonce_len()
1436            )
1437            .unwrap(),
1438            "legacy nonce info encoding must differ from current encoding"
1439        );
1440
1441        assert_eq!(
1442            aead_decrypt(
1443                AeadAlgo::AesGcmSiv256,
1444                &key,
1445                &legacy_nonce,
1446                &aad,
1447                &ciphertext,
1448            )
1449            .unwrap_err(),
1450            FormatError::AeadFailure
1451        );
1452    }
1453
1454    #[test]
1455    fn aead_round_trips_all_registered_algorithms() {
1456        for algo in [
1457            AeadAlgo::AesGcmSiv256,
1458            AeadAlgo::XChaCha20Poly1305,
1459            AeadAlgo::AesGcm256,
1460        ] {
1461            let key = [0x66; SUBKEY_LEN];
1462            let nonce = derive_nonce(
1463                &[0x77; SUBKEY_LEN],
1464                b"envelope",
1465                &uuid(),
1466                &session(),
1467                0,
1468                algo.nonce_len(),
1469            )
1470            .unwrap();
1471            let aad = build_aad(b"envelope", &uuid(), &session(), 0).unwrap();
1472            let ciphertext = aead_encrypt(algo, &key, &nonce, &aad, b"plaintext").unwrap();
1473            assert_ne!(ciphertext, b"plaintext");
1474            let plaintext = aead_decrypt(algo, &key, &nonce, &aad, &ciphertext).unwrap();
1475            assert_eq!(plaintext, b"plaintext");
1476
1477            let mut tampered = ciphertext;
1478            tampered[0] ^= 1;
1479            assert_eq!(
1480                aead_decrypt(algo, &key, &nonce, &aad, &tampered).unwrap_err(),
1481                FormatError::AeadFailure
1482            );
1483        }
1484    }
1485
1486    #[test]
1487    fn aead_none_passes_plaintext_through() {
1488        let ciphertext =
1489            aead_encrypt(AeadAlgo::None, &[0; SUBKEY_LEN], &[], b"aad", b"plaintext").unwrap();
1490        assert_eq!(ciphertext, b"plaintext");
1491        assert_eq!(
1492            aead_decrypt(AeadAlgo::None, &[0; SUBKEY_LEN], &[], b"aad", &ciphertext).unwrap(),
1493            b"plaintext"
1494        );
1495        assert_eq!(AeadAlgo::None.nonce_len(), 0);
1496        assert_eq!(AeadAlgo::None.tag_len(), 0);
1497    }
1498
1499    #[test]
1500    fn aead_rejects_wrong_nonce_length() {
1501        assert_eq!(
1502            aead_encrypt(AeadAlgo::AesGcmSiv256, &[0; SUBKEY_LEN], &[0; 11], b"", b"").unwrap_err(),
1503            FormatError::InvalidNonceLength {
1504                algo: AeadAlgo::AesGcmSiv256,
1505                expected: 12,
1506                actual: 11
1507            }
1508        );
1509    }
1510
1511    #[test]
1512    fn padded_aead_object_round_trips_with_derived_nonce_and_aad() {
1513        let key = [0x66; SUBKEY_LEN];
1514        let nonce_seed = [0x77; SUBKEY_LEN];
1515        let uuid = uuid();
1516        let session = session();
1517        let context = AeadObjectContext {
1518            algo: AeadAlgo::AesGcmSiv256,
1519            key: &key,
1520            nonce_seed: &nonce_seed,
1521            domain: b"envelope",
1522            archive_uuid: &uuid,
1523            session_id: &session,
1524            counter: 3,
1525        };
1526        let ciphertext = encrypt_padded_aead_object(context, 4096, b"packed frames").unwrap();
1527        assert_eq!(ciphertext.len() % 4096, 0);
1528
1529        let plaintext = decrypt_padded_aead_object(context, &ciphertext).unwrap();
1530        assert_eq!(plaintext, b"packed frames");
1531
1532        assert_eq!(
1533            decrypt_padded_aead_object(
1534                AeadObjectContext {
1535                    domain: b"idxroot",
1536                    ..context
1537                },
1538                &ciphertext,
1539            )
1540            .unwrap_err(),
1541            FormatError::AeadFailure
1542        );
1543    }
1544
1545    #[test]
1546    fn rejects_index_root_aad_counter_mismatch() {
1547        let key = [0x99; SUBKEY_LEN];
1548        let nonce_seed = [0x88; SUBKEY_LEN];
1549        let uuid = uuid();
1550        let session = session();
1551
1552        let ciphertext = encrypt_padded_aead_object(
1553            AeadObjectContext {
1554                algo: AeadAlgo::AesGcmSiv256,
1555                key: &key,
1556                nonce_seed: &nonce_seed,
1557                domain: b"idxroot",
1558                archive_uuid: &uuid,
1559                session_id: &session,
1560                counter: 0,
1561            },
1562            4096,
1563            b"index-root-meta",
1564        )
1565        .unwrap();
1566
1567        let nonce = derive_nonce(
1568            &nonce_seed,
1569            b"idxroot",
1570            &uuid,
1571            &session,
1572            0,
1573            AeadAlgo::AesGcmSiv256.nonce_len(),
1574        )
1575        .unwrap();
1576        let mismatched_aad = build_aad(b"idxroot", &uuid, &session, 1).unwrap();
1577
1578        assert_eq!(
1579            aead_decrypt(
1580                AeadAlgo::AesGcmSiv256,
1581                &key,
1582                &nonce,
1583                &mismatched_aad,
1584                &ciphertext,
1585            )
1586            .unwrap_err(),
1587            FormatError::AeadFailure
1588        );
1589    }
1590}