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(¶ms, passphrase).unwrap();
1047 assert_eq!(hex::encode(master.0), expected_hex, "{name}");
1048 }
1049
1050 let without_newline = MasterKey::derive_from_passphrase(¶ms, "pass").unwrap();
1051 let with_newline = MasterKey::derive_from_passphrase(¶ms, "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(¶ms, "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(¶ms, "e\u{301}").unwrap();
1089 let two = MasterKey::derive_from_passphrase(¶ms, "é").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(¶ms, "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}