use crate::crypto::CipherId;
use crate::error::{Error, Result};
use crate::util::{Cursor, Writer};
pub const MAGIC_FILE: [u8; 8] = *b"OMYFILE\x01";
pub const MAGIC_SHARD: [u8; 8] = *b"OMYSHRD\x01";
pub const VERSION_MAJOR: u16 = 1;
pub const VERSION_MINOR: u16 = 0;
pub const FIXED_HEADER_LEN: usize = 96;
pub const SLOT_LEN: usize = 48;
pub const SLOT_COUNT: usize = 8;
pub const SLOT_COUNT_U8: u8 = 8;
const _: () = assert!(SLOT_COUNT == SLOT_COUNT_U8 as usize, "SLOT_COUNT 与其 u8 形式不一致");
pub const SLOT_AREA_LEN: usize = SLOT_LEN * SLOT_COUNT;
pub const SLOT_AREA_OFFSET: usize = FIXED_HEADER_LEN;
pub const TLV_AREA_OFFSET: usize = SLOT_AREA_OFFSET + SLOT_AREA_LEN;
pub const HEADER_MAC_LEN: usize = 32;
pub const SHARD_HEADER_LEN: usize = 56;
pub const MIN_CHUNK_SIZE: u32 = 64 * 1024;
pub const MAX_CHUNK_SIZE: u32 = 16 * 1024 * 1024;
pub const DEFAULT_CHUNK_SIZE: u32 = 256 * 1024;
pub const MOBILE_MAX_CHUNK_SIZE: u32 = 1024 * 1024;
pub const FILENAME_BUCKET: usize = 64;
pub const HEADER_LEN_FIELD_OFFSET: usize = 12;
const _: () = assert!(
HEADER_LEN_FIELD_OFFSET == MAGIC_FILE.len() + 2 + 2,
"HEADER_LEN_FIELD_OFFSET 必须等于 magic + version_major + version_minor 的长度"
);
pub mod parse_limits {
pub const MAX_HEADER_LEN: u32 = 64 * 1024 * 1024;
pub const MAX_TLV_LEN: u32 = 32 * 1024 * 1024;
pub const MAX_TLV_ENTRIES: usize = 4096;
pub const MAX_INDEX_ENTRIES: u32 = 16 * 1024 * 1024;
pub const MAX_PARSE_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
pub const MAX_FOLDER_ENTRIES: usize = 10_000_000;
}
pub const KDF_ARGON2ID: u8 = 1;
pub const COMPRESS_NONE: u8 = 0;
pub const COMPRESS_ZSTD: u8 = 1;
pub mod flags {
pub const FILENAME_ENCRYPTED: u32 = 1 << 0;
pub const EXT_PRESERVED: u32 = 1 << 1;
pub const COMPRESSED: u32 = 1 << 2;
pub const SHARDED: u32 = 1 << 3;
pub const HAS_THUMBNAIL: u32 = 1 << 4;
pub const CONTAINER: u32 = 1 << 5;
pub const DISGUISED: u32 = 1 << 6;
pub const TRANSCODED: u32 = 1 << 7;
pub const META_NORMALIZED: u32 = 1 << 8;
pub const SIZE_PADDED: u32 = 1 << 9;
pub const SLOT_DIRECTORY: u32 = 1 << 10;
pub const KNOWN_MASK: u32 = FILENAME_ENCRYPTED
| EXT_PRESERVED
| COMPRESSED
| SHARDED
| HAS_THUMBNAIL
| CONTAINER
| DISGUISED
| TRANSCODED
| META_NORMALIZED
| SIZE_PADDED
| SLOT_DIRECTORY;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixedHeader {
pub version_major: u16,
pub version_minor: u16,
pub header_len: u32,
pub file_uuid: [u8; 16],
pub vault_salt: [u8; 16],
pub flags: u32,
pub cipher_id: CipherId,
pub kdf_id: u8,
pub compress_id: u8,
pub slot_count: u8,
pub argon2_m_kib: u32,
pub argon2_t: u32,
pub argon2_p: u32,
pub chunk_size: u32,
pub plaintext_size: u64,
pub base_nonce: [u8; 7],
pub chunk_version: u8,
pub tlv_len: u32,
}
impl FixedHeader {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < FIXED_HEADER_LEN {
return Err(Error::Truncated {
context: "fixed header",
need: FIXED_HEADER_LEN,
got: data.len(),
});
}
let mut c = Cursor::new(data, "fixed header");
let magic: [u8; 8] = c.take_array()?;
if magic != MAGIC_FILE {
return Err(Error::BadMagic { got: magic });
}
let version_major = c.u16le()?;
let version_minor = c.u16le()?;
if version_major > VERSION_MAJOR {
return Err(Error::UnsupportedVersion {
found_major: version_major,
found_minor: version_minor,
supported_major: VERSION_MAJOR,
});
}
let header_len = c.u32le()?;
let file_uuid: [u8; 16] = c.take_array()?;
let vault_salt: [u8; 16] = c.take_array()?;
let flags = c.u32le()?;
let cipher_id = CipherId::from_u8(c.u8()?)?;
let kdf_id = c.u8()?;
let compress_id = c.u8()?;
let slot_count = c.u8()?;
let argon2_m_kib = c.u32le()?;
let argon2_t = c.u32le()?;
let argon2_p = c.u32le()?;
let chunk_size = c.u32le()?;
let plaintext_size = c.u64le()?;
let base_nonce: [u8; 7] = c.take_array()?;
let chunk_version = c.u8()?;
let tlv_len = c.u32le()?;
let _reserved = c.u32le()?;
debug_assert_eq!(c.position(), FIXED_HEADER_LEN, "fixed header 布局与规范不符");
let h = Self {
version_major,
version_minor,
header_len,
file_uuid,
vault_salt,
flags,
cipher_id,
kdf_id,
compress_id,
slot_count,
argon2_m_kib,
argon2_t,
argon2_p,
chunk_size,
plaintext_size,
base_nonce,
chunk_version,
tlv_len,
};
h.validate()?;
Ok(h)
}
pub fn validate(&self) -> Result<()> {
if self.slot_count as usize != SLOT_COUNT {
return Err(Error::MalformedHeader { reason: "slot_count must be 8" });
}
if self.kdf_id != KDF_ARGON2ID {
return Err(Error::MalformedHeader { reason: "unknown kdf_id" });
}
if self.compress_id != COMPRESS_NONE && self.compress_id != COMPRESS_ZSTD {
return Err(Error::MalformedHeader { reason: "unknown compress_id" });
}
if self.chunk_version != 0 {
return Err(Error::MalformedHeader { reason: "chunk_version must be 0 in v1" });
}
if self.header_len > parse_limits::MAX_HEADER_LEN {
return Err(Error::MalformedHeader {
reason: "header_len exceeds the 64 MiB parse limit",
});
}
if self.tlv_len > parse_limits::MAX_TLV_LEN {
return Err(Error::MalformedHeader {
reason: "tlv_len exceeds the 32 MiB parse limit",
});
}
if self.chunk_size == 0 {
return Err(Error::MalformedHeader { reason: "chunk_size must not be zero" });
}
if self.chunk_size > parse_limits::MAX_PARSE_CHUNK_SIZE {
return Err(Error::MalformedHeader {
reason: "chunk_size exceeds the 64 MiB parse limit",
});
}
let expect_len = TLV_AREA_OFFSET
.checked_add(self.tlv_len as usize)
.and_then(|v| v.checked_add(HEADER_MAC_LEN))
.ok_or(Error::MalformedHeader { reason: "tlv_len overflows header_len" })?;
if (self.header_len as usize) != expect_len {
return Err(Error::MalformedHeader {
reason: "header_len does not match 96 + 384 + tlv_len + 32",
});
}
let n = self.n_chunks();
if n > u64::from(u32::MAX) {
return Err(Error::MalformedHeader {
reason: "chunk count exceeds u32; nonce space would be exhausted",
});
}
let compressed_flag = self.flags & flags::COMPRESSED != 0;
if compressed_flag != (self.compress_id == COMPRESS_ZSTD) {
return Err(Error::MalformedHeader {
reason: "COMPRESSED flag contradicts compress_id",
});
}
Ok(())
}
pub fn to_bytes(&self) -> Result<[u8; FIXED_HEADER_LEN]> {
self.validate()?;
let mut w = Writer::with_capacity(FIXED_HEADER_LEN);
w.bytes(&MAGIC_FILE)
.u16le(self.version_major)
.u16le(self.version_minor)
.u32le(self.header_len)
.bytes(&self.file_uuid)
.bytes(&self.vault_salt)
.u32le(self.flags)
.u8(self.cipher_id as u8)
.u8(self.kdf_id)
.u8(self.compress_id)
.u8(self.slot_count)
.u32le(self.argon2_m_kib)
.u32le(self.argon2_t)
.u32le(self.argon2_p)
.u32le(self.chunk_size)
.u64le(self.plaintext_size)
.bytes(&self.base_nonce)
.u8(self.chunk_version)
.u32le(self.tlv_len)
.u32le(0);
let v = w.into_vec();
let mut out = [0u8; FIXED_HEADER_LEN];
let src = v.get(..FIXED_HEADER_LEN).ok_or(Error::MalformedHeader {
reason: "serialized fixed header shorter than 96 bytes",
})?;
out.copy_from_slice(src);
Ok(out)
}
#[must_use]
pub const fn n_chunks(&self) -> u64 {
let cs = self.chunk_size as u64;
if cs == 0 {
return 1;
}
let n = self.plaintext_size.div_ceil(cs);
if n == 0 { 1 } else { n }
}
#[must_use]
pub const fn has_flag(&self, flag: u32) -> bool {
self.flags & flag != 0
}
#[must_use]
pub const fn argon2_params(&self) -> crate::crypto::Argon2Params {
crate::crypto::Argon2Params {
m_kib: self.argon2_m_kib,
t: self.argon2_t,
p: self.argon2_p,
}
}
pub fn chunk_ct_range(&self, index: u64) -> Result<(u64, u64)> {
if self.has_flag(flags::COMPRESSED) {
return Err(Error::MalformedHeader {
reason: "chunk_ct_range is invalid for compressed payloads; use the index table",
});
}
let total = self.n_chunks();
if index >= total {
return Err(Error::ChunkOutOfRange { index, total });
}
let cs = u64::from(self.chunk_size);
let tag = crate::crypto::TAG_LEN as u64;
let stride = cs
.checked_add(tag)
.ok_or(Error::MalformedHeader { reason: "chunk stride overflow" })?;
let ct_offset = u64::from(self.header_len)
.checked_add(index.checked_mul(stride).ok_or(Error::MalformedHeader {
reason: "chunk offset overflow",
})?)
.ok_or(Error::MalformedHeader { reason: "chunk offset overflow" })?;
let consumed = index
.checked_mul(cs)
.ok_or(Error::MalformedHeader { reason: "chunk offset overflow" })?;
let plain_len = self.plaintext_size.saturating_sub(consumed).min(cs);
let ct_len = plain_len
.checked_add(tag)
.ok_or(Error::MalformedHeader { reason: "chunk length overflow" })?;
Ok((ct_offset, ct_len))
}
#[must_use]
pub const fn chunk_index_of(&self, plain_offset: u64) -> u64 {
match plain_offset.checked_div(self.chunk_size as u64) {
Some(v) => v,
None => 0,
}
}
}
pub const fn validate_chunk_size(chunk_size: u32) -> Result<()> {
if chunk_size < MIN_CHUNK_SIZE || chunk_size > MAX_CHUNK_SIZE {
return Err(Error::InvalidChunkSize {
got: chunk_size,
min: MIN_CHUNK_SIZE,
max: MAX_CHUNK_SIZE,
});
}
Ok(())
}