Skip to main content

tzap_core/
format.rs

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}