1use thiserror::Error;
2
3pub const FORMAT_VERSION: u16 = 1;
4pub const VOLUME_FORMAT_REV_43: u16 = 43;
5pub const VOLUME_FORMAT_REV_44: u16 = 44;
6pub const READER_MAX_SUPPORTED_VOLUME_FORMAT_REV: u16 = VOLUME_FORMAT_REV_44;
7pub const VOLUME_FORMAT_REV: u16 = VOLUME_FORMAT_REV_44;
8
9pub const VOLUME_HEADER_LEN: usize = 128;
10pub const CRYPTO_HEADER_FIXED_LEN: usize = 76;
11pub const MANIFEST_FOOTER_LEN: usize = 136;
12pub const VOLUME_TRAILER_LEN: usize = 128;
13pub const ROOT_AUTH_FOOTER_FIXED_LEN: usize = 318;
14pub const ROOT_AUTH_SPEC_ID_V43: [u8; 24] = *b"tzap-root-auth-v0.43\0\0\0\0";
15pub const ROOT_AUTH_SPEC_ID_V44: [u8; 24] = *b"tzap-root-auth-v0.44\0\0\0\0";
16pub const ROOT_AUTH_SPEC_ID: [u8; 24] = ROOT_AUTH_SPEC_ID_V44;
17pub const CRITICAL_METADATA_IMAGE_FIXED_LEN_V43: usize = 320;
18pub const CRITICAL_METADATA_IMAGE_FIXED_LEN: usize = 364;
19pub const SERIALIZED_REGION_HEADER_LEN: usize = 16;
20pub const IMAGE_CRC_LEN: usize = 4;
21pub const CRITICAL_METADATA_RECOVERY_HEADER_LEN: usize = 116;
22pub const CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN: usize = 16;
23pub const CRITICAL_RECOVERY_LOCATOR_LEN: usize = 128;
24pub const LOCATOR_PAIR_LEN: usize = CRITICAL_RECOVERY_LOCATOR_LEN * 2;
25pub const READER_MAX_ROOT_AUTH_FOOTER_LEN: u32 = 160 * 1024;
26pub const READER_MAX_ROOT_AUTH_SIGNER_IDENTITY_LEN: u32 = 16 * 1024;
27pub const READER_MAX_ROOT_AUTH_AUTHENTICATOR_VALUE_LEN: u32 = 128 * 1024;
28pub const READER_MAX_KEY_WRAP_TABLE_LEN: u32 = 1024 * 1024;
29pub const READER_MAX_KEY_WRAP_TABLE_RECIPIENT_RECORDS: u32 = 4096;
30pub const READER_MAX_CMRA_PARITY_PCT: u32 = 100;
31pub const BOOTSTRAP_SIDECAR_HEADER_LEN: usize = 128;
32pub const BLOCK_RECORD_FRAMING_LEN: usize = 20;
33pub const CRYPTO_HEADER_HMAC_LEN: usize = 32;
34pub const CRYPTO_EXTENSION_HEADER_LEN: usize = 6;
35pub const CRYPTO_EXTENSION_MAX_VALUE_LEN: u32 = 256;
36pub const MASTER_KEY_LEN: usize = 32;
37pub const SUBKEY_LEN: usize = 32;
38pub const READER_MAX_ARGON2ID_M_COST_KIB: u32 = 4 * 1024 * 1024;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[repr(u16)]
42pub enum VolumeFormatRevision {
43 V43 = VOLUME_FORMAT_REV_43,
44 V44 = VOLUME_FORMAT_REV_44,
45}
46
47impl VolumeFormatRevision {
48 pub const fn from_u16(value: u16) -> Option<Self> {
49 match value {
50 VOLUME_FORMAT_REV_43 => Some(Self::V43),
51 VOLUME_FORMAT_REV_44 => Some(Self::V44),
52 _ => None,
53 }
54 }
55}
56
57#[derive(Debug, Error)]
58pub enum ExtractError {
59 #[error(transparent)]
60 Format(#[from] FormatError),
61
62 #[error("extraction output write failed")]
63 Output(#[source] std::io::Error),
64}
65
66#[derive(Debug, Error)]
67pub enum ArchiveWriteError {
68 #[error(transparent)]
69 Format(#[from] FormatError),
70
71 #[error("archive I/O failed")]
72 Io(#[source] std::io::Error),
73}
74pub const READER_MAX_ARGON2ID_T_COST: u32 = 100;
75pub const READER_MAX_ARGON2ID_PARALLELISM: u32 = 64;
76pub const READER_MAX_CRYPTO_HEADER_LEN: u32 = 64 * 1024;
77pub const READER_MAX_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
78pub const READER_MAX_ENVELOPE_TARGET_SIZE: u32 = 64 * 1024 * 1024;
79pub const READER_MAX_BLOCK_SIZE: u32 = 1024 * 1024;
80pub const READER_MAX_STRIPE_WIDTH: u32 = 4096;
81pub const READER_MAX_FEC_CLASS_SHARDS: u32 = 4096;
82pub const READER_MAX_INDEX_FEC_CLASS_SHARDS: u32 = 4096;
83pub const READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS: u32 = 131_070;
84pub const READER_MAX_PATH_LENGTH: u32 = 4096;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[repr(u16)]
88pub enum CompressionAlgo {
89 None = 0,
90 ZstdFramed = 1,
91}
92
93impl TryFrom<u16> for CompressionAlgo {
94 type Error = FormatError;
95
96 fn try_from(value: u16) -> Result<Self, Self::Error> {
97 match value {
98 0 => Ok(Self::None),
99 1 => Ok(Self::ZstdFramed),
100 other => Err(FormatError::UnknownCompressionAlgo(other)),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[repr(u16)]
107pub enum AeadAlgo {
108 None = 0,
109 AesGcmSiv256 = 1,
110 XChaCha20Poly1305 = 2,
111 AesGcm256 = 3,
112}
113
114impl TryFrom<u16> for AeadAlgo {
115 type Error = FormatError;
116
117 fn try_from(value: u16) -> Result<Self, Self::Error> {
118 match value {
119 0 => Ok(Self::None),
120 1 => Ok(Self::AesGcmSiv256),
121 2 => Ok(Self::XChaCha20Poly1305),
122 3 => Ok(Self::AesGcm256),
123 other => Err(FormatError::UnknownAeadAlgo(other)),
124 }
125 }
126}
127
128impl AeadAlgo {
129 pub const fn nonce_len(self) -> usize {
130 match self {
131 Self::None => 0,
132 Self::AesGcmSiv256 | Self::AesGcm256 => 12,
133 Self::XChaCha20Poly1305 => 24,
134 }
135 }
136
137 pub const fn tag_len(self) -> usize {
138 match self {
139 Self::None => 0,
140 Self::AesGcmSiv256 | Self::XChaCha20Poly1305 | Self::AesGcm256 => 16,
141 }
142 }
143
144 pub const fn is_encrypted(self) -> bool {
145 !matches!(self, Self::None)
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150#[repr(u16)]
151pub enum FecAlgo {
152 None = 0,
153 ReedSolomonGF16 = 1,
154 Wirehair = 2,
155}
156
157impl TryFrom<u16> for FecAlgo {
158 type Error = FormatError;
159
160 fn try_from(value: u16) -> Result<Self, Self::Error> {
161 match value {
162 0 => Ok(Self::None),
163 1 => Ok(Self::ReedSolomonGF16),
164 2 => Ok(Self::Wirehair),
165 other => Err(FormatError::UnknownFecAlgo(other)),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171#[repr(u16)]
172pub enum KdfAlgo {
173 Raw = 0,
174 Argon2id = 1,
175 None = 2,
176 RecipientWrap = 3,
177}
178
179impl TryFrom<u16> for KdfAlgo {
180 type Error = FormatError;
181
182 fn try_from(value: u16) -> Result<Self, Self::Error> {
183 match value {
184 0 => Ok(Self::Raw),
185 1 => Ok(Self::Argon2id),
186 2 => Ok(Self::None),
187 3 => Ok(Self::RecipientWrap),
188 other => Err(FormatError::UnknownKdfAlgo(other)),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[repr(u8)]
195pub enum BlockKind {
196 PayloadData = 0,
197 PayloadParity = 1,
198 IndexRootData = 2,
199 IndexRootParity = 3,
200 IndexShardData = 4,
201 IndexShardParity = 5,
202 DictionaryData = 6,
203 DictionaryParity = 7,
204 DirectoryHintData = 8,
205 DirectoryHintParity = 9,
206}
207
208impl TryFrom<u8> for BlockKind {
209 type Error = FormatError;
210
211 fn try_from(value: u8) -> Result<Self, Self::Error> {
212 match value {
213 0 => Ok(Self::PayloadData),
214 1 => Ok(Self::PayloadParity),
215 2 => Ok(Self::IndexRootData),
216 3 => Ok(Self::IndexRootParity),
217 4 => Ok(Self::IndexShardData),
218 5 => Ok(Self::IndexShardParity),
219 6 => Ok(Self::DictionaryData),
220 7 => Ok(Self::DictionaryParity),
221 8 => Ok(Self::DirectoryHintData),
222 9 => Ok(Self::DirectoryHintParity),
223 other => Err(FormatError::UnknownBlockKind(other)),
224 }
225 }
226}
227
228impl BlockKind {
229 pub const fn is_data(self) -> bool {
230 matches!(
231 self,
232 Self::PayloadData
233 | Self::IndexRootData
234 | Self::IndexShardData
235 | Self::DictionaryData
236 | Self::DirectoryHintData
237 )
238 }
239
240 pub const fn is_parity(self) -> bool {
241 !self.is_data()
242 }
243}
244
245#[derive(Debug, Error, PartialEq, Eq)]
246pub enum FormatError {
247 #[error("unknown compression algorithm id {0}")]
248 UnknownCompressionAlgo(u16),
249
250 #[error("unknown AEAD algorithm id {0}")]
251 UnknownAeadAlgo(u16),
252
253 #[error("unknown FEC algorithm id {0}")]
254 UnknownFecAlgo(u16),
255
256 #[error("unknown KDF algorithm id {0}")]
257 UnknownKdfAlgo(u16),
258
259 #[error("unknown block kind {0}")]
260 UnknownBlockKind(u8),
261
262 #[error("invalid length for {structure}: expected {expected}, actual {actual}")]
263 InvalidLength {
264 structure: &'static str,
265 expected: usize,
266 actual: usize,
267 },
268
269 #[error("bad magic for {structure}")]
270 BadMagic { structure: &'static str },
271
272 #[error("bad CRC32C for {structure}")]
273 BadCrc { structure: &'static str },
274
275 #[error("unsupported format version {0}")]
276 UnsupportedFormatVersion(u16),
277
278 #[error(
279 "unsupported volume format revision {volume_format_rev} for format version {format_version}; reader supports up to {reader_max_supported_revision}"
280 )]
281 UnsupportedVolumeFormatRevision {
282 format_version: u16,
283 volume_format_rev: u16,
284 reader_max_supported_revision: u16,
285 },
286
287 #[error("non-zero reserved bytes in {structure}")]
288 NonZeroReserved { structure: &'static str },
289
290 #[error("non-canonical CryptoHeader offset {0}")]
291 NonCanonicalCryptoHeaderOffset(u32),
292
293 #[error("stripe width must be non-zero")]
294 ZeroStripeWidth,
295
296 #[error("volume index {volume_index} is outside stripe width {stripe_width}")]
297 VolumeIndexOutOfRange {
298 volume_index: u32,
299 stripe_width: u32,
300 },
301
302 #[error(
303 "CryptoHeader length mismatch: fixed header says {fixed}, volume header says {volume}"
304 )]
305 CryptoHeaderLengthMismatch { fixed: u32, volume: u32 },
306
307 #[error("compression algorithm {0:?} is not valid for v0.43")]
308 UnsupportedCompression(CompressionAlgo),
309
310 #[error("FEC algorithm {0:?} is not valid for v0.43")]
311 UnsupportedFec(FecAlgo),
312
313 #[error("invalid v0.43 protection mode: aead_algo={aead_algo:?}, kdf_algo={kdf_algo:?}")]
314 InvalidProtectionMode {
315 aead_algo: AeadAlgo,
316 kdf_algo: KdfAlgo,
317 },
318
319 #[error("invalid boolean field {field}={value}")]
320 InvalidBoolean { field: &'static str, value: u8 },
321
322 #[error("volume loss tolerance {volume_loss_tolerance} must be less than stripe width {stripe_width}")]
323 VolumeLossToleranceOutOfRange {
324 volume_loss_tolerance: u8,
325 stripe_width: u32,
326 },
327
328 #[error("bit rot buffer pct {0} exceeds 100")]
329 BitRotBufferPctTooLarge(u8),
330
331 #[error("data shard maximum {field} must be non-zero")]
332 ZeroDataShardMaximum { field: &'static str },
333
334 #[error("chunk_size must be non-zero")]
335 ZeroChunkSize,
336
337 #[error("envelope_target_size must be non-zero")]
338 ZeroEnvelopeTargetSize,
339
340 #[error("chunk_size {chunk_size} exceeds envelope_target_size {envelope_target_size}")]
341 ChunkSizeExceedsEnvelopeTarget {
342 chunk_size: u32,
343 envelope_target_size: u32,
344 },
345
346 #[error("block_size {0} is below the v0.43 minimum")]
347 BlockSizeTooSmall(u32),
348
349 #[error("block_size {0} must be even")]
350 OddBlockSize(u32),
351
352 #[error("reader resource cap exceeded for {field}: cap {cap}, actual {actual}")]
353 ReaderResourceLimitExceeded {
354 field: &'static str,
355 cap: u64,
356 actual: u64,
357 },
358
359 #[error("invalid block flags 0x{0:02x}")]
360 InvalidBlockFlags(u8),
361
362 #[error("parity block must not set the last-data flag")]
363 ParityBlockHasLastDataFlag,
364
365 #[error("invalid authoritative flag {0}")]
366 InvalidAuthoritativeFlag(u8),
367
368 #[error("invalid ManifestFooter length {0}")]
369 InvalidManifestFooterLength(u32),
370
371 #[error("IndexRoot encrypted size is not data_block_count * block_size")]
372 IndexRootSizeMismatch,
373
374 #[error("IndexRoot data block count and encrypted size must be non-zero")]
375 EmptyIndexRootExtent,
376
377 #[error("bootstrap sidecar version {0} is unsupported")]
378 UnsupportedBootstrapSidecarVersion(u32),
379
380 #[error("bootstrap sidecar has unknown flags 0x{0:08x}")]
381 UnknownBootstrapSidecarFlags(u32),
382
383 #[error("bootstrap sidecar present section has zero offset or length")]
384 EmptyBootstrapSidecarSection,
385
386 #[error("bootstrap sidecar absent section has non-zero offset or length")]
387 NonZeroAbsentBootstrapSidecarSection,
388
389 #[error("bootstrap sidecar sections are not packed canonically")]
390 NonCanonicalBootstrapSidecarLayout,
391
392 #[error("extension TLV header is truncated")]
393 TruncatedExtensionHeader,
394
395 #[error("extension TLV payload is truncated")]
396 TruncatedExtensionPayload,
397
398 #[error("extension TLV payload length {0} exceeds 256")]
399 ExtensionPayloadTooLarge(u32),
400
401 #[error("extension terminator is malformed")]
402 MalformedExtensionTerminator,
403
404 #[error("extension terminator is missing")]
405 MissingExtensionTerminator,
406
407 #[error("bytes follow extension terminator")]
408 BytesAfterExtensionTerminator,
409
410 #[error("CryptoHeader is too short: minimum {min}, actual {actual}")]
411 CryptoHeaderTooShort { min: usize, actual: usize },
412
413 #[error("KdfParams algo_tag {actual} does not match expected {expected}")]
414 KdfAlgoTagMismatch { expected: u16, actual: u16 },
415
416 #[error("KdfParams are truncated")]
417 TruncatedKdfParams,
418
419 #[error("invalid KdfParams: {0}")]
420 InvalidKdfParams(&'static str),
421
422 #[error("key material does not match KDF mode")]
423 KeyMaterialMismatch,
424
425 #[error("raw master key must be exactly 32 bytes")]
426 InvalidRawMasterKeyLength,
427
428 #[error("Argon2id derivation failed")]
429 Argon2idFailure,
430
431 #[error("HKDF expansion failed")]
432 HkdfExpandFailure,
433
434 #[error("HMAC verification failed for {structure}")]
435 HmacMismatch { structure: &'static str },
436
437 #[error("integrity digest verification failed for {structure}")]
438 IntegrityDigestMismatch { structure: &'static str },
439
440 #[error("forbidden CryptoHeader extension tag 0x{0:04x}")]
441 ForbiddenExtensionTag(u16),
442
443 #[error("unknown critical CryptoHeader extension tag 0x{0:04x}")]
444 UnknownCriticalExtension(u16),
445
446 #[error("duplicate known CryptoHeader extension tag 0x{0:04x}")]
447 DuplicateKnownExtension(u16),
448
449 #[error("malformed known CryptoHeader extension tag 0x{0:04x}")]
450 MalformedKnownExtension(u16),
451
452 #[error("padding input is empty")]
453 EmptyPaddedPlaintext,
454
455 #[error("invalid suffix padding")]
456 InvalidSuffixPadding,
457
458 #[error("non-zero suffix padding bytes")]
459 NonZeroPaddingBytes,
460
461 #[error("padding arithmetic overflow")]
462 PaddingOverflow,
463
464 #[error("AEAD operation failed")]
465 AeadFailure,
466
467 #[error("nonce/AAD domain is too long")]
468 DomainTooLong,
469
470 #[error("invalid nonce length for {algo:?}: expected {expected}, actual {actual}")]
471 InvalidNonceLength {
472 algo: AeadAlgo,
473 expected: usize,
474 actual: usize,
475 },
476
477 #[error("invalid AEAD key length")]
478 InvalidAeadKeyLength,
479
480 #[error("zstd compression failed")]
481 ZstdCompressionFailure,
482
483 #[error("zstd frame is empty")]
484 EmptyZstdFrame,
485
486 #[error("zstd frame is not a standard non-skippable frame")]
487 NotStandardZstdFrame,
488
489 #[error("zstd frame is truncated or corrupt")]
490 InvalidZstdFrame,
491
492 #[error("zstd frame has trailing bytes after the first complete frame")]
493 TrailingBytesAfterZstdFrame,
494
495 #[error("zstd decompression failed")]
496 ZstdDecompressionFailure,
497
498 #[error("zstd decompressed size mismatch: expected {expected}, actual {actual}")]
499 ZstdDecompressedSizeMismatch { expected: usize, actual: usize },
500
501 #[error("FEC object must contain at least one data shard")]
502 FecZeroDataShards,
503
504 #[error("FEC object total shard count {0} exceeds ReedSolomonGF16 limit")]
505 FecTooManyShards(usize),
506
507 #[error("FEC shard size must be even")]
508 FecOddShardSize,
509
510 #[error("FEC shards have inconsistent sizes")]
511 FecInconsistentShardSize,
512
513 #[error("FEC repair has too few available shards")]
514 FecTooFewAvailableShards,
515
516 #[error("FEC repair matrix is singular")]
517 FecSingularMatrix,
518
519 #[error("invalid metadata in {structure}: {reason}")]
520 InvalidMetadata {
521 structure: &'static str,
522 reason: &'static str,
523 },
524
525 #[error("metadata arithmetic overflow in {structure}")]
526 MetadataArithmeticOverflow { structure: &'static str },
527
528 #[error("hash-prefix collision run exceeds resource caps")]
529 HashPrefixCollisionRunExceeded,
530
531 #[error("unsafe archive path")]
532 UnsafeArchivePath,
533
534 #[error("unsafe extraction overwrite")]
535 UnsafeOverwrite,
536
537 #[error("filesystem extraction failed: {0}")]
538 FilesystemExtractionFailed(&'static str),
539
540 #[error("writer unsupported case: {0}")]
541 WriterUnsupported(&'static str),
542
543 #[error("writer invariant failed: {0}")]
544 WriterInvariant(&'static str),
545
546 #[error("reader unsupported case: {0}")]
547 ReaderUnsupported(&'static str),
548
549 #[error("invalid archive: {0}")]
550 InvalidArchive(&'static str),
551}
552
553pub fn root_auth_spec_id_for_revision(
554 format_version: u16,
555 volume_format_rev: u16,
556) -> Result<[u8; 24], FormatError> {
557 if format_version != FORMAT_VERSION {
558 return Err(FormatError::UnsupportedFormatVersion(format_version));
559 }
560 match volume_format_rev {
561 VOLUME_FORMAT_REV_43 => Ok(ROOT_AUTH_SPEC_ID_V43),
562 VOLUME_FORMAT_REV_44 => Ok(ROOT_AUTH_SPEC_ID_V44),
563 other => Err(FormatError::UnsupportedVolumeFormatRevision {
564 format_version,
565 volume_format_rev: other,
566 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
567 }),
568 }
569}