Skip to main content

omy_core/
tlv.rs

1//! TLV 扩展区的编解码。
2//!
3//! 编码:`type u16le | flags u16le | length u32le | value`(规范 §4.1)。
4//!
5//! # CRITICAL 位是格式可扩展性的关键
6//!
7//! 机制类似 PNG chunk 的大小写位:未知但标记 CRITICAL 的条目**必须**导致拒绝打开,
8//! 因为它意味着「不理解此字段就无法正确解读文件」。未知且非 CRITICAL 的条目可安全忽略,
9//! 且在重写文件时应原样保留。
10
11use crate::crypto::{CipherId, Fek, ZERO_NONCE};
12use crate::error::{Error, Result};
13use crate::header::{FILENAME_BUCKET, parse_limits};
14use crate::util::{Cursor, Writer};
15
16/// TLV 条目头长度(type 2 + flags 2 + length 4)。
17pub const TLV_HEADER_LEN: usize = 8;
18
19/// 压缩索引单条记录长度(u64 offset + u32 ct_len + u32 plain_len)。
20pub const INDEX_ENTRY_LEN: usize = 16;
21
22/// 已分配的 TLV 类型(规范 §4.4)。
23pub mod types {
24    /// 加密的原文件名。CRITICAL + ENCRYPTED。
25    pub const FILENAME: u16 = 0x0001;
26    /// 明文后缀(不含点)。
27    pub const PLAIN_EXT: u16 = 0x0002;
28    /// 加密缩略图。
29    pub const THUMBNAIL: u16 = 0x0003;
30    /// 媒体元信息 JSON。
31    pub const MEDIA_META: u16 = 0x0004;
32    /// 压缩块索引表。CRITICAL + ENCRYPTED。
33    pub const COMPRESSION_INDEX: u16 = 0x0005;
34    /// MP4 moov box 副本。
35    pub const MOOV_CACHE: u16 = 0x0006;
36    /// 分片信息。CRITICAL。
37    pub const SHARD_INFO: u16 = 0x0007;
38    /// 原始文件元数据 JSON。
39    pub const ORIGINAL_META: u16 = 0x0008;
40    /// 原始明文的 BLAKE2b-256。
41    pub const CONTENT_HASH: u16 = 0x0009;
42    /// zstd 训练字典。
43    pub const ZSTD_DICT: u16 = 0x000A;
44    /// portable slot 的独立盐与参数。
45    pub const PORTABLE_SLOT: u16 = 0x000B;
46    /// 字幕数据。
47    pub const SUBTITLES: u16 = 0x000C;
48    /// 转码参数与原文件哈希。
49    pub const TRANSCODE_INFO: u16 = 0x000D;
50    /// 容器模式的目录索引。CRITICAL + ENCRYPTED。
51    pub const FOLDER_INDEX: u16 = 0x000E;
52    /// 伪装模式的真实 header 偏移。
53    pub const DISGUISE_INFO: u16 = 0x000F;
54    /// 槽位目录(可管理模式)。CRITICAL + ENCRYPTED。
55    ///
56    /// CRITICAL 是必须的:不认识此 TLV 的实现必须拒绝打开,否则老版本会
57    /// 把可管理模式的文件当普通文件改密码,槽位目录与实际 slot 区就此
58    /// 对不上——而这种损坏不会立刻显现,等到用户依赖目录去删某个协作者
59    /// 时才暴露,那时已无法判断哪份记录是对的。
60    ///
61    /// ENCRYPTED 同样必须:明文目录等于把「谁能打开这个文件」白送给任何
62    /// 拿到文件的人。
63    pub const SLOT_DIRECTORY: u16 = 0x0010;
64
65    /// 私有类型区间下界。官方实现必须忽略此区间的未知类型。
66    pub const PRIVATE_RANGE_START: u16 = 0x8000;
67
68    /// 判断类型是否为本实现已知。
69    #[must_use]
70    pub const fn is_known(t: u16) -> bool {
71        matches!(
72            t,
73            FILENAME
74                | PLAIN_EXT
75                | THUMBNAIL
76                | MEDIA_META
77                | COMPRESSION_INDEX
78                | MOOV_CACHE
79                | SHARD_INFO
80                | ORIGINAL_META
81                | CONTENT_HASH
82                | ZSTD_DICT
83                | PORTABLE_SLOT
84                | SUBTITLES
85                | TRANSCODE_INFO
86                | FOLDER_INDEX
87                | DISGUISE_INFO
88                | SLOT_DIRECTORY
89        )
90    }
91}
92
93/// TLV flags 位(规范 §4.2)。
94pub mod tlv_flags {
95    /// 不认识此类型时必须拒绝打开文件。
96    pub const CRITICAL: u16 = 1 << 0;
97    /// value 是密文,需用 FEK 派生密钥解密。
98    pub const ENCRYPTED: u16 = 1 << 1;
99}
100
101/// 单个 TLV 条目。
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct TlvEntry {
104    /// 类型号。
105    pub tlv_type: u16,
106    /// 标志位。
107    pub flags: u16,
108    /// 原始 value(若置 ENCRYPTED 则为密文)。
109    pub value: Vec<u8>,
110}
111
112impl TlvEntry {
113    /// 构造条目。
114    #[must_use]
115    pub const fn new(tlv_type: u16, flags: u16, value: Vec<u8>) -> Self {
116        Self { tlv_type, flags, value }
117    }
118
119    /// 是否为 CRITICAL。
120    #[must_use]
121    pub const fn is_critical(&self) -> bool {
122        self.flags & tlv_flags::CRITICAL != 0
123    }
124
125    /// 是否为加密条目。
126    #[must_use]
127    pub const fn is_encrypted(&self) -> bool {
128        self.flags & tlv_flags::ENCRYPTED != 0
129    }
130
131    /// 编码后占用的总字节数。
132    #[must_use]
133    pub const fn encoded_len(&self) -> usize {
134        TLV_HEADER_LEN.saturating_add(self.value.len())
135    }
136}
137
138/// TLV 区的集合。保留原始顺序,以便重写文件时保持字节稳定。
139#[derive(Debug, Clone, Default, PartialEq, Eq)]
140pub struct TlvSet {
141    entries: Vec<TlvEntry>,
142}
143
144impl TlvSet {
145    /// 空集合。
146    #[must_use]
147    pub const fn new() -> Self {
148        Self { entries: Vec::new() }
149    }
150
151    /// 追加条目。
152    pub fn push(&mut self, e: TlvEntry) -> &mut Self {
153        self.entries.push(e);
154        self
155    }
156
157    /// 全部条目。
158    #[must_use]
159    pub fn entries(&self) -> &[TlvEntry] {
160        &self.entries
161    }
162
163    /// 条目数量。
164    #[must_use]
165    pub fn len(&self) -> usize {
166        self.entries.len()
167    }
168
169    /// 是否为空。
170    #[must_use]
171    pub fn is_empty(&self) -> bool {
172        self.entries.is_empty()
173    }
174
175    /// 按类型查找第一个条目。
176    #[must_use]
177    pub fn find(&self, tlv_type: u16) -> Option<&TlvEntry> {
178        self.entries.iter().find(|e| e.tlv_type == tlv_type)
179    }
180
181    /// 编码后的总字节数。
182    #[must_use]
183    pub fn encoded_len(&self) -> usize {
184        self.entries.iter().map(TlvEntry::encoded_len).sum()
185    }
186
187    /// 解析 TLV 区。
188    ///
189    /// 只做结构解析,不检查未知 CRITICAL 类型——那需要在 MAC 验证通过之后再做,
190    /// 见 [`TlvSet::check_critical`]。
191    ///
192    /// 条目数受 [`parse_limits::MAX_TLV_ENTRIES`] 限制,防止大量零长度条目耗尽内存。
193    ///
194    /// # Errors
195    ///
196    /// - [`Error::Truncated`]:长度字段越界或数据截断
197    /// - [`Error::MalformedTlv`]:条目数超过上限
198    pub fn parse(data: &[u8]) -> Result<Self> {
199        let mut c = Cursor::new(data, "tlv area");
200        let mut entries = Vec::new();
201        while c.remaining() > 0 {
202            if entries.len() >= parse_limits::MAX_TLV_ENTRIES {
203                return Err(Error::MalformedTlv {
204                    tlv_type: 0,
205                    reason: "too many TLV entries",
206                });
207            }
208            if c.remaining() < TLV_HEADER_LEN {
209                return Err(Error::Truncated {
210                    context: "tlv entry header",
211                    need: TLV_HEADER_LEN,
212                    got: c.remaining(),
213                });
214            }
215            let tlv_type = c.u16le()?;
216            let flags = c.u16le()?;
217            let length = c.u32le()? as usize;
218            // take() 会做边界检查,因此这里无需额外校验 length 上界:
219            // 声明超长的条目会因数据不足而返回 Truncated
220            let value = c.take(length)?.to_vec();
221            entries.push(TlvEntry { tlv_type, flags, value });
222        }
223        Ok(Self { entries })
224    }
225
226    /// 检查是否存在未知且 CRITICAL 的条目。
227    ///
228    /// 规范 §9 步骤 7 要求此时拒绝打开文件。私有区间(≥ 0x8000)的未知类型即使标记
229    /// CRITICAL 也应忽略,因为那是第三方实现的自用字段。
230    ///
231    /// # Errors
232    ///
233    /// 存在未知 CRITICAL 条目时返回 [`Error::UnknownCriticalTlv`]。
234    pub fn check_critical(&self) -> Result<()> {
235        for e in &self.entries {
236            if e.is_critical()
237                && !types::is_known(e.tlv_type)
238                && e.tlv_type < types::PRIVATE_RANGE_START
239            {
240                return Err(Error::UnknownCriticalTlv { tlv_type: e.tlv_type });
241            }
242        }
243        Ok(())
244    }
245
246    /// 序列化。
247    #[must_use]
248    pub fn to_bytes(&self) -> Vec<u8> {
249        let mut w = Writer::with_capacity(self.encoded_len());
250        for e in &self.entries {
251            // length 字段是 u32,超长条目在构造时就应被拒绝;这里用饱和转换兜底
252            let len = u32::try_from(e.value.len()).unwrap_or(u32::MAX);
253            w.u16le(e.tlv_type).u16le(e.flags).u32le(len).bytes(&e.value);
254        }
255        w.into_vec()
256    }
257
258    /// 解密某个加密条目的 value。
259    ///
260    /// # Errors
261    ///
262    /// - [`Error::MissingTlv`]:条目不存在
263    /// - [`Error::MalformedTlv`]:条目未标记 ENCRYPTED,或解密认证失败
264    pub fn decrypt_value(&self, tlv_type: u16, fek: &Fek, cipher: CipherId) -> Result<Vec<u8>> {
265        let e = self.find(tlv_type).ok_or(Error::MissingTlv { tlv_type })?;
266        if !e.is_encrypted() {
267            return Err(Error::MalformedTlv {
268                tlv_type,
269                reason: "entry is not marked ENCRYPTED",
270            });
271        }
272        let key = fek.derive_tlv_key(tlv_type);
273        cipher
274            .decrypt(&key, &ZERO_NONCE, &e.value, &[])?
275            .ok_or(Error::MalformedTlv { tlv_type, reason: "AEAD authentication failed" })
276    }
277}
278
279/// 构造一个加密 TLV 条目。
280///
281/// # Errors
282///
283/// AEAD 加密失败时返回错误。
284pub fn encrypt_entry(
285    tlv_type: u16,
286    extra_flags: u16,
287    plaintext: &[u8],
288    fek: &Fek,
289    cipher: CipherId,
290) -> Result<TlvEntry> {
291    let key = fek.derive_tlv_key(tlv_type);
292    let ct = cipher.encrypt(&key, &ZERO_NONCE, plaintext, &[])?;
293    Ok(TlvEntry::new(tlv_type, extra_flags | tlv_flags::ENCRYPTED, ct))
294}
295
296/// 按 64 字节桶填充文件名(规范 §4.5)。
297///
298/// ```text
299/// need   = 2 + len(name_utf8)
300/// bucket = ceil(need / 64) * 64
301/// padded = u16le(真实长度) || name_utf8 || 0x00 × (bucket - need)
302/// ```
303///
304/// # 实现陷阱
305///
306/// 桶大小必须基于 `2 + 名字长度`,**不能**只基于名字长度。若写成
307/// `(len / 64 + 1) * 64`,当 `len = 63` 时 `need = 65` 而桶算成 64,
308/// 填充量为负、`padded` 越出桶边界,该长度的文件名反而暴露自己的长度特征。
309/// 参考实现在交叉验证中命中过这个 bug。
310///
311/// # Errors
312///
313/// 文件名字节长度超过 `u16::MAX` 时返回 [`Error::MalformedTlv`]。
314pub fn pad_filename(name: &str) -> Result<Vec<u8>> {
315    let bytes = name.as_bytes();
316    let real_len = u16::try_from(bytes.len()).map_err(|_| Error::MalformedTlv {
317        tlv_type: types::FILENAME,
318        reason: "filename longer than 65535 bytes",
319    })?;
320
321    // 关键:need 含 2 字节长度前缀
322    let need = bytes.len().saturating_add(2);
323    let bucket = need.div_ceil(FILENAME_BUCKET).saturating_mul(FILENAME_BUCKET);
324    debug_assert!(bucket >= need, "bucket 必须不小于 need,否则填充量为负");
325
326    let mut w = Writer::with_capacity(bucket);
327    w.u16le(real_len).bytes(bytes).zeros(bucket.saturating_sub(need));
328    let out = w.into_vec();
329    debug_assert_eq!(out.len(), bucket, "padded 长度必须恰好等于桶大小");
330    Ok(out)
331}
332
333/// 从填充后的字节还原文件名。
334///
335/// # Errors
336///
337/// - [`Error::MalformedTlv`]:数据过短、长度前缀越界,或内容不是合法 UTF-8
338pub fn unpad_filename(padded: &[u8]) -> Result<String> {
339    let mut c = Cursor::new(padded, "filename tlv");
340    let real_len = c.u16le().map_err(|_| Error::MalformedTlv {
341        tlv_type: types::FILENAME,
342        reason: "missing length prefix",
343    })? as usize;
344    let name = c.take(real_len).map_err(|_| Error::MalformedTlv {
345        tlv_type: types::FILENAME,
346        reason: "declared length exceeds padded data",
347    })?;
348    String::from_utf8(name.to_vec()).map_err(|_| Error::MalformedTlv {
349        tlv_type: types::FILENAME,
350        reason: "filename is not valid UTF-8",
351    })
352}
353
354/// 压缩索引表的一条记录(规范 §6.2,每条 16 字节)。
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub struct CompressionIndexEntry {
357    /// 相对载荷起始的密文偏移。
358    pub ct_offset: u64,
359    /// 该块密文长度(含 tag)。
360    pub ct_len: u32,
361    /// 该块解压后的明文长度。
362    pub plain_len: u32,
363}
364
365/// 编码压缩索引表。
366///
367/// ```text
368/// u32le entry_count
369/// repeat: u64le ct_offset | u32le ct_len | u32le plain_len
370/// ```
371#[must_use]
372pub fn encode_compression_index(entries: &[CompressionIndexEntry]) -> Vec<u8> {
373    let count = u32::try_from(entries.len()).unwrap_or(u32::MAX);
374    let mut w = Writer::with_capacity(
375        4usize.saturating_add(entries.len().saturating_mul(INDEX_ENTRY_LEN)),
376    );
377    w.u32le(count);
378    for e in entries {
379        w.u64le(e.ct_offset).u32le(e.ct_len).u32le(e.plain_len);
380    }
381    w.into_vec()
382}
383
384/// 解码压缩索引表。
385///
386/// # 分配安全
387///
388/// `entry_count` 来自不可信输入,因此在 `Vec::with_capacity` **之前**先校验上界,
389/// 再校验它与实际数据长度严格相符。否则声明 `entry_count = 0xFFFFFFFF` 的
390/// 4 字节数据就能触发 64 GiB 的预分配。
391///
392/// # Errors
393///
394/// 条目数超限、数据截断或条目数与实际长度不符时返回 [`Error::MalformedTlv`]。
395pub fn decode_compression_index(data: &[u8]) -> Result<Vec<CompressionIndexEntry>> {
396    let mut c = Cursor::new(data, "compression index");
397    let count_u32 = c.u32le().map_err(|_| Error::MalformedTlv {
398        tlv_type: types::COMPRESSION_INDEX,
399        reason: "missing entry_count",
400    })?;
401
402    // 先查上界,再谈分配
403    if count_u32 > parse_limits::MAX_INDEX_ENTRIES {
404        return Err(Error::MalformedTlv {
405            tlv_type: types::COMPRESSION_INDEX,
406            reason: "entry_count exceeds the parse limit",
407        });
408    }
409    let count = count_u32 as usize;
410
411    let need = count.checked_mul(INDEX_ENTRY_LEN).ok_or(Error::MalformedTlv {
412        tlv_type: types::COMPRESSION_INDEX,
413        reason: "entry_count overflows",
414    })?;
415    // 严格相等:多余或不足都说明数据不可信
416    if c.remaining() != need {
417        return Err(Error::MalformedTlv {
418            tlv_type: types::COMPRESSION_INDEX,
419            reason: "entry_count does not match data length",
420        });
421    }
422
423    let mut out = Vec::with_capacity(count);
424    for _ in 0..count {
425        let ct_offset = c.u64le()?;
426        let ct_len = c.u32le()?;
427        let plain_len = c.u32le()?;
428        out.push(CompressionIndexEntry { ct_offset, ct_len, plain_len });
429    }
430    Ok(out)
431}