Skip to main content

tzap_core/
format.rs

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