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