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