From b3efaee364cde15c9d1b6137da416e970f5a7a9d Mon Sep 17 00:00:00 2001 From: freeof123 Date: Fri, 22 May 2026 17:44:04 +0800 Subject: [PATCH 1/3] init --- doc/sealed-format-standard-zh.tex | 1494 +++++++++++++++++++++++++++++ 1 file changed, 1494 insertions(+) create mode 100644 doc/sealed-format-standard-zh.tex diff --git a/doc/sealed-format-standard-zh.tex b/doc/sealed-format-standard-zh.tex new file mode 100644 index 0000000..6db48fb --- /dev/null +++ b/doc/sealed-format-standard-zh.tex @@ -0,0 +1,1494 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[UTF8,fontset=none]{ctex} +\usepackage[margin=2.5cm]{geometry} +\usepackage{hyperref} +\usepackage{longtable} +\usepackage{booktabs} +\usepackage{array} +\usepackage{enumitem} +\usepackage{titlesec} +\usepackage{graphicx} + +\title{Meta-Encryptor Sealed 文件格式标准(中文草案)} +\author{YeeZ Tech} +\date{\today} + +\setlist[itemize]{leftmargin=2em} +\setlist[enumerate]{leftmargin=2em} +\newcommand{\ttf}[1]{\texttt{\detokenize{#1}}} +% 排版约定:字段名用 \ttf{field_name}(勿在参数内写 \_);算法变量用 $H_0$ 等; +% 多项并列写在同一 enumerate 项时,用嵌套 itemize 拆开,勿用长逗号串。 +\newcommand{\rollhash}{\mbox{滚动摘要}} +\newcommand{\secref}[1]{第~\ref{#1}~节} +\XeTeXlinebreaklocale "zh" +\XeTeXlinebreakskip = 0pt plus 0.15em minus 0.05em +\setCJKmainfont{PingFang SC} +\setCJKsansfont{PingFang SC} +\setCJKmonofont{PingFang SC} + +% 减轻正文过长行导致的 Overfull \hbox(中文技术长句、交叉引用) +\setlength{\emergencystretch}{2.5em} +\setlength{\tolerance}{2000} +\hbadness=10000 +\sloppy + +\begin{document} +\maketitle +\section*{文档元数据} +\begin{longtable}{>{\raggedright\arraybackslash}p{3.2cm} >{\raggedright\arraybackslash}p{10.0cm}} + \toprule + 字段 & 内容 \\ + \midrule + \endhead + 文档标识 & \ttf{YEEZ-SEALED-FORMAT-ZH} \\ + 文档状态 & Draft(草案) \\ + 文档版本 & v0.1.0 \\ + 发布日期 & \today \\ + 语言 & 中文(规范性文本) \\ + 适用范围 & Meta-Encryptor 生态下 \ttf{.sealed} 文件读写实现 \\ + 解释优先级 & 本标准正文优先于示例代码;示例仅作实现参考 \\ + 变更记录策略 & 仅在“对外发布版本”时更新变更记录;日常本地迭代与排版修订不单独记入版本历史 \\ + \bottomrule +\end{longtable} + +\section*{发布级变更记录} +\begin{longtable}{>{\raggedright\arraybackslash}p{2.2cm} >{\raggedright\arraybackslash}p{2.8cm} >{\raggedright\arraybackslash}p{6.8cm}} + \toprule + 版本 & 日期 & 说明 \\ + \midrule + \endhead + \ttf{v0.1.0} & \today & 首个公开草案:定义文件结构、密码学规范、封装/解封流程、错误码与附录。 \\ + \bottomrule +\end{longtable} + +\tableofcontents +\newpage + +\section{引言} +\subsection{目的} +本标准用于定义 \texttt{.sealed} 文件格式的二进制结构、加密语义、封装与解封流程、兼容性规则与一致性要求,确保不同实现之间的互操作性与可验证性。 + +\subsection{范围} +本标准适用于使用 Meta-Encryptor 生态读写 \texttt{.sealed} 文件的实现,包括但不限于 Node.js 与浏览器环境实现。本文档不限定上层业务协议。 + +\subsection{读者对象} +本标准面向以下读者: +\begin{itemize} + \item 文件格式实现者; + \item 安全审计人员; + \item 平台集成与运维工程师; + \item 隐私计算数据处理链路设计人员。 +\end{itemize} + +\subsection{一致性语言} +本文档使用 RFC 2119 含义的以下术语: +\begin{itemize} + \item \textbf{必须(MUST)}:对文件布局、字段取值、可验证一致性及失败语义的绝对要求; + \item \textbf{应当(SHOULD)}:推荐要求,除非有充分理由; + \item \textbf{可以(MAY)}:可选行为。 +\end{itemize} + +\textbf{适用范围:} 第~\ref{sec:format-chapter} 章、第~\ref{sec:crypto-chapter} 章,以及各章中对上述内容的直接引用,使用 MUST/SHOULD/MAY 表述\textbf{格式与校验}义务。 +第~\ref{sec:seal-chapter}、\ref{sec:unseal-chapter} 章给出\textbf{参考封装/解封流程},描述 Meta-Encryptor 生态常用的实现顺序;除明确援引格式或校验条款外,流程步骤本身不以 MUST 约束具体算法或数据结构(如读指针、队列)。 + +\section{术语与记号} +\subsection{术语} +\begin{itemize} + \item \textbf{Item}:\texttt{.sealed} 内容区中最小加密单元; + \item \textbf{Block}:由若干 Item 组成的逻辑分组,并在元信息区有对应索引项; + \item \textbf{Header}:文件尾部固定 64 字节头结构; + \item \textbf{Block Info}:描述单个 Block 的 32 字节索引结构;\textbf{Block Infos 区}为由若干条 Block Info 顺序拼接而成的索引区域; + \item \textbf{Data Hash}:Header 字段 \ttf{data_hash} 所承载的摘要;对全部规范化输入记录(\ttf{nt_input})按封装顺序滚动计算,见第~\ref{sec:rolling-hash} 节; + \item \textbf{业务载荷}:宿主提交的一段载荷字节串,记为 \ttf{payload},长度为 \ttf{L};解封侧按封装顺序输出的字节串为 \ttf{payload}(见第~\ref{sec:input-normalization} 节); + \item \textbf{规范化输入记录}:将一条业务载荷按固定规则编码后的\textbf{明文侧}容器字节串(常记 \ttf{nt_input}),含 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload};位于 Item 加密之前,是第~\ref{sec:rolling-hash} 节滚动摘要与批包拼接的基本单元,格式见第~\ref{sec:input-normalization} 节; + \item \textbf{批包}:将若干条规范化输入记录按固定容器格式首尾拼接后的字节串,记为 \ttf{B};一条批包经加密后对应内容区中的一条 Item。容器字节布局见第~\ref{sec:batch-package} 节; + \item \textbf{批刷写阈值} \ttf{T_batch}:封装实现参数,判定何时将一批 \ttf{nt_input} 送入批包封装;规则见第~\ref{sec:batch-flush-threshold} 节;队列调度见第~\ref{sec:seal-total-algorithm} 节; + \item \textbf{Block 内 Item 数上限} \ttf{N_block}:封装实现参数,限定单个 Block 所含 Item 条数的上界;划分规则见第~\ref{sec:block-split} 节;与 Block Info 字段的对应见第~\ref{sec:block-meta-fields} 节。 +\end{itemize} + +\subsection{数据类型} +\begin{itemize} + \item \texttt{BYTE}:8 位无符号整数; + \item \texttt{UINT32}:32 位无符号整数; + \item \texttt{UINT64}:64 位无符号整数; + \item \texttt{BYTESTRING[n]}:长度为 \texttt{n} 的字节串; + \item \texttt{VARBYTES}:变长字节串(长度由外层字段定义)。 +\end{itemize} + +\subsection{字节序} +除非另有说明,多字节整数均使用\textbf{小端序(Little Endian)}。 + +\section{设计目标与安全模型} +\subsection{设计目标} +\begin{itemize} + \item 提供面向隐私计算链路的数据封装格式; + \item 支持流式封装与流式解封; + \item 支持断点续传场景下的一致性恢复; + \item 支持跨运行时(Node.js / Browser)一致解封。 +\end{itemize} + +\subsection{安全属性} +\begin{itemize} + \item 机密性:明文对未授权方不可见; + \item 完整性:篡改应可被检测; + \item 认证性:每个 Item 具备认证加密保护。 +\end{itemize} + +\subsection{非目标} +\begin{itemize} + \item 本标准不定义业务语义级字段; + \item 本标准不定义应用层权限模型; + \item 本标准不保证无索引情况下的记录级随机查询性能。 +\end{itemize} + +\section{文件格式总览} +\subsection{高层布局} +\texttt{.sealed} 文件采用“数据在前、元信息在后”的尾部头设计。文件按字节从前到后布局如下: +\begin{center} + \texttt{[Content Region][Block Infos][Header(64B)]} +\end{center} +\begin{itemize} + \item \ttf{Content Region}:密文内容区,由若干 Item 顺序拼接组成;Item 为内容区最小加密单元。内容区按块组织,每块由若干连续的 Item 组成,详见第~\ref{sec:content-region} 节; + \item \ttf{Block Infos}:块索引区,由若干 Block Info 顺序拼接而成;每条 Block Info 固定 32 字节,描述一个块在内容区内的索引,条数由 Header 给出,详见第~\ref{sec:block-infos} 节; + \item \ttf{Header}:固定 64 字节,位于文件末尾,由单个 Header 记录构成,包含魔数、版本、计数和摘要,详见第~\ref{sec:header-region} 节; +\end{itemize} +该布局允许写入端在不知道最终计数的情况下先持续写入内容,再在结束时一次性写入索引和 Header。 + +\subsection{读取模型}\label{sec:read-model} +由于 Header 位于尾部,标准定义两种等价读取模型: +\begin{itemize} + \item \textbf{尾读模型}:读取器先从文件末尾读取 64B Header(字段布局见第~\ref{sec:header-region} 节),再按 Header.\ttf{block_number} 读取 Block Infos 区,再读取内容区; + \item \textbf{逻辑前置模型}:中间层先尾读 Header,并在解封管道中将 Header 逻辑前置,再按顺序喂入内容区数据。 +\end{itemize} +两种模型 MUST 产出一致的解析结果。 + +\subsection{区域长度计算}\label{sec:region-length} +本小节使用的符号先定义如下: +\begin{itemize} + \item \ttf{block_number}:\ttf{Header} 结构中的字段,表示 Block Info 条目数量(类型为 \ttf{UINT64(LE)}); + \item \ttf{N}:\ttf{block_number} 的记号化表示,即 \ttf{N = block_number}; + \item \ttf{file_size}:单个 \texttt{.sealed} 文件的总字节数;由打开文件、对象大小或传输层长度字段等在\textbf{读取 Header 之前}由实现获知,不得通过解析 Header 反推。 + \item \ttf{header_size}:Header 区总长度(单位:字节); + \item \ttf{block_infos_size}:Block Infos 区总长度(单位:字节); + \item \ttf{content_size}:Content Region 区总长度(单位:字节)。 +\end{itemize} + +给定文件总长度 \ttf{file_size},则: +\begin{itemize} + \item \ttf{header_size = 64} + \item \ttf{block_infos_size = 32 * N} + \item \ttf{content_size} $=$ \ttf{file_size} $-$ \ttf{header_size} $-$ \ttf{block_infos_size} +\end{itemize} +实现 MUST 校验 \ttf{content_size > 0}。若不满足,MUST 视为格式错误。 + +\section{二进制格式规范}\label{sec:format-chapter} +\subsection{规范约定} +\begin{itemize} + \item 除特别说明外,整数类型均为无符号; + \item 除特别说明外,所有多字节整数均为小端序(LE); + \item 偏移均以“所在结构体起始地址”为基准; + \item 本章字段名使用实现同名风格(如下划线命名)以便互操作。 +\end{itemize} + +\subsection{Header 区}\label{sec:header-region} +Header 区位于文件末尾,长度固定为 64 字节。本区由单个 Header 记录构成,保存格式识别、版本、块与 Item 计数及数据摘要等元信息。 + +\subsubsection{Header 记录(64 字节)} +Header 记录占用 64 字节,字段布局如下: +\begin{longtable}{>{\raggedright\arraybackslash}p{3.2cm} >{\raggedright\arraybackslash}p{1.3cm} >{\raggedright\arraybackslash}p{1.3cm} >{\raggedright\arraybackslash}p{6.8cm}} + \toprule + 字段名 & 偏移 & 长度 & 说明 \\ + \midrule + \endhead + \ttf{magic_number} & 0 & 8 & 文件魔数(BYTESTRING[8]) \\ + \ttf{version_number} & 8 & 8 & 格式版本(UINT64, LE) \\ + \ttf{block_number} & 16 & 8 & Block 数量(UINT64, LE) \\ + \ttf{item_number} & 24 & 8 & Item 数量(UINT64, LE) \\ + \ttf{data_hash} & 32 & 32 & 数据摘要(BYTESTRING[32]) \\ + \bottomrule +\end{longtable} +\textbf{字段语义:} +\begin{itemize} + \item \ttf{magic_number}:文件类型标识(\ttf{BYTESTRING[8]}); + \item \ttf{version_number}:格式版本; + \item \ttf{block_number}:Block Info 条目数量;维护规则见第~\ref{sec:block-meta} 节; + \item \ttf{item_number}:内容区 Item 总数量(见第~\ref{sec:content-region} 节);每写出一条 Item 递增,见第~\ref{sec:item-generation} 节; + \item \ttf{data_hash}:对全部 \ttf{nt_input}(按封装顺序)滚动计算得到的最终摘要(\ttf{BYTESTRING[32]})。 + 记录格式见第~\ref{sec:input-normalization} 节; + 算法与写入见第~\ref{sec:rolling-hash}、\ref{sec:seal-finalize} 节; + 解封校验见第~\ref{sec:integrity-check} 节。 +\end{itemize} + +\subsubsection{Header 识别与版本兼容规则}\label{sec:header-compat} +本小节统一约束 \ttf{magic_number} 与 \ttf{version_number} 的识别、校验与版本演进策略: +\begin{itemize} + \item \textbf{文件}:指单个 \ttf{.sealed} 文件实例; + \item \textbf{实现}:指遵循本标准的读写器(Writer/Reader); + \item \textbf{未来版本}:指 \ttf{version_number} 高于当前版本的格式版本。 +\end{itemize} +\begin{itemize} + \item 每个 \ttf{.sealed} 文件 MUST 在 Header 中携带 \ttf{version_number}; + \item \textbf{文件识别}:\ttf{magic_number} MUST 等于约定魔数; + \item \textbf{版本识别}:\ttf{version_number} MUST 位于实现声明的支持范围内; + \item 读取实现 MUST 在进入内容解析前完成上述检查; + \item 任一检查失败时,读取器 MUST 拒绝继续解析。 + \item 新版本在可行时 SHOULD 保持向后兼容;若无法兼容,MUST 提供迁移或拒绝策略说明。 +\end{itemize} + +\subsubsection{当前版本 Header 字段约定值} +当前版本(\ttf{version_number = 2})的 Header 字段约定值如下: +\begin{itemize} + \item \ttf{magic_number = 1fe2ef7f3ed18847} +\end{itemize} +若后续版本调整上述字段取值或字段语义,\ttf{version_number} MUST 递增并提供兼容说明。 + +\subsection{Block Infos 区}\label{sec:block-infos} +Block Infos 区位于 Content Region 与尾部 Header 之间,总长度 \ttf{block_infos_size} +及其在文件中的起止位置由第~\ref{sec:region-length} 节给出。 + +本区由 \ttf{block_number} 条 Block Info 顺序拼接而成。本文所称单条 \textbf{Block Info},即长度为 32 字节的索引记录;\textbf{Block Infos 区}即上述全部 Block Info 按序拼接形成的区域。本区总长度为 \ttf{32 * block_number}。第 \ttf{i} 条 Block Info(\ttf{i} 自 0 起)在本区内的字节偏移为 \ttf{32 * i}。 + +\subsubsection{Block Info 记录(每项 32 字节)} +每条 Block Info 对应 Content Region 中的一个 Block:在 Item 下标维度标出该 Block 覆盖的 Item 范围,在内容区字节偏移维度标出上述 Item 序列所占用的字节范围。两条范围彼此对应。字段布局如下: +\begin{longtable}{>{\raggedright\arraybackslash}p{3.4cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{6.8cm}} + \toprule + 字段名 & 偏移 & 长度 & 说明 \\ + \midrule + \endhead + \ttf{start_item_index} & 0 & 8 & 起始 Item 下标(含) \\ + \ttf{end_item_index} & 8 & 8 & 结束 Item 下标(不含) \\ + \ttf{start_file_pos} & 16 & 8 & 内容区内起始字节偏移(非源明文文件偏移) \\ + \ttf{end_file_pos} & 24 & 8 & 内容区内结束字节偏移(半开,非源明文文件偏移) \\ + \bottomrule +\end{longtable} +\textbf{字段语义:} +\begin{itemize} + \item \ttf{start_item_index}、\ttf{end_item_index}:该 Block 内 Item 的下标半开区间 \ttf{[start, end)},下标从 0 起计; + \item \ttf{start_file_pos}、\ttf{end_file_pos}:本 Block 在 \texttt{.sealed} 文件 Content Region 内占据的字节半开区间 \ttf{[start, end)},偏移以内容区首字节为 \texttt{0};\textbf{不}表示业务侧源文件内偏移(见第~\ref{sec:block-meta-fields} 节)。 +\end{itemize} +全体 Block Info 共同构成 Content Region 的块级索引: +\begin{itemize} + \item Item 下标维度:各 Block 的 \ttf{start_item_index}、\ttf{end_item_index} 以半开区间无空隙、无重叠地划分 + \ttf{[0, item_number)}; + \item 内容区字节维度:各 Block 的 \ttf{start_file_pos}、\ttf{end_file_pos} 以内容区起点为 0,同样无隙、无重叠地划分 \ttf{[0, content_size)}。 +\end{itemize} + +\subsection{Content Region 区}\label{sec:content-region} +Content Region 区位于文件最前部,在 Block Infos 区与 Header 区之前;总长度 \ttf{content_size} +及其在文件中的起止位置由第~\ref{sec:region-length} 节给出。 + +本区由 \ttf{item_number} 条 Item 顺序拼接而成。本文所称单条 \textbf{Item},即内容区最小加密单元;\textbf{Content Region 区}即上述全部 Item 按序拼接形成的区域。内容区按块组织,每块由若干连续的 Item 组成。各条 Item 长度随密文载荷变化,Item 之间无固定步长。 + +\subsubsection{Item 记录}\label{sec:item-record} +每条 Item 由前置长度字段与密文载荷顺序组成,按字节从前到后布局如下: +\begin{center} + \texttt{[item\_size (8B)][cipher\_payload (item\_size B)]} +\end{center} +完整展开(含 \texttt{cipher\_payload} 内部字段)为: +\begin{center} + \small + \texttt{[item\_size (8B)][encrypted\_data (变长)][iv (12B)][ephemeral\_public\_key (64B)][gcm\_tag (16B)]} +\end{center} +字段说明: +\begin{enumerate} + \item \texttt{item\_size}:8 字节无符号整数(LE); + \item \texttt{cipher\_payload}:长度为 \texttt{item\_size} 的字节串。 +\end{enumerate} +\textbf{注意:} \ttf{item_size} 仅描述当前 Item 的密文载荷长度,不包含其自身 8 字节长度字段;其取值范围由 \ttf{UINT64(LE)} 确定,\ttf{cipher_payload} 的字节长度 MUST 等于 \ttf{item_size},且 MUST NOT 超过 \ttf{UINT64} 可表示的最大值。 + +\subsubsection{\texttt{cipher\_payload} 内部结构}\label{sec:cipher-payload-layout} +\ttf{cipher_payload} 长度为 \ttf{item_size},自其起始偏移 0 起按字节从前到后布局如下: +\begin{center} + \texttt{[encrypted\_data (变长)][iv (12B)][ephemeral\_public\_key (64B)][gcm\_tag (16B)]} +\end{center} +后三段长度在当前版本固定,\ttf{encrypted_data} 变长;四段顺序拼接,字段说明如下: +\begin{enumerate} + \item \texttt{encrypted\_data}(变长); + \item \texttt{iv}(12B); + \item \texttt{ephemeral\_public\_key}(64B); + \item \texttt{gcm\_tag}(16B)。 +\end{enumerate} +\textbf{字段语义:} +\begin{itemize} + \item \ttf{encrypted_data}:\ttf{AES-128-GCM} 密文;其明文为批包,即本条 Item 内全部规范化输入记录按固定格式拼接后、加密前的那段字节串,加解密参数见第~\ref{sec:crypto-chapter} 节; + \item \ttf{iv}:本条 Item 的 \ttf{AES-128-GCM} Nonce(常称 IV);供加解密双方标识本次独立 GCM 运算,避免同一密钥下因 Nonce 复用而产出相同密文。长度固定 12 字节,取值由安全随机源逐 Item 生成,见第~\ref{sec:randomness} 节; + \item \ttf{ephemeral_public_key}:本 Item 临时密钥对的公钥;采用椭圆曲线型号 \texttt{secp256k1} 的未压缩公钥,去掉首字节 \texttt{0x04} 后保留 \texttt{x||y},长度固定 64 字节; + \item \ttf{gcm_tag}:\ttf{AES-128-GCM} 认证标签;长度固定 16 字节,由 GCM 运算得到。 +\end{itemize} +\textbf{当前版本固定长度与偏移:} +\begin{itemize} + \item \ttf{encrypted_data_len = item_size - 12 - 64 - 16}; + \item \ttf{iv} 起始于 \ttf{encrypted_data_len},长度 12 字节; + \item \ttf{ephemeral_public_key} 起始于 \ttf{encrypted_data_len + 12},长度 64 字节; + \item \ttf{gcm_tag} 起始于 \ttf{encrypted_data_len + 76},长度 16 字节; + \item 若 \ttf{encrypted_data_len < 0},读取器 MUST 返回格式错误。 +\end{itemize} +上述 12、64、16 字节为当前版本规定的尾字段长度;\ttf{iv}、\ttf{ephemeral_public_key}、\ttf{gcm_tag} 与 \ttf{encrypted_data} 的字节值逐 Item 变化,除长度外无固定取值。Item 密文封装使用的前缀字节为 \texttt{0x02},见第~\ref{sec:ref-derive-params} 节。 + +\subsection{批包布局}\label{sec:batch-package} +术语「批包」及记号 \ttf{B} 见第 2 章。本小节规定 \ttf{B} 的容器字节布局(加密前明文侧结构,经第~\ref{sec:content-encryption} 节编码后再送入 GCM)。 + +\textbf{与 Item 的关系:} 内容区每条 Item 的 \ttf{cipher_payload} 由\textbf{一个}批包 \ttf{B} 加密得到(见第~\ref{sec:item-record}、\ref{sec:content-encryption} 节)。 + +\textbf{布局(当前版本):} +\begin{center} + \texttt{[package\_id (4B)][record\_count (8B)] * record} +\end{center} +其中每条 \texttt{record} 为: +\begin{center} + \texttt{[record\_len (8B)][nt\_input (record\_len B)]} +\end{center} +字段语义: +\begin{itemize} + \item \ttf{package_id}:\ttf{UINT32(LE)},当前版本 MUST 为 \texttt{0x82c4e8d8}; + \item \ttf{record_count}:\ttf{UINT64(LE)},批内规范化输入记录条数; + \item \ttf{record_len}:本条记录的 \ttf{nt_input} 总字节数(含 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload}); + \item 批内记录顺序 MUST 与封装侧接收业务载荷的顺序一致。 +\end{itemize} + +\textbf{构造与拆分:} +\begin{itemize} + \item \textbf{构造}:将若干条 \ttf{nt_input} 按上述布局首尾拼接为 \ttf{B}; + \item \textbf{拆分}:从 \ttf{B} 解析各字段并依次取出完整 \ttf{nt_input};\ttf{package_id}、长度字段与剩余字节不一致时 MUST 判为格式错误。 +\end{itemize} +实现行为 MUST 与本节布局一致;与参考实现源码符号的对照见附录~\ref{app:ref-impl-symbols}。 + +\subsection{约束与限制} +\begin{itemize} + \item Header 固定长度为 64 字节; + \item Block Info 固定长度为 32 字节; + \item Item 数量与 Block 数量 MUST 与实际内容一致; + \item 超出实现上限时 MUST 返回错误。 +\end{itemize} + +\subsection{规范性解析流程} +实现方在读取 \texttt{.sealed} 时 MUST 按以下顺序执行: +\begin{enumerate} + \item 从文件末尾读取 64 字节 Header; + \item 校验 \ttf{magic_number} 和 \ttf{version_number}; + \item 基于 \ttf{block_number} 计算并定位 Block Infos 区; + \item 校验 \ttf{content_size} 为正且区域不重叠; + \item 顺序解析内容区 Item:先读 \ttf{item_size},再读 \ttf{cipher_payload}; + \item 每个 Item 执行长度合法性检查和密码学认证检查; + \item 当解析 Item 数量达到 \ttf{item_number} 时结束并进入完整性校验阶段。 +\end{enumerate} +\subsection{规范性校验规则} +\begin{itemize} + \item \textbf{计数校验}:实际解析 Item 数 MUST 等于 \ttf{item_number}; + \item \textbf{区间校验}:最后一个 Block 的 \ttf{end_item_index} SHOULD 等于 \ttf{item_number}(Block Info 语义见第~\ref{sec:block-meta-fields} 节); + \item \textbf{边界校验}:任一 \ttf{start > end} 或越界偏移 MUST 判为错误; + \item \textbf{摘要校验}:实现 SHOULD 按第~\ref{sec:rolling-hash} 节重算摘要并比对 Header.\ttf{data_hash},不一致 MUST 报错(亦见第~\ref{sec:integrity-check} 节); + \item \textbf{版本校验}:未知版本 MAY 拒绝处理;若处理 MUST 在实现文档中声明兼容边界。 +\end{itemize} + +\section{密码学规范}\label{sec:crypto-chapter} +本章规定 \ttf{version_number = 2} 下 Item 密文载荷的密码学处理要求。 +第~\ref{sec:key-derivation}、\ref{sec:aes-gcm}、\ref{sec:content-encryption} 节按\textbf{模块}给出输入与输出;其余小节为概念、常量或约束说明。 +符号 \ttf{B}(批包)含义见第 2 章;\ttf{B} 的字节布局见第~\ref{sec:batch-package} 节。 + +\subsection{曲线与密钥}\label{sec:curve-keys} +本小节规定逐条 Item 协商对称密钥所需的曲线型号与密钥材料。 + +Item 的 \ttf{encrypted_data} 由第~\ref{sec:aes-gcm} 节规定的 \texttt{AES-128-GCM} +保护。对称密钥不由接收方长期公钥直接充当,而是经临时密钥对与 ECDH 得到共享秘密,再经第~\ref{sec:key-derivation} 节派生 +\ttf{session_key}。 + +\begin{itemize} + \item \textbf{椭圆曲线}:\texttt{secp256k1}; + \item \textbf{临时密钥对}:封装每条 Item 时 MUST 新生成(公钥写入 \ttf{ephemeral_public_key});不同 Item MUST 彼此独立; + \item \textbf{ECDH}:封装侧以「本条 Item 临时私钥 + 接收方长期公钥」运算;解封侧以「接收方长期私钥 + \ttf{ephemeral_public_key}」运算;双方在曲线与格式一致时得到同一曲线点,再经第~\ref{sec:key-derivation} 节派生 \ttf{session_key}; + \item \textbf{下游}:\ttf{session_key} 在第~\ref{sec:content-encryption}、\ref{sec:decrypt-crypto} 节\textbf{模块内部}使用,不对外作为独立调用接口。 +\end{itemize} + +\textbf{长期密钥与临时公钥格式:} +\begin{itemize} + \item \textbf{长期公钥、长期私钥}:\texttt{.sealed} 文件内 \textbf{不} 保存接收方的长期公钥或私钥。封装时,写入端 MUST 在应用层向加密模块提供接收方长期公钥;解封时,读取端 MUST 在应用层提供对应的长期私钥。当前参考实现中,二者均以十六进制字符串传入:私钥 32 字节(64 个十六进制字符),公钥 64 字节 \texttt{x||y}(128 个十六进制字符,未压缩公钥去掉首字节 \texttt{0x04});模块内部再解码为曲线点参与 ECDH。 + \item \ttf{ephemeral_public_key}:\textbf{写入} \texttt{.sealed} 文件,为每条 Item 的临时公钥;64 字节 \texttt{x||y},格式与长期公钥相同;参与 ECDH 时 MUST 在首部补回 \texttt{0x04}。 +\end{itemize} + +\subsection{密钥派生}\label{sec:key-derivation} +本小节规定\textbf{密钥派生}模块:将 ECDH 共享秘密转换为本条 Item 的 GCM 会话密钥 \ttf{session_key}。 + +\textbf{输入(来源):} +\begin{itemize} + \item \textbf{ECDH 所用密钥对}(运算规则见第~\ref{sec:curve-keys} 节):封装侧为「本条 Item 临时私钥 + 接收方长期公钥」;解封侧为「接收方长期私钥 + \ttf{cipher_payload} 中的 \ttf{ephemeral_public_key}」; + \item \ttf{cmac_key}、\ttf{derivation_buffer} 布局常量:见第~\ref{sec:ref-derive-params} 节。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item \ttf{session_key}:16 字节,供第~\ref{sec:aes-gcm}、\ref{sec:content-encryption}、\ref{sec:decrypt-crypto} 节使用。 +\end{itemize} + +\subsubsection{ECDH 共享秘密}\label{sec:ecdh-shared-secret} +ECDH(Elliptic Curve Diffie-Hellman)是一类椭圆曲线密钥协商方法,可参照 SEC 1 的 ECDH 原语或 NIST SP +800-56A 中的 ECC CDH 密钥协商术语理解:一方使用自己的私钥与对方公钥运算,双方在参数一致时得到同一曲线点。当前版本的 ECDH 在 +\texttt{secp256k1} 上完成,并将该曲线点按下述规则转换为 32 字节共享秘密 \ttf{shared_key}(与参考实现一致): +\begin{itemize} + \item 设 ECDH 结果为曲线点坐标 \texttt{(x, y)},各 32 字节; + \item 构造 33 字节缓冲区:首字节为 \texttt{0x02}(\texttt{y} 最低位为 0)或 \texttt{0x03}(\texttt{y} 最低位为 1),后接 \texttt{x}; + \item \ttf{shared_key = SHA256(上述 33 字节)}。 +\end{itemize} + +\subsubsection{CMAC 派生会话密钥}\label{sec:cmac-session-key} +CMAC(Cipher-based Message Authentication Code)是一类基于分组密码的消息认证码,可参照 NIST SP +800-38B 中的 CMAC 模式理解;AES-CMAC 的具体算法也可参照 RFC 4493。国密算法体系中可类比以 SM4 等分组密码为底层算法的 +CMAC 类用法,但当前版本固定使用 AES-CMAC,不引入 SM4 参数。 + +本标准将 \ttf{AES_CMAC(K, M)} 视为确定性函数:输入 16 字节密钥 \ttf{K} 与变长消息 \ttf{M},输出 16 字节认证标签。当前版本不直接使用该标签做外部认证,而是将 AES-CMAC 用作密钥派生步骤。会话密钥由两步 AES-CMAC(128 位密钥、128 位标签)派生: +\begin{enumerate} + \item \ttf{key_derive_key = AES_CMAC(cmac_key, shared_key)},输出 16 字节; + \item \ttf{session_key = AES_CMAC(key_derive_key, derivation_buffer)},输出 16 字节,即本条 Item 的 \texttt{AES-128-GCM} 会话密钥。 +\end{enumerate} + +\textbf{\ttf{derivation_buffer} 布局(当前版本,共 25 字节):} +\begin{center} + \small + \begin{tabular}{>{\raggedright\arraybackslash}p{1.8cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{8.8cm}} + \toprule + 偏移 & 长度 & 内容 \\ + \midrule + 0 & 1 & 固定值 \texttt{0x01} \\ + 1 & 21 & 域分离字符串 \texttt{tech.yeez.key.manager}(UTF-8) \\ + 22 & 1 & 固定值 \texttt{0x00} \\ + 23 & 1 & 固定值 \texttt{0x80} \\ + 24 & 1 & 固定值 \texttt{0x00} \\ + \bottomrule + \end{tabular} +\end{center} + +\subsection{参考派生参数(当前版本)}\label{sec:ref-derive-params} +为保证互操作,当前版本 (\ttf{version_number = 2}) 采用下列固定参数(字符串均为 UTF-8,\textbf{无}换行符、\textbf{无} BOM): +\begin{itemize} + \item \ttf{cmac_key}(16 字节): + \begin{itemize} + \item 字符串形式:\texttt{yeez.tech.stbox} 后接 1 字节 \texttt{0x00}(共 16 字节,\textbf{不}含额外换行或第二处 NUL); + \item 十六进制:\texttt{7965657a2e746563682e7374626f7800}。 + \end{itemize} + \item 域分离字符串(21 字节): + \begin{itemize} + \item 字符串形式:\texttt{tech.yeez.key.manager}(恰好 21 个 UTF-8 字节,\textbf{无}结尾 \texttt{0x00}、\textbf{无}换行); + \item 十六进制:\texttt{746563682e7965657a2e6b65792e6d616e61676572}。 + \end{itemize} + \item 该字符串用于第~\ref{sec:cmac-session-key} 节 \ttf{derivation_buffer}(偏移 1 起 21 字节)及下文 GCM \ttf{AAD}(偏移 0 起 21 字节); + \item Item 载荷加密前缀字节:\texttt{0x02}(写入 AAD 偏移 24 处); + \item GCM \ttf{AAD} 容器长度:64 字节。 +\end{itemize} + +\ttf{AAD}(Additional Authenticated Data,附加认证数据)是不加密但参与认证的上下文字节串。GCM 会把 AAD 纳入认证标签计算,解封时必须提供完全相同的 AAD 才能通过认证;AAD 本身不是加密输出,也不作为独立字段写入 \texttt{.sealed} 文件。 + +\textbf{AAD 布局(64 字节,\ttf{BYTESTRING[64]}):} +实现 MUST 分配 64 字节缓冲区并初始化为全 \texttt{0x00},再按下述规则写入(与参考实现 \texttt{tad.set(aad); tad[24]=prefix} 一致): +\begin{itemize} + \item 偏移 0--20:域分离字符串的 21 字节 UTF-8 原样拷贝(与上列十六进制一致); + \item 偏移 21--23:保持 \texttt{0x00}(字符串未占用,\textbf{不}写入 NUL 或换行); + \item 偏移 24:前缀字节(Item 载荷加密为 \texttt{0x02}); + \item 偏移 25--63:保持 \texttt{0x00}。 +\end{itemize} +换言之,当前版本 Item 载荷加密使用的 AAD 为: +\begin{center} + \small + \ttf{746563682e7965657a2e6b65792e6d616e61676572 || 000000 || 02 || 00 * 39} +\end{center} +(共 64 字节;\texttt{00 * 39} 表示 39 个零字节,非 ASCII 字符 \texttt{'0'}。) +若后续版本调整上述常量或布局,\ttf{version_number} MUST 递增并提供兼容说明。 + +\subsection{AES-128-GCM 运算}\label{sec:aes-gcm} +本小节规定\textbf{AES-128-GCM 运算}模块:在已知 \ttf{session_key} 的前提下,对变长数据提供认证加解密抽象接口(算法可参照 FIPS 197 与 NIST SP 800-38D;本标准不展开内部轮函数)。 + +\textbf{输入(来源):} +\begin{itemize} + \item \textbf{封装(\texttt{GCM\_Encrypt})}:\ttf{K}(16 字节,即 \ttf{session_key})、\ttf{IV}(12 字节 Nonce)、\ttf{P}(明文)、\ttf{A}(64 字节 AAD,布局见第~\ref{sec:ref-derive-params} 节); + \item \textbf{解封(\texttt{GCM\_Decrypt})}:\ttf{K}、\ttf{IV}、\ttf{C}(密文)、\ttf{A}、\ttf{T}(16 字节认证标签)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item \textbf{封装}:\ttf{C}(\ttf{|C| = |P|})、\ttf{T}(16 字节); + \item \textbf{解封}:认证成功时 \ttf{P}(\ttf{|P| = |C|});认证失败时 MUST NOT 输出 \ttf{P}(见第~\ref{sec:auth-failure} 节)。 +\end{itemize} + +\ttf{A} 不参与加密,仅参与认证标签计算。 + +\textbf{封装接口:} +\begin{center} + \texttt{GCM\_Encrypt(K, IV, P, A) $\rightarrow$ (C, T)} +\end{center} +\ttf{P} 为明文(变长);\ttf{C} 为密文且 \ttf{|C| = |P|};\ttf{T} 为认证标签,当前版本固定 16 字节。 + +\textbf{解封接口:} +\begin{center} + \texttt{GCM\_Decrypt(K, IV, C, A, T) $\rightarrow$ P \quad 或 \quad 认证失败} +\end{center} + +\subsection{内容加密}\label{sec:content-encryption} +本小节规定封装侧\textbf{内容加密}模块:输入批包 \ttf{B} 与接收方长期公钥,输出本条 Item 的 \ttf{item_size} 与 \ttf{cipher_payload}(字段布局见第~\ref{sec:cipher-payload-layout} 节)。 +封装流程(第~\ref{sec:seal-chapter} 章)\textbf{仅}通过本模块接口调用,\textbf{不}展开模块内部的密钥协商、编码与 GCM 步骤。 + +\textbf{输入:} +\begin{itemize} + \item \ttf{B}:本条 Item 对应的批包,布局见第~\ref{sec:batch-package} 节; + \item 接收方长期公钥:应用层提供(格式见第~\ref{sec:curve-keys} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item \ttf{item_size}:\ttf{cipher_payload} 总字节数(见第~\ref{sec:item-record} 节); + \item \ttf{cipher_payload}:含解封所需全部字段(布局见第~\ref{sec:cipher-payload-layout} 节)。 +\end{itemize} + +\textbf{模块实施步骤:}\label{sec:seal-item-crypto} +封装侧对本条 Item MUST 在本节内依次完成下列步骤(不对外暴露中间量接口): +\begin{enumerate} + \item 按第~\ref{sec:curve-keys} 与第~\ref{sec:key-derivation} 节为本条 Item 协商会话密钥,生成 \ttf{iv} 与 \ttf{ephemeral_public_key}(随机性见第~\ref{sec:randomness} 节); + \item 按下文「明文编码」处理 \ttf{B}; + \item 按下文「GCM 加密」与第~\ref{sec:aes-gcm} 节完成认证加密; + \item 按下文「写入 \texttt{.sealed} 字段」组装 \ttf{cipher_payload} 并计算 \ttf{item_size}。 +\end{enumerate} +字段字节布局见第~\ref{sec:cipher-payload-layout} 节;向内容区追加 \texttt{item\_size} 前缀见第~\ref{sec:item-generation} 节。 + +\textbf{明文编码(当前版本):} +为实现互操作,在 \textbf{GCM 运算边界}上对批包字节串 \ttf{B} 做小写十六进制 ASCII 编码(常记 \ttf{HexEncode})。 + +该步骤与第~\ref{sec:seal-chapter} 章「业务输入」中的 \textbf{UTF-8 文本编码}无关: +前者作用于已形成的 \ttf{B}; +后者在更早阶段将 Unicode 文本转为载荷字节串,再进入 \ttf{nt_input} 与 \ttf{B}。 +启用文本输入且采用当前版本 GCM 时,\textbf{二者均须执行}。 + +\begin{itemize} + \item 设 \ttf{HexEncode(B)} 将 \ttf{B} 中每个字节编码为两个小写十六进制 ASCII 字符(\texttt{0--9a--f}),得到字节串 \ttf{P},长度为 \texttt{2 $\times$ |B|}; + \item \ttf{P} MUST 为 \ttf{HexEncode(B)} 的结果,并作为 \ttf{GCM_Encrypt} 的明文输入。 +\end{itemize} + +\textbf{GCM 加密(当前版本)} + +按第~\ref{sec:aes-gcm} 节 \texttt{GCM\_Encrypt} 接口,参数绑定如下: +\begin{itemize} + \item \ttf{K = session_key}:16 字节会话密钥; + \item \ttf{IV = iv}:12 字节,逐 Item 随机生成; + \item \ttf{P}:由上文明文编码得到的字节串; + \item \ttf{A = AAD}:64 字节,布局见第~\ref{sec:ref-derive-params} 节。 +\end{itemize} + +\textbf{算法输出:} +\begin{itemize} + \item \ttf{C}:GCM 密文(变长,不含认证标签); + \item \ttf{T}:16 字节认证标签。 +\end{itemize} + +\textbf{写入 \texttt{.sealed} 字段:} +\begin{itemize} + \item \ttf{encrypted_data = C}; + \item \ttf{gcm_tag = T}; + \item \ttf{iv} 与 \ttf{ephemeral_public_key} 不是 GCM 输出,但作为解封必需参数,按第~\ref{sec:cipher-payload-layout} 节顺序与 \ttf{encrypted_data}、\ttf{gcm_tag} 一并写入 \ttf{cipher_payload}; + \item \ttf{AAD} 由版本常量和前缀规则重建,不写入文件; + \item \ttf{item_size}:\ttf{cipher_payload} 总字节数。 +\end{itemize} + +\subsection{解封侧密码学操作}\label{sec:decrypt-crypto} +本小节规定解封侧\textbf{内容解密还原}模块:输入 \ttf{cipher_payload} 与接收方长期私钥,输出批包 \ttf{B}(为第~\ref{sec:content-encryption} 节的逆运算)。 +解封流程(第~\ref{sec:unseal-chapter} 章)\textbf{仅}通过本模块接口调用,\textbf{不}展开模块内部步骤。 + +\textbf{输入:} +\begin{itemize} + \item \ttf{cipher_payload}:本条 Item 密文载荷(布局见第~\ref{sec:cipher-payload-layout} 节); + \item 接收方长期私钥:应用层提供(格式见第~\ref{sec:curve-keys} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 认证成功:批包 \ttf{B}; + \item 认证失败:见第~\ref{sec:auth-failure} 节(MUST NOT 输出 \ttf{B})。 +\end{itemize} + +\textbf{模块实施步骤:} +\begin{enumerate} + \item 按下文「解析 \ttf{cipher_payload}」取得密文字段; + \item 按第~\ref{sec:curve-keys} 与第~\ref{sec:key-derivation} 节派生会话密钥; + \item 按下文「GCM 解密」与第~\ref{sec:aes-gcm} 节完成认证解密; + \item 按下文「明文解码」得批包 \ttf{B}。 +\end{enumerate} + +\textbf{解析 \ttf{cipher_payload}(当前版本):} +\begin{itemize} + \item 按第~\ref{sec:cipher-payload-layout} 节切分 \ttf{encrypted_data}、\ttf{iv}、\ttf{ephemeral_public_key}、\ttf{gcm_tag}; + \item 长度或布局非法时 MUST 判为格式错误。 +\end{itemize} + +\textbf{GCM 解密(当前版本)} + +按第~\ref{sec:aes-gcm} 节 \texttt{GCM\_Decrypt} 接口,参数绑定如下: +\begin{itemize} + \item \ttf{K = session_key}:16 字节会话密钥; + \item \ttf{IV = iv}:12 字节,取自 \ttf{cipher_payload}; + \item \ttf{C = encrypted_data};\ttf{T = gcm_tag}; + \item \ttf{A = AAD}:64 字节,布局见第~\ref{sec:ref-derive-params} 节; + \item 认证成功时得 \ttf{P}(与封装侧 \ttf{GCM_Encrypt} 所用 \ttf{P} 同型)。 +\end{itemize} + +\textbf{明文解码(当前版本):} +对 \ttf{GCM_Decrypt} 输出的 \ttf{P} 做与第~\ref{sec:content-encryption} 节 \ttf{HexEncode} 互逆的小写十六进制还原(常记 \ttf{HexDecode}): +\begin{itemize} + \item \ttf{HexDecode(P)} 将 \ttf{P} 按每两个 ASCII 十六进制字符还原为一个字节;字符 MUST 属于 \texttt{0--9a--f},\ttf{|P|} MUST 为偶数,否则 MUST 判为格式错误; + \item \ttf{B} MUST 为 \ttf{HexDecode(P)} 的结果。 +\end{itemize} + +\subsection{随机性要求}\label{sec:randomness} +\begin{itemize} + \item \ttf{iv} MUST 由密码学安全随机源逐 Item 生成,且在同一 \ttf{session_key} 下 MUST NOT 重用; + \item 每条 Item 的临时私钥 MUST 由密码学安全随机源生成,且 MUST 满足 \texttt{secp256k1} 私钥有效性要求; + \item 随机源不可用或生成失败时,封装流程 MUST 中止并返回错误,MUST NOT 以固定值或可预测值替代。 +\end{itemize} + +\subsection{认证失败语义}\label{sec:auth-failure} +\begin{itemize} + \item \ttf{gcm_tag} 校验失败、密文长度非法或密钥派生输入非法时,解封器 MUST 将该 Item 视为无效; + \item 上述失败后,解封器 MUST 停止继续输出本条 Item 及后续 Item 的明文(除非实现文档明确声明并验证的其他恢复语义); + \item 实现 SHOULD 返回可区分错误码(如附录 C 中 \ttf{E_AEAD_AUTH_FAILED}),便于审计与告警。 +\end{itemize} + +\section{封装流程(参考)}\label{sec:seal-chapter} +本章给出将业务输入写入 \texttt{.sealed} 文件的\textbf{参考流程}。 +\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;下文各节为信息性步骤说明,除非某条明确要求校验或布局。 + +\textbf{子流程(独立说明):} +\begin{itemize} + \item 输入规范化(第~\ref{sec:input-normalization} 节); + \item 滚动摘要累计(第~\ref{sec:rolling-hash} 节); + \item 批包封装与 Item 写出(第~\ref{sec:item-generation} 节); + \item 批刷写阈值(第~\ref{sec:batch-flush-threshold} 节); + \item Block 组织与切分(第~\ref{sec:block-split} 节); + \item Block 元信息维护(第~\ref{sec:block-meta} 节); + \item 收尾落盘(第~\ref{sec:seal-finalize} 节); + \item 封装流程总览(第~\ref{sec:seal-total-algorithm} 节,含主循环图与组合调度)。 +\end{itemize} +全局一致性见第~\ref{sec:seal-consistency} 节。 + +\subsection{输入规范化}\label{sec:input-normalization} +将宿主提交的业务载荷编码为一条规范化输入记录 \ttf{nt_input}。 + +\textbf{输入:} +\begin{itemize} + \item 一段载荷字节串,长度为 \ttf{L}(由宿主决定切分策略); + \item 若上层以 Unicode 文本提交,须先经 UTF-8 编码为字节串(见下文说明)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 一条 \ttf{nt_input}(完整字节串,长度为 \texttt{12 + L})。 +\end{itemize} + +\textbf{相关格式:} +\ttf{nt_input} 属于加密前明文侧数据,\texttt{.sealed} 文件内不单独存放其原文。 +当前版本(\ttf{version_number = 2})\ttf{nt_input} 布局为: +\begin{center} + \texttt{[reserved (4B)][payload\_len (8B)][payload (L B)]} +\end{center} +\begin{itemize} + \item \ttf{reserved}(偏移 0--3):MUST 为 \texttt{0x00000000}; + \item \ttf{payload_len}(偏移 4--11):\ttf{UINT64(LE)},MUST 等于 \ttf{L}; + \item \ttf{payload}(偏移 12 起):长度为 \ttf{L} 的载荷字节串。 +\end{itemize} + +\textbf{实施步骤:} +\begin{enumerate} + \item 若输入为 Unicode 文本,MUST 以 UTF-8 编码为字节串,且 \textbf{MUST NOT} 在首部附带 BOM(\texttt{EF BB BF});若以原始二进制流提交,跳过本步; + \item 按上文布局写入 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload},得到一条 \ttf{nt_input}。 +\end{enumerate} + +\textbf{说明:} +\begin{itemize} + \item \textbf{宿主 / 集成方}决定每次提交的载荷字节串长度 \ttf{L},从而决定本条 \ttf{nt_input} 长度为 \texttt{12 + L};本标准不规定 \ttf{L} 的切分策略; + \item 同一业务输入在不同实现中 MUST 产生逐字节相同的 \ttf{nt_input};本标准不规定 \ttf{payload} 业务语义; + \item UTF-8 属于应用层字符编码,发生在 \ttf{nt_input} 之前;批包进入第~\ref{sec:content-encryption} 节前的编码与 + UTF-8 \textbf{不是}同一步; + \item 封装实现 MUST NOT 在载荷外附加未在本节定义的额外前缀或后缀; + \item 解封侧从完整 \ttf{nt_input} 校验字段后提取 \ttf{payload},不一致时 MUST 判为格式错误; + \item 实现符号对照见附录~\ref{app:ref-impl-symbols}。 +\end{itemize} + +\subsection{滚动摘要累计}\label{sec:rolling-hash} +在封装过程中按顺序累计全部 \ttf{nt_input} 的滚动摘要,终值写入 Header.\ttf{data_hash}。 + +\textbf{输入:} +\begin{itemize} + \item 按封装顺序产生的各条 \ttf{nt_input} 完整字节串(含 12 字节头,布局见第~\ref{sec:input-normalization} 节); + \item 初值 $H_0$(当前版本见下文)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item \rollhash 终值 $H_n$(32 字节),封装完成时写入 Header.\ttf{data_hash}(时机见第~\ref{sec:seal-finalize} 节)。 +\end{itemize} + +\textbf{相关格式:} +Header.\ttf{data_hash} 字段定义见第~\ref{sec:header-region} 节;累计与 Item 边界无关,仅依赖 \ttf{nt_input} 全局顺序。 + +\textbf{实施步骤:} +当前版本 (\ttf{version_number = 2}) 规则如下: +\begin{center} + \texttt{H\_0 = keccak256("Fidelius")} \\ + \texttt{H\_{i+1} = keccak256(H\_i || nt\_input\_i)} +\end{center} +记共产生 \ttf{n} 条 \ttf{nt_input},终态为 $H_n$。\textbf{\rollhash 终值}\label{sec:rolling-hash-final}定义为 $H_n$;封装完成时 Header.\ttf{data_hash} MUST 等于 $H_n$。 + +\textbf{说明:} +每得一条 \ttf{nt_input} 可按本节规则更新摘要状态;终值 $H_n$ 在收尾写入 Header(第~\ref{sec:seal-finalize} 节)。何时调用本节见第~\ref{sec:seal-total-algorithm} 节;解封侧比对见第~\ref{sec:integrity-check} 节。 + +\subsection{批包封装与 Item 写出}\label{sec:item-generation} +将\textbf{同一批次}的多条 \ttf{nt_input} 打成批包 \ttf{B},加密并在内容区追加\textbf{一条} Item 记录。 + +\textbf{输入:} +\begin{itemize} + \item 若干条完整的 \ttf{nt_input}(条数 $\ge 1$,批内顺序 MUST 与拼接顺序一致); + \item 接收方长期公钥(应用层提供,见第~\ref{sec:curve-keys} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 内容区追加的一条 Item:\texttt{[item\_size][cipher\_payload]}(布局见第~\ref{sec:item-record}、\ref{sec:cipher-payload-layout} 节)。 +\end{itemize} + +\textbf{相关格式:} +\begin{itemize} + \item 若干 \ttf{nt_input} 首尾拼接为批包 \ttf{B}(布局见第~\ref{sec:batch-package} 节); + \item 一个 \ttf{B} 对应内容区\textbf{一条} Item:\texttt{UINT64(LE) item\_size} 后跟 \ttf{cipher_payload}(见第~\ref{sec:item-record}、\ref{sec:cipher-payload-layout} 节)。 +\end{itemize} + +\textbf{实施步骤:} +\begin{enumerate} + \item 将输入的各条 \ttf{nt_input} 按第~\ref{sec:batch-package} 节首尾拼接,得到批包 \ttf{B}; + \item 对 \ttf{B} 与接收方长期公钥调用第~\ref{sec:content-encryption} 节内容加密模块,得到 \ttf{item_size} 与 \ttf{cipher_payload}; + \item 按第~\ref{sec:item-record} 节向内容区追加 \texttt{UINT64(LE) item\_size} 与 \ttf{cipher_payload}。 +\end{enumerate} + +\textbf{说明:} +本节\textbf{不}规定何时凑满一批、也不维护待刷写队列;触发条件见第~\ref{sec:batch-flush-threshold} 节,组合调度见第~\ref{sec:seal-total-algorithm} 节。 +向内容区追加顺序见第~\ref{sec:seal-consistency} 节;写出后更新 Header 计数与 Block Info 见第~\ref{sec:block-meta} 节。 + +\subsection{批刷写阈值}\label{sec:batch-flush-threshold} +规定封装实现\textbf{何时}将当前累积的一批 \ttf{nt_input} 作为同一批次,调用第~\ref{sec:item-generation} 节打成 \ttf{B} 并写出 Item;队列维护与调用顺序见第~\ref{sec:seal-total-algorithm} 节。 + +\textbf{参数:} +\begin{itemize} + \item \ttf{T_batch}:批刷写阈值,由封装实现声明,\textbf{不}写入 \texttt{.sealed} 文件。 +\end{itemize} + +\textbf{度量:} +设当前\textbf{待打包批次}(尚未送入第~\ref{sec:item-generation} 节的 \ttf{nt_input} 集合)为 $\mathcal{N}$。 +记 +\[ + S_{\mathrm{batch}} = \sum_{n \in \mathcal{N}} |n| +\] +其中 $|n|$ 为 \ttf{nt_input} 的\textbf{完整编码字节长度}(含 12 字节头,即 \texttt{12 + L},布局见第~\ref{sec:input-normalization} 节)。 +第~\ref{sec:seal-total-algorithm} 节队列 \ttf{Q} 上的 $S(Q)$ 与本节 $S_{\mathrm{batch}}$ 采用同一度量。 + +\textbf{刷写条件:} +\begin{enumerate} + \item \textbf{阈值触达:}当 $S_{\mathrm{batch}} \ge T_{\mathrm{batch}}$ 时,封装实现 MUST 对 $\mathcal{N}$ 中\textbf{全部}条目执行一次第~\ref{sec:item-generation} 节,并清空该待打包批次; + \item \textbf{输入结束:}宿主声明输入流结束时,若 $\mathcal{N}$ 非空,封装实现 MUST 再执行一次第~\ref{sec:item-generation} 节(即使 $S_{\mathrm{batch}} < T_{\mathrm{batch}}$)。 +\end{enumerate} + +\textbf{完整性约束:} +\begin{itemize} + \item 单条 \ttf{nt_input} MUST 完整地属于一个批次;MUST NOT 为凑满 \ttf{T_batch} 而在某条 \ttf{nt_input} 内部截断载荷或头字段。 +\end{itemize} + +\textbf{说明:} +\ttf{L}、\ttf{T_batch} 均不在 Header 中编码。 +参考实现取 \ttf{T_batch = 65536}(\ttf{MaxItemSize});若变更阈值导致批次划分或 Item 数量变化,\ttf{version_number} MUST 递增。 +若宿主按固定块大小 \ttf{T_chunk} 提交载荷且 \ttf{T_chunk = T_batch},常见为每批 1 条 \ttf{nt_input};\ttf{L} 较小时一批可含多条 \ttf{nt_input}(附录~\ref{app:ref-impl-symbols})。 + +\subsection{Block 组织与切分}\label{sec:block-split} +本节说明内容区中的 Block 如何由 Item 构成及何时切分。 + +\textbf{组织关系说明:} +\begin{itemize} + \item 内容区由按封装顺序写出的 Item 首尾拼接而成; + \item 按该顺序将 Item 划分为若干 \textbf{Block}:每个 Block 是其中连续若干条 Item 组成的段,且单个 Block 内 Item 条数 MUST NOT 超过 \ttf{N_block};各 Block 在 Item 下标与内容区字节偏移上均无隙、无重叠,共同覆盖全部 Item; + \item 每个 Block 对应 Block Infos 区中的一条 \textbf{Block Info}(32 字节,布局见第~\ref{sec:block-infos} 节);Header.\ttf{block_number} 为 Block 个数,MUST 等于 Block Infos 区条目数。 +\end{itemize} + +\textbf{划分规则(当前版本):} +封装侧按 Item \textbf{写出顺序}维护\textbf{当前 Block}: +\begin{enumerate} + \item \textbf{首条 Item}:开启第 0 个 Block,该 Block 内仅含本条 Item; + \item \textbf{并入当前 Block}:若当前 Block 内 Item 条数尚未达到 \ttf{N_block},本条 Item 并入当前 Block,仅扩展该 Block 的结束下标与结束偏移; + \item \textbf{切分新建 Block}:若当前 Block 内 Item 条数已达 \ttf{N_block},则关闭当前 Block 并新建下一 Block,本条 Item 作为新 Block 的首条 Item。 +\end{enumerate} +单条 Item MUST 完整地属于一个 Block;MUST NOT 将一条 Item 拆入两个 Block。 + +\textbf{参考实现:} +当前版本参考实现取 \ttf{N_block = 256}(\ttf{ItemNumPerBlockLimit},见附录~\ref{app:ref-impl-symbols})。 + +\textbf{说明:} +\ttf{N_block} 由封装实现声明,\textbf{不}写入 \texttt{.sealed} 文件;变更且影响互操作时 \ttf{version_number} MUST 递增。 +宿主 \textbf{MAY} 将多段业务数据编码为多条 \ttf{nt_input} 并写入同一 \texttt{.sealed} 内容区;Block Info 不记录「第几个源文件」,仅索引本文件内 Item 与密文字节。 + +\subsection{Block 元信息维护}\label{sec:block-meta} +每写出一条 Item 后,封装实现 MUST 按第~\ref{sec:block-split} 节的划分规则,更新 Header.\ttf{item_number} 与内存中的 Block Info 列表,供第~\ref{sec:seal-finalize} 节写入 Block Infos 区。 + +\textbf{维护对象:} +\begin{itemize} + \item Header.\ttf{item_number}、Header.\ttf{block_number}; + \item 长度为 \ttf{block_number} 的 Block Info 列表(每条 32 字节,四字段语义见第~\ref{sec:block-meta-fields} 节)。 +\end{itemize} + +\textbf{本条 Item 占用字节。} 记刚写出的 Item 其 \ttf{item_size} 字段值为 \ttf{s}(即 \ttf{cipher_payload} 长度),则该 Item 在内容区占用 +\[ + L_{\mathrm{item}} = 8 + s +\] +字节(8 字节 \texttt{item\_size} 前缀 + \ttf{cipher_payload},见第~\ref{sec:item-record} 节)。下文凡写 $L_{\mathrm{item}}$,均指本条 Item 的该占用长度。 + +\subsubsection{Block Info 字段含义}\label{sec:block-meta-fields} +每条 Block Info 含四个 UINT64 字段,在 \textbf{本 \texttt{.sealed} 文件} 内描述一个 Block 所覆盖的 Item 范围;坐标系均针对该文件的内容区与 Item 序列,\textbf{不}针对业务侧被封装前的源文件。布局见第~\ref{sec:block-infos} 节。 + +\textbf{Item 下标:\ttf{start_item_index}、\ttf{end_item_index}。} +\begin{itemize} + \item 含义:本 Block 在内容区 Item \textbf{逻辑序号}上的半开区间 \texttt{[start\_item\_index, end\_item\_index)}。按封装写出顺序,自 \texttt{0} 起为第 0 条 Item、第 1 条 Item……直至 \texttt{item\_number - 1}(与 Header.\ttf{item_number} 一致)。 + \item \ttf{start_item_index}:该 Block 内\textbf{第一条} Item 的下标(\textbf{含})。 + \item \ttf{end_item_index}:该 Block 内\textbf{最后一条} Item 的下标加 1(\textbf{不含});Block 内 Item 条数为 \texttt{end\_item\_index - start\_item\_index},MUST NOT 大于 \ttf{N_block};当该值已达 \ttf{N_block} 时,下一条写出的 Item MUST 按第~\ref{sec:block-split} 节开启新 Block; + \item \textbf{不是}:源明文文件编号、源文件内块号、\ttf{nt_input} 的全局序号(一条 Item 的批包内可含多条 \ttf{nt_input},见第~\ref{sec:batch-package} 节),亦 \textbf{不是} 其它 \texttt{.sealed} 文件中的 Item 下标。 +\end{itemize} + +\textbf{内容区字节偏移:\ttf{start_file_pos}、\ttf{end_file_pos}。} +\begin{itemize} + \item 含义:本 Block 在 \texttt{.sealed} 文件 \textbf{Content Region} 内占据的字节半开区间 \texttt{[start\_file\_pos, end\_file\_pos)}。偏移以内容区首字节为 \texttt{0}(内容区在文件最前,故从该文件物理偏移 \texttt{0} 起算)。\textbf{不包含} Block Infos 区与 Header 区(见第~\ref{sec:region-length} 节)。 + \item \ttf{start_file_pos}:该 Block 第一条 Item 的 \texttt{item\_size} 前缀起始处(\textbf{含}); + \item \ttf{end_file_pos}:该 Block 末字节的内容区偏移加 \texttt{1}(半开区间右端,\textbf{不含});Block 在内容区占用 \texttt{end\_file\_pos - start\_file\_pos} 字节。 + \item \textbf{不是}:被封装前原始明文文件(或任意宿主路径下文件)内的字节偏移。字段名 \texttt{file} 指本 \texttt{.sealed} 输出文件的内容区(与参考实现 \texttt{block\_info\_t.start\_file\_pos} 一致),勿与「源文件偏移」混读。 +\end{itemize} + +\textbf{两对字段的对应关系。} +同一 Block 上,Item 下标区间与内容区字节区间一一对应。 +区间内第 \ttf{k} 号 Item(\ttf{start_item_index} $\le$ \ttf{k} $<$ \ttf{end_item_index})的密文记录 \texttt{[item\_size][cipher\_payload]} 按写出顺序首尾相接,填满 \texttt{[start\_file\_pos, end\_file\_pos)}。 +全体 Block Info 在 Item 维度无隙覆盖 \texttt{[0, item\_number)},在字节维度无隙覆盖 \texttt{[0, content\_size)}。 + +\textbf{更新规则:} +记刚完成内容区写出、本条 Item 已追加之后:Header.\ttf{item_number} 已为 \ttf{n}(\ttf{n} 从 1 起),刚写入的为第 \ttf{n-1} 号 Item(下标从 0 起)。记 \ttf{cur} 为\textbf{当前 Block} 的 Block Info,即列表中下标 \texttt{block\_number - 1} 的那一条(当 \ttf{block_number > 0} 时)。 + +\textbf{情形 1:首个 Item}(\ttf{block_number = 0},\ttf{n = 1})。新建第 \texttt{0} 条 Block Info,记为 \ttf{cur}: +\begin{itemize} + \item \ttf{start_item_index} $\leftarrow$ \texttt{0}(不变); + \item \ttf{end_item_index} $\leftarrow$ \texttt{1}; + \item \ttf{start_file_pos} $\leftarrow$ \texttt{0}(不变); + \item \ttf{end_file_pos} $\leftarrow$ \texttt{0} $+$ $L_{\mathrm{item}}$; + \item Header.\ttf{block_number} $\leftarrow$ \texttt{1}。 +\end{itemize} + +\textbf{情形 2:后续 Item,并入当前 Block}(\ttf{block_number > 0},且当前 Block 内 Item 条数小于 \ttf{N_block})。仅更新 \ttf{cur} 的\textbf{结束边界},起始两字段不变: +\begin{itemize} + \item \ttf{start_item_index}:不变; + \item \ttf{end_item_index} $\leftarrow$ \ttf{cur.end_item_index} $+$ \texttt{1}; + \item \ttf{start_file_pos}:不变; + \item \ttf{end_file_pos} $\leftarrow$ \ttf{cur.end_file_pos} $+$ $L_{\mathrm{item}}$; + \item Header.\ttf{block_number}:不变。 +\end{itemize} + +\textbf{情形 3:后续 Item,新建 Block}(\ttf{block_number > 0},且当前 Block 内 Item 条数已达 \ttf{N_block})。令 \ttf{prev} $=$ \ttf{cur},新建下一条 Block Info(下标为原 \ttf{block_number})记为 \ttf{cur},四字段均从前一 Block 的结束处接续: +\begin{itemize} + \item \ttf{start_item_index} $\leftarrow$ \ttf{prev.end_item_index}; + \item \ttf{end_item_index} $\leftarrow$ \ttf{prev.end_item_index} $+$ \texttt{1}; + \item \ttf{start_file_pos} $\leftarrow$ \ttf{prev.end_file_pos}; + \item \ttf{end_file_pos} $\leftarrow$ \ttf{prev.end_file_pos} $+$ $L_{\mathrm{item}}$; + \item Header.\ttf{block_number} $\leftarrow$ 原值 $+$ \texttt{1}。 +\end{itemize} + +\textbf{不变量(每次写出 Item 后 MUST 成立):} +\begin{itemize} + \item Header.\ttf{item_number} $=$ 各 Block 的 \texttt{(end\_item\_index - + start\_item\_index)} 之和; + \item 各 Block 的 Item 下标区间首尾相接、无重叠,并覆盖 \texttt{[0, item\_number)}; + \item 各 Block 的 \ttf{start_file_pos}、\ttf{end_file_pos} 首尾相接、无重叠,并覆盖 \texttt{[0, content\_size)},且 \ttf{content_size} 等于最后一条 Block Info 的 \ttf{end_file_pos}(见第~\ref{sec:region-length} 节); + \item 对内容区 Item 的\textbf{全局}下标 $k$($0 \le k < \texttt{item\_number}$),记第 $k$ 条 Item 在内容区内的起始字节为 $p_k$,占用长度为 $L_{\mathrm{item}}(k)$。则存在唯一的 Block 下标 $t$($0 \le t < \texttt{block\_number}$),使第 $t$ 条 Block Info 满足 + \[ + \texttt{start\_item\_index} \le k < \texttt{end\_item\_index}, + \] + 且 + \[ + p_k = \texttt{start\_file\_pos} + + \sum_{j=\texttt{start\_item\_index}}^{k-1} L_{\mathrm{item}}(j) + \] + (上式四字段均指第 $t$ 条 Block Info;当 $k$ 等于该条 \texttt{start\_item\_index} 时求和为空)。 +\end{itemize} + +\textbf{说明:} +情形 2、3 与第~\ref{sec:block-split} 节「划分规则」及 Item 数上限一致;情形 1 对应首条 Item 开启第 0 个 Block。 + +\subsection{收尾落盘}\label{sec:seal-finalize} +内容区全部 Item 写出后,将 Block Infos 区与 Header 区追加到文件尾部,完成 \texttt{.sealed} 物理布局。 + +\textbf{输入:} +\begin{itemize} + \item 已写完的内容区及 \ttf{content_size}; + \item \ttf{N} 条 Block Info 取值(\ttf{N = block_number},由第~\ref{sec:block-meta} 节维护); + \item 滚动摘要终值 $H_n$(第~\ref{sec:rolling-hash-final} 节); + \item \ttf{item_number}、\ttf{block_number}(由第~\ref{sec:block-meta} 节维护); + \item \ttf{magic_number}、\ttf{version_number}(取值见第~\ref{sec:header-compat} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 完整的 \texttt{.sealed} 文件(三区布局见第 4 章)。 +\end{itemize} + +\textbf{相关格式:} +三区长度见第~\ref{sec:region-length} 节;Block Info、Header 布局见第~\ref{sec:block-infos}、\ref{sec:header-region} 节。 +物理写入顺序 MUST 为:内容区 $\rightarrow$ Block Infos 区 $\rightarrow$ Header(第~\ref{sec:seal-consistency} 节)。 + +\textbf{实施步骤:} +\begin{enumerate} + \item \textbf{写出 Block Infos 区:}记 \ttf{N = block_number},自偏移 \ttf{content_size} 起顺序写入 \ttf{N} 条 Block Info(各 32 字节,取值见第~\ref{sec:block-meta} 节);\ttf{block_infos_size} $= 32 \times N$; + \item \textbf{写出 Header 区:}紧接其后按第~\ref{sec:header-region} 节写入 64 字节 Header;各字段按本节\textbf{输入}赋值,其中 Header.\ttf{data_hash} 取 $H_n$(MUST 为第~\ref{sec:rolling-hash} 节累计结果,\textbf{不得}以 Item 密文另行摘要替代)。 +\end{enumerate} +完成后 \ttf{file_size} $=$ \ttf{content_size} $+$ \ttf{block_infos_size} $+$ \texttt{64}。 + +\textbf{说明:} +进入本节前须已满足:内容区已全部写完;Block Info 与 \rollhash 终值 $H_n$ 已就绪(见第~\ref{sec:seal-total-algorithm} 节)。 +应用层伴随元数据(公钥、签名等)不在 \texttt{.sealed} 文件内,本标准不做字段约束。 + +\subsection{封装流程总览}\label{sec:seal-total-algorithm}\label{sec:batch-flush-flow} +本节定义封装侧\textbf{如何组合}前述子流程。各子节仅描述单步职责;本节给出调度变量、主循环参考图及\textbf{按图解读}的实施步骤。 + +\textbf{变量与状态(仅在本节及图中使用):} +\begin{itemize} + \item 待刷写队列 \ttf{Q}:元素为完整的 \ttf{nt_input}; + \item $S(Q)$:\ttf{Q} 中各 \ttf{nt_input} 编码后总字节数(与第~\ref{sec:batch-flush-threshold} 节中 $S_{\mathrm{batch}}$ 同型,用于阈值判断); + \item $H$:滚动摘要状态,初值 $H_0$、终值 $H_n$(规则见第~\ref{sec:rolling-hash} 节)。 +\end{itemize} + +\textbf{主循环图与子节对应:} +图~\ref{fig:batch-flush-flow} 为全量顺序封装的一种参考主流程;图源 \texttt{doc/diagrams/seal-batch-flush-flow.mmd}。 +图中流程名词与独立子节的对应关系如下(图中\textbf{不}标注章节号): +\begin{itemize} + \item 「输入规范化」「滚动摘要更新 $H$」:分别见第~\ref{sec:input-normalization}、\ref{sec:rolling-hash} 节;「\ttf{nt_input} 入队 \ttf{Q}」指将完整 \ttf{nt_input} 入 \ttf{Q}; + \item 「批刷写阈值」:见第~\ref{sec:batch-flush-threshold} 节(图中 $S(Q)$ 与 \ttf{T_batch} 同该节); + \item 「Item 写出」:见第~\ref{sec:item-generation} 节;「Block 元信息更新」:见第~\ref{sec:block-meta} 节; + \item 「收尾落盘」:见第~\ref{sec:seal-finalize} 节。 +\end{itemize} + +\begin{figure}[htbp] + \centering + \includegraphics[width=0.85\textwidth]{diagrams/seal-batch-flush-flow.png} + \caption{封装主循环(参考流程)} + \label{fig:batch-flush-flow} +\end{figure} + +\textbf{实施步骤:} +以下按图~\ref{fig:batch-flush-flow} 自上而下解读;各步具体操作以对应子节为准。 +\begin{enumerate} + \item \textbf{开始与初始化}(图:\texttt{开始}):\ttf{version_number} 按约定取值;\ttf{block_number} $= 0$,\ttf{item_number} $= 0$;$H \leftarrow H_0$;\ttf{Q} 为空。 + \item \textbf{主循环入口}(图:判断「宿主已声明输入结束且无新载荷」,取\textbf{否}):每收到一段业务载荷,执行图中「输入规范化 / 滚动摘要更新 $H$ / \ttf{nt_input} 入队 \ttf{Q}」框(第~\ref{sec:input-normalization}、\ref{sec:rolling-hash} 节)。 + \item \textbf{批刷写判断}(图:判断 $S(Q) \ge T_{\mathrm{batch}}$):若为\textbf{是},执行「Item 写出 / Block 元信息更新 / 清空 \ttf{Q}」(第~\ref{sec:item-generation}、\ref{sec:block-meta} 节),返回步骤 2 的判断;若为\textbf{否},直接返回步骤 2 的判断。 + \item \textbf{输入结束}(图:步骤 2 判断取\textbf{是}后,判断「\ttf{Q} 非空」):若为\textbf{是},再执行一次 Item 写出与 Block 元信息更新并清空 \ttf{Q};若为\textbf{否},进入步骤 5。 + \item \textbf{收尾落盘}(图:「收尾落盘」$\rightarrow$ \texttt{结束}):按第~\ref{sec:seal-finalize} 节写出 Block Infos 与 Header,\ttf{data_hash} $\leftarrow H_n$。 +\end{enumerate} + +\subsection{封装一致性要求}\label{sec:seal-consistency} +\begin{itemize} + \item 物理写入顺序 MUST 为:内容区 $\rightarrow$ Block Infos 区 $\rightarrow$ Header; + \item \ttf{item_number} MUST 等于内容区实际 Item 条数; + \item \ttf{block_number} MUST 等于 Block Infos 区条目数; + \item 任意 Block Info 的 \ttf{end_file_pos} MUST 不超过 \ttf{content_size},且相邻 Block 的偏移 SHOULD 首尾相接; + \item 实现 SHOULD 在封装结束后校验计数与 Header.\ttf{data_hash} 按第~\ref{sec:rolling-hash} 节重算结果一致。 +\end{itemize} + +\section{解封流程(参考)}\label{sec:unseal-chapter} +本章给出从 \texttt{.sealed} 文件按序还原业务载荷的\textbf{参考流程}。 +\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;下文各节为信息性步骤说明,除非某条明确要求校验或布局。 + +\textbf{子流程(独立说明):} +\begin{itemize} + \item Header 获取与校验(第~\ref{sec:unseal-header-model} 节); + \item 内容区顺序读取(第~\ref{sec:unseal-item-parse} 节); + \item 单条 Item 认证解密(第~\ref{sec:unseal-item-crypto} 节); + \item 批包拆分、业务输出与滚动摘要(第~\ref{sec:unseal-batch-output} 节); + \item 解封流程总览(第~\ref{sec:unseal-total-algorithm} 节,含主循环图、完整性比对与组合调度); + \item 进度与可恢复状态(第~\ref{sec:unseal-recoverable} 节,建议性)。 +\end{itemize} +上述步骤的\textbf{组合调度}见第~\ref{sec:unseal-total-algorithm} 节;全局一致性见第~\ref{sec:unseal-consistency} 节;失败语义见第~\ref{sec:unseal-errors} 节。 + +\subsection{Header 获取与校验}\label{sec:unseal-header-model} +从 \texttt{.sealed} 文件尾部取得 Header 记录,校验识别字段并计算三区长度(读取顺序见第~\ref{sec:read-model} 节)。 + +\textbf{输入:} +\begin{itemize} + \item 已打开的 \texttt{.sealed} 文件及 \ttf{file_size}(见第~\ref{sec:region-length} 节); + \item \ttf{magic_number}、\ttf{version_number} 的约定取值(见第~\ref{sec:header-compat} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item \ttf{block_number}、\ttf{item_number}、\ttf{data_hash}; + \item \ttf{content_size}、\ttf{block_infos_size}(由 \ttf{file_size} 与 \ttf{block_number} 按第~\ref{sec:region-length} 节算出)。 +\end{itemize} + +\textbf{相关格式:} +Header 记录布局见第~\ref{sec:header-region} 节;\ttf{data_hash} 字段语义见第~\ref{sec:rolling-hash} 节。 + +\textbf{实施步骤:} +\begin{enumerate} + \item 读取文件末尾 64 字节,按第~\ref{sec:header-region} 节解析,得到各 Header 字段; + \item 将解析得到的 \ttf{magic_number}、\ttf{version_number} 与\textbf{输入}中的约定取值比对;不符时 MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_MAGIC_MISMATCH} 或 \ttf{E_VERSION_UNSUPPORTED}; + \item 令 \ttf{N} $=$ \ttf{block_number},按第~\ref{sec:region-length} 节计算 \ttf{block_infos_size} 与 \ttf{content_size};若 \ttf{content_size} 不大于 \texttt{0},MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_FORMAT_TOO_SMALL}(或等价格式错误码)。 +\end{enumerate} + +\textbf{说明:} +全文件顺序解封\textbf{不必}事先读取 Block Infos 区;块级随机访问须先读 Block Infos 区(见第 9 章)。 + +\subsection{内容区顺序读取}\label{sec:unseal-item-parse} +本节说明全文件顺序解封时,如何从内容区切分出单条 Item 的 \ttf{item_size} 与 \ttf{cipher_payload}。 + +\textbf{输入:} +\begin{itemize} + \item \ttf{content_size}、\ttf{item_number}(由第~\ref{sec:unseal-header-model} 节得到)。 +\end{itemize} + +\textbf{维护的状态:} +与第~\ref{sec:unseal-total-algorithm} 节主循环共享,本节负责在读每条 Item 后更新: +\begin{itemize} + \item $p$:内容区读指针(相对内容区起点,单位:字节;初值 $p \leftarrow 0$ 见第~\ref{sec:unseal-total-algorithm} 节); + \item $n$:已处理 Item 计数(供主循环终止判断及完整性比对;初值 $n \leftarrow 0$ 见同上)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 本条 Item 的 \ttf{item_size}(记为 $L$)与 \ttf{cipher_payload}。 +\end{itemize} + +\textbf{相关格式:} +内容区布局见第~\ref{sec:region-length}、\ref{sec:content-region} 节;Item 记录为 \texttt{[item\_size (8B)][cipher\_payload (L B)]}(见第~\ref{sec:item-record}、\ref{sec:cipher-payload-layout} 节)。\textbf{条数不由扫描推断},以 \ttf{item_number} 为终止条件。本节\textbf{每次调用}读取\textbf{一条} Item。 + +\textbf{实施步骤:} +\begin{enumerate} + \item 若 \ttf{content_size} $-$ $p < 8$,MUST 终止本条 Item 读取,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_TRUNCATED}; + \item 自偏移 $p$ 起读取 8 字节,解码为 $L$;若 $L$ 非法(如为 \texttt{0} 或超出实现上限),MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_SIZE_INVALID}; + \item 若 \ttf{content_size} $-$ $p < 8 + L$,MUST 终止本条 Item 读取,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_TRUNCATED}; + \item 自偏移 $p + 8$ 起读取 $L$ 字节,得到 \ttf{cipher_payload};若其布局不满足第~\ref{sec:cipher-payload-layout} 节,MUST 失败(格式错误); + \item 更新 $p \leftarrow p + 8 + L$、$n \leftarrow n + 1$;当 $n = \ttf{item_number}$ 时,若 $p \ne \ttf{content_size}$,MUST 失败(格式错误)。 +\end{enumerate} + +\textbf{说明:} +解密、批包拆分与 \ttf{payload} 输出不在本节展开;见第~\ref{sec:unseal-item-crypto}、\ref{sec:unseal-batch-output} 节及第~\ref{sec:unseal-total-algorithm} 节。 + +\subsection{单条 Item 认证解密}\label{sec:unseal-item-crypto} +将一条 Item 的密文载荷还原为批包 \ttf{B}。 + +\textbf{输入:} +\begin{itemize} + \item \ttf{cipher_payload}(由第~\ref{sec:unseal-item-parse} 节读出); + \item 接收方长期私钥(应用层提供,见第~\ref{sec:curve-keys} 节)。 +\end{itemize} + +\textbf{输出:} +\begin{itemize} + \item 批包 \ttf{B}(认证成功后)。 +\end{itemize} + +\textbf{实施步骤:} +\begin{enumerate} + \item 对 \ttf{cipher_payload} 与接收方长期私钥调用第~\ref{sec:decrypt-crypto} 节内容解密还原模块; + \item 若认证失败(第~\ref{sec:auth-failure} 节),MUST 终止解封,MUST NOT 输出 \ttf{B},错误码 SHOULD 为附录 C 中的 \ttf{E_AEAD_AUTH_FAILED}; + \item 认证成功时,得到 \ttf{B}。 +\end{enumerate} + +\textbf{说明:} +算法细节见第~\ref{sec:decrypt-crypto} 节;本章不重复模块内部步骤。 + +\subsection{批包拆分、业务输出与滚动摘要}\label{sec:unseal-batch-output}\label{sec:unseal-rolling-hash} +将批包 \ttf{B} 拆为若干条 \ttf{nt_input},在输出每条 \ttf{payload} 之前用对应 \ttf{nt_input} 更新滚动摘要 $H$,并按序输出 \ttf{payload}。 + +\textbf{维护的状态:} +$H$:滚动摘要状态($H_0$ 见第~\ref{sec:rolling-hash} 节;初值与调度见第~\ref{sec:unseal-total-algorithm} 节)。每处理完本条 Item 内全部 \ttf{nt_input} 后,按实施步骤更新 $H$。 + +\textbf{输入(每次调用):} +批包 \ttf{B}(由第~\ref{sec:unseal-item-crypto} 节得到)。 + +\textbf{输出(每次调用):} +按批内顺序输出的各条 \ttf{payload}。 + +\textbf{相关格式:} +\ttf{B} 布局见第~\ref{sec:batch-package} 节;\ttf{nt_input} 与 \ttf{payload} 提取规则见第~\ref{sec:input-normalization} 节;$H$ 递推规则与 $H_0$ 定义见第~\ref{sec:rolling-hash}、\ref{sec:rolling-hash-final} 节。 + +\textbf{实施步骤:} +\begin{enumerate} + \item 按第~\ref{sec:batch-package} 节将 \ttf{B} 拆为若干条 \ttf{nt_input};若 \ttf{package_id}、长度字段与剩余字节不一致,MUST 终止解封(格式错误),并 MUST NOT 输出 \ttf{payload}; + \item 对批内每条 \ttf{nt_input} 按序执行下列子步骤(\textbf{不得}在输出 \ttf{payload} 后再用该条 \ttf{nt_input} 更新 $H$): + \begin{enumerate} + \item 按第~\ref{sec:input-normalization} 节校验布局;失败时 MUST 终止解封(格式错误); + \item 在输出 \ttf{payload} 之前,用本条 \ttf{nt_input} 完整字节串(含 12 字节头)按第~\ref{sec:rolling-hash} 节更新 $H$; + \item 提取 \ttf{payload} 并输出。 + \end{enumerate} +\end{enumerate} + +\textbf{说明:} +\ttf{nt_input} 仅在本节步骤 2 内存在;$H$ 的累计顺序 MUST 与封装侧 \ttf{nt_input} 全局顺序一致。完整性比对见第~\ref{sec:unseal-total-algorithm} 节。 + +\subsection{解封流程总览}\label{sec:unseal-total-algorithm}\label{sec:unseal-main-flow} +本节定义解封侧\textbf{如何组合}前述子流程。各子节仅描述单步职责;本节给出调度变量、主循环参考图及\textbf{按图解读}的实施步骤(含完整性比对)。 + +\textbf{变量与状态(仅在本节及图中使用):} +\begin{itemize} + \item 内容区读指针 $p$、已处理 Item 计数 $n$(由第~\ref{sec:unseal-item-parse} 节维护); + \item 滚动摘要状态 $H$(由第~\ref{sec:unseal-batch-output} 节维护;$H_0$ 见第~\ref{sec:rolling-hash} 节); + \item \ttf{item_number}、\ttf{content_size}、\ttf{data_hash}(由第~\ref{sec:unseal-header-model} 节得到)。 +\end{itemize} + +\textbf{主循环图与子节对应:} +图~\ref{fig:unseal-main-flow} 为全量顺序解封的一种参考主流程;图源 \texttt{doc/diagrams/unseal-main-flow.mmd}。 +图中流程名词与独立子节的对应关系如下(图中\textbf{不}标注章节号;\ttf{data_hash} 等字段语义以正文为准): +\begin{itemize} + \item 「读 Header 并校验」「计算 \ttf{content_size}」:见第~\ref{sec:unseal-header-model} 节;图中「初始化 $H = H_0$,$n = 0$」另需 $p \leftarrow 0$($H_0$ 见第~\ref{sec:rolling-hash} 节); + \item 「读 \ttf{item_size} 与 \ttf{cipher_payload}」:见第~\ref{sec:unseal-item-parse} 节; + \item 「认证解密得批包 \ttf{B}」:见第~\ref{sec:unseal-item-crypto} 节; + \item 「拆分 \ttf{B}」「逐条输出 \ttf{payload} 并用 \ttf{nt_input} 更新 $H$」:见第~\ref{sec:unseal-batch-output} 节(须在输出 \ttf{payload} 之前更新 $H$); + \item 「$H$ 等于 \ttf{data_hash}」:见下文实施步骤第 3 项(完整性比对)。 +\end{itemize} + +\begin{figure}[htbp] + \centering + \includegraphics[width=0.85\textwidth]{diagrams/unseal-main-flow.png} + \caption{解封主循环(参考流程,全量顺序解封)} + \label{fig:unseal-main-flow} +\end{figure} + +\textbf{实施步骤:} +以下按图~\ref{fig:unseal-main-flow} 自上而下解读;各步具体操作以对应子节为准。 +\begin{enumerate} + \item \textbf{开始与 Header}(图:\texttt{开始} $\rightarrow$ 「读 Header 并校验」$\rightarrow$ 「计算 \ttf{content_size} / 初始化」):按第~\ref{sec:unseal-header-model} 节取得并校验 Header;$H \leftarrow H_0$,$p \leftarrow 0$,$n \leftarrow 0$。 + \item \textbf{主循环}(图:判断 $n$ 小于 \ttf{item_number},取\textbf{是}):以当前 $p$、$n$ 调用第~\ref{sec:unseal-item-parse} 节得 \ttf{cipher_payload} 及更新后的 $p$、$n$;调用第~\ref{sec:unseal-item-crypto} 节得 \ttf{B};以当前 $H$ 调用第~\ref{sec:unseal-batch-output} 节输出 \ttf{payload} 并得到更新后的 $H$(对应图中「读 Item $\rightarrow$ 认证解密 $\rightarrow$ 拆分并输出」链);$n$ 加 1 后返回本步判断。认证或格式失败时 MUST 终止(图虚线至错误结束)。 + \item \textbf{完整性比对}\label{sec:integrity-check}(图:$n$ 判断取\textbf{否} $\rightarrow$ 「$H$ 等于 \ttf{data_hash}」):将 $H$ 与 \ttf{data_hash} 逐字节比较;相等则成功结束,不等则 MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_INTEGRITY_MISMATCH},并视已输出的 \ttf{payload} 为不可信;流式输出时 SHOULD 停止下游消费并上报错误。 +\end{enumerate} + +\subsection{错误处理}\label{sec:unseal-errors} +下列情况 MUST 立即失败,且\textbf{不得}继续输出后续 \ttf{payload}(已输出部分视实现策略可标记为不可信): +\begin{itemize} + \item \ttf{magic_number} 或 \ttf{version_number} 校验失败(第~\ref{sec:header-compat} 节); + \item \ttf{content_size} 非法,或第~\ref{sec:unseal-item-parse} 节读取 Item 时字节不足或布局非法; + \item 第~\ref{sec:decrypt-crypto} 节内容解密还原模块认证失败(第~\ref{sec:auth-failure} 节); + \item 第~\ref{sec:unseal-batch-output} 节批包拆分或 \ttf{nt_input} 校验失败; + \item 第~\ref{sec:unseal-total-algorithm} 节完整性比对中 $H$ 与 \ttf{data_hash} 不一致。 +\end{itemize} + +\subsection{解封一致性要求}\label{sec:unseal-consistency} +\begin{itemize} + \item 输出的各段 \ttf{payload} 全局顺序 MUST 与封装侧提交的载荷顺序一致; + \item 每条 \ttf{payload} MUST 来自经第~\ref{sec:decrypt-crypto} 节认证成功的批包; + \item 处理完全部 Item 后,$H$ MUST 与 \ttf{data_hash} 一致; + \item 全文件顺序解封时,实际读取的 Item 条数 MUST 等于 \ttf{item_number}; + \item 失败时 MUST 符合第~\ref{sec:unseal-errors} 节,不得静默跳过损坏 Item 继续输出。 +\end{itemize} + +\subsection{进度与可恢复状态(建议)}\label{sec:unseal-recoverable} +支持断点续传或分块交付的实现,在恢复语义下 SHOULD 持久化并可恢复下列状态(名称可因实现而异): +\begin{itemize} + \item 已完成处理的 Item 数(对应 $n$); + \item 内容区读指针 $p$(含已读 \texttt{item\_size} 前缀与 \ttf{cipher_payload}); + \item 滚动摘要状态 $H$(规则 MUST 与第~\ref{sec:rolling-hash} 节一致); + \item 已提交给下游的 \ttf{payload} 字节进度; + \item 已解密但尚未全部交付下游的缓冲(若存在)。 +\end{itemize} + +\textbf{说明:} +写入端与读取端 MUST 在同一恢复语义下同步推进密文读取偏移与明文提交偏移,避免重复输出或跳写;调度见图~\ref{fig:unseal-main-flow}。 + +\section{随机访问与结构化查询} +\subsection{能力分级定义} +本标准将随机访问能力划分为三级: +\begin{itemize} + \item \textbf{L1(块级随机访问)}:基于 \ttf{block_info} 定位指定块范围; + \item \textbf{L2(Item 级随机访问)}:在已定位块内快速跳转到目标 Item; + \item \textbf{L3(结构化随机访问)}:通过记录主键或结构化条件直接定位记录并解封。 +\end{itemize} +其中 L1 为当前格式可直接支撑能力;L2/L3 需要实现侧补充索引策略。 + +\subsection{索引基础与扩展索引} +\begin{itemize} + \item 文件内置索引基础为 \ttf{block_info}(Item 范围 + 文件偏移范围); + \item 实现 MAY 维护外部或伴随索引(例如 \ttf{record_id -> item_index -> offset}); + \item 若启用记录级索引,索引构建规则、校验方式、版本兼容策略 MUST 在实现文档中明示。 +\end{itemize} + +\subsection{结构化查询流程(规范性建议)} +支持 L3 的实现 SHOULD 按以下流程执行: +\begin{enumerate} + \item 根据查询条件命中记录级索引,得到目标 \ttf{item_index}; + \item 通过 \ttf{block_info} 计算最小读取区间; + \item 解析并解密目标 Item; + \item 在批包内提取目标记录并返回; + \item 记录审计信息(查询条件、命中项、校验状态)。 +\end{enumerate} + +\subsection{完整性与安全约束} +\begin{itemize} + \item 局部读取场景下,Item 级 AEAD 校验 MUST 成功方可输出明文; + \item 若实现依赖外部索引,索引篡改风险 MUST 被评估并缓解(签名、哈希链或可信存储); + \item 使用结构化查询时 SHOULD 明确披露可见元数据范围(长度、访问模式等)。 +\end{itemize} + +\subsection{性能与退化策略} +\begin{itemize} + \item 无记录级索引时,读取器 SHOULD 退化为顺序扫描或块级扫描; + \item 实现 SHOULD 提供索引构建开销与查询延迟指标; + \item 对超大文件,建议提供分层索引(块索引 + 记录索引)以控制查询放大率。 +\end{itemize} + +\section{可恢复与流式扩展} +\subsection{断点上下文模型} +可恢复实现可记录以下状态: +\begin{itemize} + \item 已提交密文偏移; + \item 已提交明文偏移; + \item 待提交块队列及剩余字节数。 +\end{itemize} + +\subsection{恢复语义} +恢复时必须保证密文读取偏移与明文写入偏移一致推进,防止重复写入或跳写。 + +\subsection{结构化随机访问扩展(预留)} +结构化随机访问能力已独立成章,见“随机访问与结构化查询”。本节仅保留与断点恢复共同作用时的实现注意事项。 + +\section{兼容性与演进} +\subsection{向后兼容} +新版本字段扩展 SHOULD 不破坏既有字段语义与位置约束。 + +\subsection{向前兼容} +读取器遇到未知扩展字段时 MAY 忽略,但 MUST 保持已知字段解析稳定。 + +\subsection{版本协商} +实现应声明其支持的最小与最大版本范围,并在初始化阶段完成协商或报错。 + +\section{安全考虑} +\subsection{密钥管理} +私钥存储与传输应采用安全载体,避免明文落盘与日志泄露。 + +\subsection{Nonce 误用风险} +Nonce 重复将破坏 GCM 安全性,必须通过实现约束与审计机制防止。 + +\subsection{元信息泄露} +即使内容加密,长度与块分布仍可能泄露模式信息。上层可通过填充策略缓解。 + +\subsection{完整性与签名建议} +建议配合外部签名机制(如 \texttt{data\_hash} 签名)实现跨系统可验证交付。 + +\section{一致性测试与样例} +\subsection{最小一致性测试集} +\begin{itemize} + \item 头字段解析测试; + \item 加解密回环测试; + \item 篡改检测测试; + \item 中断恢复测试; + \item 跨端一致性测试(Node/Browser)。 +\end{itemize} + +\subsection{二进制样例(占位)} +本节后续补充标准样例文件及十六进制字段对照。 + +\subsection{互操作矩阵(占位)} +本节后续补充实现版本与兼容性矩阵。 + +\appendix +\section{附录 A:字段偏移总表} +\subsection{A.1 文件级布局} +\begin{longtable}{>{\raggedright\arraybackslash}p{3.0cm} >{\raggedright\arraybackslash}p{2.8cm} >{\raggedright\arraybackslash}p{2.2cm} >{\raggedright\arraybackslash}p{5.8cm}} + \toprule + 区域 & 长度 & 是否固定 & 说明 \\ + \midrule + \endhead + \ttf{Content Region} & 变长 & 否 & 由若干 Item 顺序构成 \\ + \ttf{Block Infos} & \ttf{32 * block_number} & 否 & 每条 Block Info 固定 32B \\ + \ttf{Header} & 64B & 是 & 位于文件末尾 \\ + \bottomrule +\end{longtable} + +\subsection{A.2 Header 字段总表} +{\small + \begin{longtable}{>{\raggedright\arraybackslash}p{3.0cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{2.6cm} >{\raggedright\arraybackslash}p{5.0cm}} + \toprule + 字段名 & 偏移 & 长度 & 类型 & 约束 \\ + \midrule + \endhead + \ttf{magic_number} & 0 & 8 & \ttf{BYTESTRING[8]} & MUST 等于格式魔数 \\ + \ttf{version_number} & 8 & 8 & \ttf{UINT64(LE)} & MUST 为支持版本 \\ + \ttf{block_number} & 16 & 8 & \ttf{UINT64(LE)} & MUST 与 Block Info 数一致 \\ + \ttf{item_number} & 24 & 8 & \ttf{UINT64(LE)} & MUST 与 Item 数一致 \\ + \ttf{data_hash} & 32 & 32 & \ttf{BYTESTRING[32]} & MUST 为第~\ref{sec:rolling-hash} 节终值;解封比对见第~\ref{sec:integrity-check} 节 \\ + \bottomrule + \end{longtable} +} + +\subsection{A.3 Block Info 字段总表} +{\small + \begin{longtable}{>{\raggedright\arraybackslash}p{3.6cm} >{\raggedright\arraybackslash}p{1.0cm} >{\raggedright\arraybackslash}p{1.0cm} >{\raggedright\arraybackslash}p{2.4cm} >{\raggedright\arraybackslash}p{5.0cm}} + \toprule + 字段名 & 偏移 & 长度 & 类型 & 约束 \\ + \midrule + \endhead + \ttf{start_item_index} & 0 & 8 & \ttf{UINT64(LE)} & 与 \ttf{end_item_index} 组成半开区间 \\ + \ttf{end_item_index} & 8 & 8 & \ttf{UINT64(LE)} & MUST 大于或等于起始下标 \\ + \ttf{start_file_pos} & 16 & 8 & \ttf{UINT64(LE)} & 内容区内起始偏移;见第~\ref{sec:block-meta-fields} 节 \\ + \ttf{end_file_pos} & 24 & 8 & \ttf{UINT64(LE)} & 内容区内结束偏移(不含);见第~\ref{sec:block-meta-fields} 节 \\ + \bottomrule + \end{longtable} +} + +\subsection{A.4 Item 与密文载荷字段总表} +{\small + \begin{longtable}{>{\raggedright\arraybackslash}p{3.8cm} >{\raggedright\arraybackslash}p{1.2cm} >{\raggedright\arraybackslash}p{2.6cm} >{\raggedright\arraybackslash}p{4.8cm}} + \toprule + 字段名 & 长度 & 类型 & 约束 \\ + \midrule + \endhead + \ttf{item_size} & 8 & \ttf{UINT64(LE)} & MUST 与后续 \ttf{cipher_payload} 字节数一致;取值不超过 \ttf{UINT64} 最大值 \\ + \ttf{encrypted_data} & 变长 & \ttf{VARBYTES} & 长度 \ttf{encrypted_data_len};\ttf{AES-128-GCM} 密文 \\ + \ttf{iv} & 12 & \ttf{BYTESTRING[12]} & 当前版本固定 12B;逐 Item 随机生成 \\ + \ttf{ephemeral_public_key} & 64 & \ttf{BYTESTRING[64]} & 当前版本固定 64B;\texttt{secp256k1} 曲线 \texttt{x||y} \\ + \ttf{gcm_tag} & 16 & \ttf{BYTESTRING[16]} & 当前版本固定 16B;GCM 认证标签 \\ + \bottomrule + \end{longtable} +} + +\section{附录 B:伪代码与格式语法} +\subsection{B.1 封装伪代码(规范性参考)} +下列伪代码采用参考实现中的函数名书写,符号与本标准表述的对照见附录~\ref{app:ref-impl-symbols};规范性语义以第~\ref{sec:seal-chapter} 章文字与第~\ref{sec:format-chapter} 章布局为准。 +\begin{verbatim} +input: plaintext_stream, receiver_public_key +state: + H = keccak256("Fidelius") + header = {magic, version, block_number=0, item_number=0, data_hash=0} + block_infos = [] + batch = [] + +for each normalized_record nt_input from plaintext_stream: + batch.append(nt_input) + H = keccak256(H || nt_input) + if batch_size_reach_threshold(): + pkg = batch2ntpackage(batch) + cipher_payload = encrypt_item(pkg, receiver_public_key, prefix=0x02) + write_uint64_le(len(cipher_payload)) + write_bytes(cipher_payload) + update_block_infos_and_header_counter() + batch.clear() + +if batch not empty: + ... (same as above) + +header.data_hash = H +write_block_infos(block_infos) +write_header_64B(header) +\end{verbatim} + +\subsection{B.2 解封伪代码(规范性参考)} +\begin{verbatim} +input: sealed_stream, private_key +state: + read header first + validate(magic, version) + H = keccak256("Fidelius") + read_item_count = 0 + +while read_item_count < header.item_number: + item_size = read_uint64_le() + cipher_payload = read_exact(item_size) + B = decrypt_item_to_batch(cipher_payload, private_key) // 见第 6.5 节,输出 B + batch = ntpackage2batch(B) + for each nt_input in batch: + emit fromNtInput(nt_input) + H = keccak256(H || nt_input) + read_item_count += 1 + +if H != header.data_hash: + error(INTEGRITY_MISMATCH) +\end{verbatim} + +\subsection{B.3 ABNF 风格语法(信息性)} +\begin{verbatim} +sealed-file = content-region block-infos header +content-region = *item-record +item-record = item-size cipher-payload +item-size = uint64-le +cipher-payload = encrypted-data iv epk gcm-tag +iv = 12BYTE +epk = 64BYTE +gcm-tag = 16BYTE +block-infos = *block-info +block-info = start-item-index end-item-index start-pos end-pos +start-item-index = uint64-le +end-item-index = uint64-le +start-pos = uint64-le +end-pos = uint64-le +header = magic version block-num item-num data-hash +magic = 8BYTE +version = uint64-le +block-num = uint64-le +item-num = uint64-le +data-hash = 32BYTE +\end{verbatim} + +\section{附录 C:错误码} +\subsection{C.1 标准错误码定义} +{\small + \begin{longtable}{>{\raggedright\arraybackslash}p{4.2cm} >{\raggedright\arraybackslash}p{2.0cm} >{\raggedright\arraybackslash}p{5.4cm}} + \toprule + 错误码 & 类别 & 含义 \\ + \midrule + \endhead + \ttf{E_FORMAT_TOO_SMALL} & 格式错误 & 文件长度不足以包含最小 Header \\ + \ttf{E_MAGIC_MISMATCH} & 格式错误 & 魔数不匹配,非本格式文件 \\ + \ttf{E_VERSION_UNSUPPORTED} & 兼容性错误 & 版本超出实现支持范围 \\ + \ttf{E_BLOCKINFO_OVERFLOW} & 格式错误 & Block Info 区长度与文件布局不一致 \\ + \ttf{E_ITEM_SIZE_INVALID} & 格式错误 & Item 长度字段越界或不合法 \\ + \ttf{E_ITEM_TRUNCATED} & IO/格式错误 & Item 数据未完整读取 \\ + \ttf{E_AEAD_AUTH_FAILED} & 密码学错误 & GCM Tag 校验失败 \\ + \ttf{E_INTEGRITY_MISMATCH} & 完整性错误 & 最终 data\_hash 校验失败 \\ + \ttf{E_RANDOM_SOURCE_FAILED} & 运行时错误 & 随机源不可用,无法安全封装 \\ + \ttf{E_RESUME_STATE_INVALID} & 恢复错误 & 断点上下文与当前流状态不一致 \\ + \bottomrule + \end{longtable} +} + +\subsection{C.2 错误处理要求} +\begin{itemize} + \item 发生 \ttf{E_AEAD_AUTH_FAILED} 或 \ttf{E_INTEGRITY_MISMATCH} 时,解封器 MUST 停止输出后续明文; + \item 发生格式错误时,读取器 MUST 报错并拒绝继续解析; + \item 实现 SHOULD 对外暴露机器可读错误码及可读错误信息; + \item 断点恢复路径发生 \ttf{E_RESUME_STATE_INVALID} 时,SHOULD 提供“从头重试”建议。 +\end{itemize} + +\section{附录 D:参考实现映射} +\subsection{D.1 标准条款到模块映射} +\begin{longtable}{>{\raggedright\arraybackslash}p{4.0cm} >{\raggedright\arraybackslash}p{7.8cm}} + \toprule + 标准条款域 & 参考实现模块 \\ + \midrule + \endhead + Header / Block Info 编解码 & \ttf{src/header_util.js} \\ + 封装主流程 & \ttf{src/DataProvider.js}, \ttf{src/Sealer.js} \\ + 解封主流程 & \ttf{src/Unsealer.js} \\ + 密码算法与密钥派生 & \ttf{src/ypccrypto.js} \\ + 文件流 Header 前置读取 & \ttf{src/SealedFileStream.js} \\ + 文件识别与版本读取 & \ttf{src/SealedFileUtil.js} \\ + 断点续传上下文 & \ttf{src/Recoverable.js}, \ttf{src/PipelineConext.js} \\ + \bottomrule +\end{longtable} + +\subsection{D.2 封装流程表述与源码符号对照(信息性)}\label{app:ref-impl-symbols} +本附录条目\textbf{不}构成规范性 API;仅便于阅读 Meta-Encryptor Node 参考实现(\ttf{src/header_util.js} 等)。实现者 MAY 使用不同命名,但产生的字节布局 MUST 与第~\ref{sec:seal-chapter} 章一致。 +{\small +\begin{longtable}{>{\raggedright\arraybackslash}p{4.2cm} >{\raggedright\arraybackslash}p{3.6cm} >{\raggedright\arraybackslash}p{4.6cm}} + \toprule + 本标准表述(第~\ref{sec:seal-chapter} 章) & 参考实现符号(Node) & 说明 \\ + \midrule + \endhead + 封装侧编码 & \ttf{toNtInput} & 载荷字节串 $\to$ 一条 \ttf{nt_input} \\ + 解封侧解码 & \ttf{fromNtInput} & 一条 \ttf{nt_input} $\to$ \ttf{payload} \\ + 批包构造 & \ttf{batch2ntpackage} & 若干 \ttf{nt_input} $\to$ 批包 \ttf{B} \\ + 批包拆分 & \ttf{ntpackage2batch} & 批包 \ttf{B} $\to$ \ttf{nt_input} 列表 \\ + 流式封装入口(信息性) & \ttf{Sealer} & \ttf{src/Sealer.js};按 \ttf{T_chunk} 切分载荷 \\ + 批刷写阈值 \ttf{T_batch} & \ttf{MaxItemSize} & \ttf{src/limits.js},参考取值 65536 \\ + Block 内 Item 数上限 \ttf{N_block} & \ttf{ItemNumPerBlockLimit} & \ttf{src/blockfile.js} 构造参数,参考取值 256 \\ + \bottomrule +\end{longtable} +} + +\end{document} From d6a3f3c5e34869e023370eefcde6d129e82de441 Mon Sep 17 00:00:00 2001 From: freeof123 Date: Mon, 25 May 2026 11:06:15 +0800 Subject: [PATCH 2/3] release v0.1.0 --- doc/sealed-format-standard-zh.tex | 202 +++++++++++++----------------- 1 file changed, 86 insertions(+), 116 deletions(-) diff --git a/doc/sealed-format-standard-zh.tex b/doc/sealed-format-standard-zh.tex index 6db48fb..947d74e 100644 --- a/doc/sealed-format-standard-zh.tex +++ b/doc/sealed-format-standard-zh.tex @@ -94,15 +94,15 @@ \subsection{一致性语言} \section{术语与记号} \subsection{术语} \begin{itemize} - \item \textbf{Item}:\texttt{.sealed} 内容区中最小加密单元; + \item \textbf{Item}:\texttt{.sealed} 内容区中\textbf{最小不可分}加密单元;本标准在格式与流程上的规范性边界以 Item 为准(见第~\ref{sec:content-region} 节); \item \textbf{Block}:由若干 Item 组成的逻辑分组,并在元信息区有对应索引项; \item \textbf{Header}:文件尾部固定 64 字节头结构; \item \textbf{Block Info}:描述单个 Block 的 32 字节索引结构;\textbf{Block Infos 区}为由若干条 Block Info 顺序拼接而成的索引区域; - \item \textbf{Data Hash}:Header 字段 \ttf{data_hash} 所承载的摘要;对全部规范化输入记录(\ttf{nt_input})按封装顺序滚动计算,见第~\ref{sec:rolling-hash} 节; - \item \textbf{业务载荷}:宿主提交的一段载荷字节串,记为 \ttf{payload},长度为 \ttf{L};解封侧按封装顺序输出的字节串为 \ttf{payload}(见第~\ref{sec:input-normalization} 节); - \item \textbf{规范化输入记录}:将一条业务载荷按固定规则编码后的\textbf{明文侧}容器字节串(常记 \ttf{nt_input}),含 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload};位于 Item 加密之前,是第~\ref{sec:rolling-hash} 节滚动摘要与批包拼接的基本单元,格式见第~\ref{sec:input-normalization} 节; - \item \textbf{批包}:将若干条规范化输入记录按固定容器格式首尾拼接后的字节串,记为 \ttf{B};一条批包经加密后对应内容区中的一条 Item。容器字节布局见第~\ref{sec:batch-package} 节; - \item \textbf{批刷写阈值} \ttf{T_batch}:封装实现参数,判定何时将一批 \ttf{nt_input} 送入批包封装;规则见第~\ref{sec:batch-flush-threshold} 节;队列调度见第~\ref{sec:seal-total-algorithm} 节; + \item \textbf{Data Hash}:Header 字段 \ttf{data_hash} 所承载的摘要;对全部\textbf{输入单元}按封装全局顺序滚动计算,见第~\ref{sec:rolling-hash} 节; + \item \textbf{业务载荷}:宿主提交的一段载荷字节串,记为 \ttf{payload},长度为 \ttf{L};业务语义与切分由应用层决定;解封侧按封装顺序还原的 \ttf{payload} 由集成层从输入单元解码得到(见第~\ref{sec:input-normalization} 节); + \item \textbf{输入单元}:写入批包前的变长字节串(实现中常记 \ttf{nt_input}),须满足第~\ref{sec:input-normalization} 节;多条输入单元可装入同一 Item 的批包 \ttf{B},再整体加密为一条 Item; + \item \textbf{批包}:将若干条输入单元按第~\ref{sec:batch-package} 节容器格式首尾拼接后的字节串,记为 \ttf{B};一个 \ttf{B} 经加密后对应内容区\textbf{一条} Item; + \item \textbf{批刷写阈值} \ttf{T_batch}:封装实现参数,判定何时将一批输入单元打成 \ttf{B} 并写出 Item;规则见第~\ref{sec:batch-flush-threshold} 节;队列调度见第~\ref{sec:seal-total-algorithm} 节; \item \textbf{Block 内 Item 数上限} \ttf{N_block}:封装实现参数,限定单个 Block 所含 Item 条数的上界;划分规则见第~\ref{sec:block-split} 节;与 Block Info 字段的对应见第~\ref{sec:block-meta-fields} 节。 \end{itemize} @@ -213,8 +213,8 @@ \subsubsection{Header 记录(64 字节)} \item \ttf{version_number}:格式版本; \item \ttf{block_number}:Block Info 条目数量;维护规则见第~\ref{sec:block-meta} 节; \item \ttf{item_number}:内容区 Item 总数量(见第~\ref{sec:content-region} 节);每写出一条 Item 递增,见第~\ref{sec:item-generation} 节; - \item \ttf{data_hash}:对全部 \ttf{nt_input}(按封装顺序)滚动计算得到的最终摘要(\ttf{BYTESTRING[32]})。 - 记录格式见第~\ref{sec:input-normalization} 节; + \item \ttf{data_hash}:对全部输入单元(按封装全局顺序,字节串与批包记录体一致)滚动计算得到的最终摘要(\ttf{BYTESTRING[32]})。 + 累计规则见第~\ref{sec:rolling-hash} 节;输入单元要求见第~\ref{sec:input-normalization} 节; 算法与写入见第~\ref{sec:rolling-hash}、\ref{sec:seal-finalize} 节; 解封校验见第~\ref{sec:integrity-check} 节。 \end{itemize} @@ -277,7 +277,7 @@ \subsection{Content Region 区}\label{sec:content-region} Content Region 区位于文件最前部,在 Block Infos 区与 Header 区之前;总长度 \ttf{content_size} 及其在文件中的起止位置由第~\ref{sec:region-length} 节给出。 -本区由 \ttf{item_number} 条 Item 顺序拼接而成。本文所称单条 \textbf{Item},即内容区最小加密单元;\textbf{Content Region 区}即上述全部 Item 按序拼接形成的区域。内容区按块组织,每块由若干连续的 Item 组成。各条 Item 长度随密文载荷变化,Item 之间无固定步长。 +本区由 \ttf{item_number} 条 Item 顺序拼接而成。本文所称单条 \textbf{Item},即内容区\textbf{最小不可分}加密单元;\textbf{Content Region 区}即上述全部 Item 按序拼接形成的区域。内容区按块组织,每块由若干连续的 Item 组成。各条 Item 长度随密文载荷变化,Item 之间无固定步长。 \subsubsection{Item 记录}\label{sec:item-record} 每条 Item 由前置长度字段与密文载荷顺序组成,按字节从前到后布局如下: @@ -310,7 +310,7 @@ \subsubsection{\texttt{cipher\_payload} 内部结构}\label{sec:cipher-payload-l \end{enumerate} \textbf{字段语义:} \begin{itemize} - \item \ttf{encrypted_data}:\ttf{AES-128-GCM} 密文;其明文为批包,即本条 Item 内全部规范化输入记录按固定格式拼接后、加密前的那段字节串,加解密参数见第~\ref{sec:crypto-chapter} 节; + \item \ttf{encrypted_data}:\ttf{AES-128-GCM} 密文;其明文为批包 \ttf{B},即本条 Item 对应的整段加密前批包字节串(见第~\ref{sec:batch-package} 节),加解密参数见第~\ref{sec:crypto-chapter} 节; \item \ttf{iv}:本条 Item 的 \ttf{AES-128-GCM} Nonce(常称 IV);供加解密双方标识本次独立 GCM 运算,避免同一密钥下因 Nonce 复用而产出相同密文。长度固定 12 字节,取值由安全随机源逐 Item 生成,见第~\ref{sec:randomness} 节; \item \ttf{ephemeral_public_key}:本 Item 临时密钥对的公钥;采用椭圆曲线型号 \texttt{secp256k1} 的未压缩公钥,去掉首字节 \texttt{0x04} 后保留 \texttt{x||y},长度固定 64 字节; \item \ttf{gcm_tag}:\ttf{AES-128-GCM} 认证标签;长度固定 16 字节,由 GCM 运算得到。 @@ -336,20 +336,21 @@ \subsection{批包布局}\label{sec:batch-package} \end{center} 其中每条 \texttt{record} 为: \begin{center} - \texttt{[record\_len (8B)][nt\_input (record\_len B)]} + \texttt{[record\_len (8B)][unit (record\_len B)]} \end{center} +其中 \texttt{unit} 为一条输入单元的完整字节串(须满足第~\ref{sec:input-normalization} 节)。 字段语义: \begin{itemize} \item \ttf{package_id}:\ttf{UINT32(LE)},当前版本 MUST 为 \texttt{0x82c4e8d8}; - \item \ttf{record_count}:\ttf{UINT64(LE)},批内规范化输入记录条数; - \item \ttf{record_len}:本条记录的 \ttf{nt_input} 总字节数(含 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload}); - \item 批内记录顺序 MUST 与封装侧接收业务载荷的顺序一致。 + \item \ttf{record_count}:\ttf{UINT64(LE)},批内输入单元条数; + \item \ttf{record_len}:本条 \texttt{unit} 的字节长度(\ttf{UINT64(LE)}); + \item 批内记录顺序 MUST 与封装侧提交输入单元的全局顺序一致。 \end{itemize} \textbf{构造与拆分:} \begin{itemize} - \item \textbf{构造}:将若干条 \ttf{nt_input} 按上述布局首尾拼接为 \ttf{B}; - \item \textbf{拆分}:从 \ttf{B} 解析各字段并依次取出完整 \ttf{nt_input};\ttf{package_id}、长度字段与剩余字节不一致时 MUST 判为格式错误。 + \item \textbf{构造}:将若干条输入单元按上述布局首尾拼接为 \ttf{B}; + \item \textbf{拆分}:从 \ttf{B} 解析各字段并依次取出各条 \texttt{unit};\ttf{package_id}、长度字段与剩余字节不一致时 MUST 判为格式错误。 \end{itemize} 实现行为 MUST 与本节布局一致;与参考实现源码符号的对照见附录~\ref{app:ref-impl-symbols}。 @@ -372,6 +373,8 @@ \subsection{规范性解析流程} \item 每个 Item 执行长度合法性检查和密码学认证检查; \item 当解析 Item 数量达到 \ttf{item_number} 时结束并进入完整性校验阶段。 \end{enumerate} +全量顺序解封时,上述 MUST 条款与第~\ref{sec:unseal-chapter} 章参考流程及第~\ref{sec:unseal-total-algorithm} 节应对齐;块级随机访问见第 9 章。 + \subsection{规范性校验规则} \begin{itemize} \item \textbf{计数校验}:实际解析 Item 数 MUST 等于 \ttf{item_number}; @@ -552,7 +555,7 @@ \subsection{内容加密}\label{sec:content-encryption} 该步骤与第~\ref{sec:seal-chapter} 章「业务输入」中的 \textbf{UTF-8 文本编码}无关: 前者作用于已形成的 \ttf{B}; -后者在更早阶段将 Unicode 文本转为载荷字节串,再进入 \ttf{nt_input} 与 \ttf{B}。 +后者在更早阶段将 Unicode 文本转为载荷字节串,再由集成层表示为输入单元并装入 \ttf{B}。 启用文本输入且采用当前版本 GCM 时,\textbf{二者均须执行}。 \begin{itemize} @@ -597,15 +600,14 @@ \subsection{解封侧密码学操作}\label{sec:decrypt-crypto} \textbf{输出:} \begin{itemize} - \item 认证成功:批包 \ttf{B}; - \item 认证失败:见第~\ref{sec:auth-failure} 节(MUST NOT 输出 \ttf{B})。 + \item 批包 \ttf{B}(认证成功后)。 \end{itemize} \textbf{模块实施步骤:} \begin{enumerate} \item 按下文「解析 \ttf{cipher_payload}」取得密文字段; \item 按第~\ref{sec:curve-keys} 与第~\ref{sec:key-derivation} 节派生会话密钥; - \item 按下文「GCM 解密」与第~\ref{sec:aes-gcm} 节完成认证解密; + \item 按下文「GCM 解密」与第~\ref{sec:aes-gcm} 节完成认证解密;若认证失败(第~\ref{sec:auth-failure} 节),MUST NOT 输出 \ttf{B}; \item 按下文「明文解码」得批包 \ttf{B}。 \end{enumerate} @@ -649,11 +651,11 @@ \subsection{认证失败语义}\label{sec:auth-failure} \section{封装流程(参考)}\label{sec:seal-chapter} 本章给出将业务输入写入 \texttt{.sealed} 文件的\textbf{参考流程}。 -\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;下文各节为信息性步骤说明,除非某条明确要求校验或布局。 +\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;\texttt{.sealed} 内容区的最小不可分单元为 \textbf{Item},输入单元须满足第~\ref{sec:input-normalization} 节规范性要求。下文各节为信息性步骤说明,除非某条明确要求校验或布局。 \textbf{子流程(独立说明):} \begin{itemize} - \item 输入规范化(第~\ref{sec:input-normalization} 节); + \item 标准化输入基本规范(第~\ref{sec:input-normalization} 节); \item 滚动摘要累计(第~\ref{sec:rolling-hash} 节); \item 批包封装与 Item 写出(第~\ref{sec:item-generation} 节); \item 批刷写阈值(第~\ref{sec:batch-flush-threshold} 节); @@ -664,55 +666,28 @@ \section{封装流程(参考)}\label{sec:seal-chapter} \end{itemize} 全局一致性见第~\ref{sec:seal-consistency} 节。 -\subsection{输入规范化}\label{sec:input-normalization} -将宿主提交的业务载荷编码为一条规范化输入记录 \ttf{nt_input}。 +\subsection{标准化输入基本规范}\label{sec:input-normalization} +本标准以 \textbf{Item} 为 \texttt{.sealed} 内容区最小不可分单元(第~\ref{sec:content-region} 节)。宿主逐段提交的逻辑数据,在写入批包前由集成层表示为变长字节串,下文统称\textbf{输入单元}(实现中常记 \ttf{nt_input})。\textbf{本标准不规定}输入单元的具体字节布局;本节仅列出输入单元\textbf{应满足}的规范性要求,以便与批包、滚动摘要及解封输出对齐。如何由 \ttf{payload} 得到输入单元属于集成层实现,不在本节展开。 -\textbf{输入:} -\begin{itemize} - \item 一段载荷字节串,长度为 \ttf{L}(由宿主决定切分策略); - \item 若上层以 Unicode 文本提交,须先经 UTF-8 编码为字节串(见下文说明)。 -\end{itemize} - -\textbf{输出:} -\begin{itemize} - \item 一条 \ttf{nt_input}(完整字节串,长度为 \texttt{12 + L})。 -\end{itemize} - -\textbf{相关格式:} -\ttf{nt_input} 属于加密前明文侧数据,\texttt{.sealed} 文件内不单独存放其原文。 -当前版本(\ttf{version_number = 2})\ttf{nt_input} 布局为: -\begin{center} - \texttt{[reserved (4B)][payload\_len (8B)][payload (L B)]} -\end{center} +\textbf{规范性要求:} \begin{itemize} - \item \ttf{reserved}(偏移 0--3):MUST 为 \texttt{0x00000000}; - \item \ttf{payload_len}(偏移 4--11):\ttf{UINT64(LE)},MUST 等于 \ttf{L}; - \item \ttf{payload}(偏移 12 起):长度为 \ttf{L} 的载荷字节串。 + \item 封装与解封 MUST 对输入单元采用同一套、且在实现文档中声明的语义; + \item 每条输入单元 MUST 对应一段可还原的业务载荷 \ttf{payload}(还原方式由集成层定义,不属于 \texttt{.sealed} 文件布局); + \item 每条输入单元 MUST 完整地落入一个批包记录体(第~\ref{sec:batch-package} 节),MUST NOT 为凑批而在单元内部截断; + \item 全部输入单元 MUST 具有确定的\textbf{全局总序};该顺序 MUST 与第~\ref{sec:rolling-hash} 节累计顺序及解封侧输出顺序一致; + \item 参与滚动摘要的字节串 MUST 与写入批包记录体 \texttt{unit} 逐字节相同; + \item 输入单元明文不单独写入 \texttt{.sealed} 文件,仅通过加密后的 Item 对外存在。 \end{itemize} -\textbf{实施步骤:} -\begin{enumerate} - \item 若输入为 Unicode 文本,MUST 以 UTF-8 编码为字节串,且 \textbf{MUST NOT} 在首部附带 BOM(\texttt{EF BB BF});若以原始二进制流提交,跳过本步; - \item 按上文布局写入 \ttf{reserved}、\ttf{payload_len} 与 \ttf{payload},得到一条 \ttf{nt_input}。 -\end{enumerate} - \textbf{说明:} -\begin{itemize} - \item \textbf{宿主 / 集成方}决定每次提交的载荷字节串长度 \ttf{L},从而决定本条 \ttf{nt_input} 长度为 \texttt{12 + L};本标准不规定 \ttf{L} 的切分策略; - \item 同一业务输入在不同实现中 MUST 产生逐字节相同的 \ttf{nt_input};本标准不规定 \ttf{payload} 业务语义; - \item UTF-8 属于应用层字符编码,发生在 \ttf{nt_input} 之前;批包进入第~\ref{sec:content-encryption} 节前的编码与 - UTF-8 \textbf{不是}同一步; - \item 封装实现 MUST NOT 在载荷外附加未在本节定义的额外前缀或后缀; - \item 解封侧从完整 \ttf{nt_input} 校验字段后提取 \ttf{payload},不一致时 MUST 判为格式错误; - \item 实现符号对照见附录~\ref{app:ref-impl-symbols}。 -\end{itemize} +\ttf{payload} 的业务语义、字符编码(如 UTF-8)及切分粒度由应用层决定。Meta-Encryptor 的 \ttf{toNtInput}/\ttf{fromNtInput} 仅为一种集成示例(见附录~\ref{app:ref-impl-symbols}),\textbf{不是}本标准的必选布局。 \subsection{滚动摘要累计}\label{sec:rolling-hash} -在封装过程中按顺序累计全部 \ttf{nt_input} 的滚动摘要,终值写入 Header.\ttf{data_hash}。 +在封装过程中按顺序累计全部输入单元的滚动摘要,终值写入 Header.\ttf{data_hash}。 \textbf{输入:} \begin{itemize} - \item 按封装顺序产生的各条 \ttf{nt_input} 完整字节串(含 12 字节头,布局见第~\ref{sec:input-normalization} 节); + \item 按封装全局顺序产生的各条输入单元完整字节串(与第~\ref{sec:batch-package} 节记录体 \texttt{unit} 逐字节一致,见第~\ref{sec:input-normalization} 节); \item 初值 $H_0$(当前版本见下文)。 \end{itemize} @@ -722,25 +697,25 @@ \subsection{滚动摘要累计}\label{sec:rolling-hash} \end{itemize} \textbf{相关格式:} -Header.\ttf{data_hash} 字段定义见第~\ref{sec:header-region} 节;累计与 Item 边界无关,仅依赖 \ttf{nt_input} 全局顺序。 +Header.\ttf{data_hash} 字段定义见第~\ref{sec:header-region} 节;累计与 Item 边界无关,仅依赖输入单元全局顺序(非 Item 条数)。 \textbf{实施步骤:} 当前版本 (\ttf{version_number = 2}) 规则如下: \begin{center} \texttt{H\_0 = keccak256("Fidelius")} \\ - \texttt{H\_{i+1} = keccak256(H\_i || nt\_input\_i)} + \texttt{H\_{i+1} = keccak256(H\_i || unit\_i)} \end{center} -记共产生 \ttf{n} 条 \ttf{nt_input},终态为 $H_n$。\textbf{\rollhash 终值}\label{sec:rolling-hash-final}定义为 $H_n$;封装完成时 Header.\ttf{data_hash} MUST 等于 $H_n$。 +其中 \texttt{unit\_i} 为全局顺序下第 $i$ 条输入单元的完整字节串。记共产生 $N$ 条输入单元,终态为 $H_n$。\textbf{\rollhash 终值}\label{sec:rolling-hash-final}定义为 $H_n$;封装完成时 Header.\ttf{data_hash} MUST 等于 $H_n$。 \textbf{说明:} -每得一条 \ttf{nt_input} 可按本节规则更新摘要状态;终值 $H_n$ 在收尾写入 Header(第~\ref{sec:seal-finalize} 节)。何时调用本节见第~\ref{sec:seal-total-algorithm} 节;解封侧比对见第~\ref{sec:integrity-check} 节。 +本节规定递推规则;封装主循环中 $H$ 的初值与更新时机见第~\ref{sec:seal-total-algorithm} 节。每得一条输入单元可按本节规则更新摘要状态;终值 $H_n$ 在收尾写入 Header(第~\ref{sec:seal-finalize} 节)。解封侧比对见第~\ref{sec:integrity-check} 节。 \subsection{批包封装与 Item 写出}\label{sec:item-generation} -将\textbf{同一批次}的多条 \ttf{nt_input} 打成批包 \ttf{B},加密并在内容区追加\textbf{一条} Item 记录。 +将\textbf{同一批次}的多条输入单元打成批包 \ttf{B},加密并在内容区追加\textbf{一条} Item 记录(Item 为内容区最小不可分单元)。 \textbf{输入:} \begin{itemize} - \item 若干条完整的 \ttf{nt_input}(条数 $\ge 1$,批内顺序 MUST 与拼接顺序一致); + \item 若干条完整的输入单元(条数 $\ge 1$,批内顺序 MUST 与全局提交顺序一致); \item 接收方长期公钥(应用层提供,见第~\ref{sec:curve-keys} 节)。 \end{itemize} @@ -751,13 +726,13 @@ \subsection{批包封装与 Item 写出}\label{sec:item-generation} \textbf{相关格式:} \begin{itemize} - \item 若干 \ttf{nt_input} 首尾拼接为批包 \ttf{B}(布局见第~\ref{sec:batch-package} 节); + \item 若干输入单元首尾拼接为批包 \ttf{B}(布局见第~\ref{sec:batch-package} 节); \item 一个 \ttf{B} 对应内容区\textbf{一条} Item:\texttt{UINT64(LE) item\_size} 后跟 \ttf{cipher_payload}(见第~\ref{sec:item-record}、\ref{sec:cipher-payload-layout} 节)。 \end{itemize} \textbf{实施步骤:} \begin{enumerate} - \item 将输入的各条 \ttf{nt_input} 按第~\ref{sec:batch-package} 节首尾拼接,得到批包 \ttf{B}; + \item 将输入的各条输入单元按第~\ref{sec:batch-package} 节首尾拼接,得到批包 \ttf{B}; \item 对 \ttf{B} 与接收方长期公钥调用第~\ref{sec:content-encryption} 节内容加密模块,得到 \ttf{item_size} 与 \ttf{cipher_payload}; \item 按第~\ref{sec:item-record} 节向内容区追加 \texttt{UINT64(LE) item\_size} 与 \ttf{cipher_payload}。 \end{enumerate} @@ -767,7 +742,7 @@ \subsection{批包封装与 Item 写出}\label{sec:item-generation} 向内容区追加顺序见第~\ref{sec:seal-consistency} 节;写出后更新 Header 计数与 Block Info 见第~\ref{sec:block-meta} 节。 \subsection{批刷写阈值}\label{sec:batch-flush-threshold} -规定封装实现\textbf{何时}将当前累积的一批 \ttf{nt_input} 作为同一批次,调用第~\ref{sec:item-generation} 节打成 \ttf{B} 并写出 Item;队列维护与调用顺序见第~\ref{sec:seal-total-algorithm} 节。 +规定封装实现\textbf{何时}将当前累积的一批输入单元作为同一批次,调用第~\ref{sec:item-generation} 节打成 \ttf{B} 并写出 Item;队列维护与调用顺序见第~\ref{sec:seal-total-algorithm} 节。 \textbf{参数:} \begin{itemize} @@ -775,29 +750,25 @@ \subsection{批刷写阈值}\label{sec:batch-flush-threshold} \end{itemize} \textbf{度量:} -设当前\textbf{待打包批次}(尚未送入第~\ref{sec:item-generation} 节的 \ttf{nt_input} 集合)为 $\mathcal{N}$。 +设当前\textbf{待打包批次}(尚未送入第~\ref{sec:item-generation} 节的输入单元集合)为 $\mathcal{N}$。 记 \[ - S_{\mathrm{batch}} = \sum_{n \in \mathcal{N}} |n| + S_{\mathrm{batch}} = \sum_{u \in \mathcal{N}} |u| \] -其中 $|n|$ 为 \ttf{nt_input} 的\textbf{完整编码字节长度}(含 12 字节头,即 \texttt{12 + L},布局见第~\ref{sec:input-normalization} 节)。 +其中 $|u|$ 为输入单元的\textbf{完整字节长度}(与第~\ref{sec:batch-package} 节 \ttf{record_len} 一致)。 第~\ref{sec:seal-total-algorithm} 节队列 \ttf{Q} 上的 $S(Q)$ 与本节 $S_{\mathrm{batch}}$ 采用同一度量。 \textbf{刷写条件:} \begin{enumerate} \item \textbf{阈值触达:}当 $S_{\mathrm{batch}} \ge T_{\mathrm{batch}}$ 时,封装实现 MUST 对 $\mathcal{N}$ 中\textbf{全部}条目执行一次第~\ref{sec:item-generation} 节,并清空该待打包批次; - \item \textbf{输入结束:}宿主声明输入流结束时,若 $\mathcal{N}$ 非空,封装实现 MUST 再执行一次第~\ref{sec:item-generation} 节(即使 $S_{\mathrm{batch}} < T_{\mathrm{batch}}$)。 + \item \textbf{输入结束:}宿主声明输入流结束时,若 $\mathcal{N}$ 非空,封装实现 MUST 再执行一次第~\ref{sec:item-generation} 节(即使 $S_{\mathrm{batch}} < T_{\mathrm{batch}}$); + \item 单条输入单元 MUST 完整地属于一个批次;MUST NOT 为凑满 \ttf{T_batch} 而在某条输入单元内部截断。 \end{enumerate} -\textbf{完整性约束:} -\begin{itemize} - \item 单条 \ttf{nt_input} MUST 完整地属于一个批次;MUST NOT 为凑满 \ttf{T_batch} 而在某条 \ttf{nt_input} 内部截断载荷或头字段。 -\end{itemize} - \textbf{说明:} \ttf{L}、\ttf{T_batch} 均不在 Header 中编码。 参考实现取 \ttf{T_batch = 65536}(\ttf{MaxItemSize});若变更阈值导致批次划分或 Item 数量变化,\ttf{version_number} MUST 递增。 -若宿主按固定块大小 \ttf{T_chunk} 提交载荷且 \ttf{T_chunk = T_batch},常见为每批 1 条 \ttf{nt_input};\ttf{L} 较小时一批可含多条 \ttf{nt_input}(附录~\ref{app:ref-impl-symbols})。 +若宿主按固定块大小 \ttf{T_chunk} 提交载荷且 \ttf{T_chunk = T_batch},常见为每批 1 条输入单元;\ttf{L} 较小时一批可含多条输入单元(附录~\ref{app:ref-impl-symbols})。 \subsection{Block 组织与切分}\label{sec:block-split} 本节说明内容区中的 Block 如何由 Item 构成及何时切分。 @@ -823,7 +794,7 @@ \subsection{Block 组织与切分}\label{sec:block-split} \textbf{说明:} \ttf{N_block} 由封装实现声明,\textbf{不}写入 \texttt{.sealed} 文件;变更且影响互操作时 \ttf{version_number} MUST 递增。 -宿主 \textbf{MAY} 将多段业务数据编码为多条 \ttf{nt_input} 并写入同一 \texttt{.sealed} 内容区;Block Info 不记录「第几个源文件」,仅索引本文件内 Item 与密文字节。 +宿主 \textbf{MAY} 将多段业务数据表示为多条输入单元并写入同一 \texttt{.sealed} 内容区(经若干 Item);Block Info 不记录「第几个源文件」,仅索引本文件内 Item 与密文字节。 \subsection{Block 元信息维护}\label{sec:block-meta} 每写出一条 Item 后,封装实现 MUST 按第~\ref{sec:block-split} 节的划分规则,更新 Header.\ttf{item_number} 与内存中的 Block Info 列表,供第~\ref{sec:seal-finalize} 节写入 Block Infos 区。 @@ -848,7 +819,7 @@ \subsubsection{Block Info 字段含义}\label{sec:block-meta-fields} \item 含义:本 Block 在内容区 Item \textbf{逻辑序号}上的半开区间 \texttt{[start\_item\_index, end\_item\_index)}。按封装写出顺序,自 \texttt{0} 起为第 0 条 Item、第 1 条 Item……直至 \texttt{item\_number - 1}(与 Header.\ttf{item_number} 一致)。 \item \ttf{start_item_index}:该 Block 内\textbf{第一条} Item 的下标(\textbf{含})。 \item \ttf{end_item_index}:该 Block 内\textbf{最后一条} Item 的下标加 1(\textbf{不含});Block 内 Item 条数为 \texttt{end\_item\_index - start\_item\_index},MUST NOT 大于 \ttf{N_block};当该值已达 \ttf{N_block} 时,下一条写出的 Item MUST 按第~\ref{sec:block-split} 节开启新 Block; - \item \textbf{不是}:源明文文件编号、源文件内块号、\ttf{nt_input} 的全局序号(一条 Item 的批包内可含多条 \ttf{nt_input},见第~\ref{sec:batch-package} 节),亦 \textbf{不是} 其它 \texttt{.sealed} 文件中的 Item 下标。 + \item \textbf{不是}:源明文文件编号、源文件内块号、批包内输入单元的序号(一条 Item 的 \ttf{B} 内可含多条记录,见第~\ref{sec:batch-package} 节),亦 \textbf{不是} 其它 \texttt{.sealed} 文件中的 Item 下标。 \end{itemize} \textbf{内容区字节偏移:\ttf{start_file_pos}、\ttf{end_file_pos}。} @@ -952,16 +923,16 @@ \subsection{封装流程总览}\label{sec:seal-total-algorithm}\label{sec:batch- \textbf{变量与状态(仅在本节及图中使用):} \begin{itemize} - \item 待刷写队列 \ttf{Q}:元素为完整的 \ttf{nt_input}; - \item $S(Q)$:\ttf{Q} 中各 \ttf{nt_input} 编码后总字节数(与第~\ref{sec:batch-flush-threshold} 节中 $S_{\mathrm{batch}}$ 同型,用于阈值判断); - \item $H$:滚动摘要状态,初值 $H_0$、终值 $H_n$(规则见第~\ref{sec:rolling-hash} 节)。 + \item 待刷写队列 \ttf{Q}:元素为完整的输入单元; + \item $S(Q)$:\ttf{Q} 中各输入单元字节总长度(与第~\ref{sec:batch-flush-threshold} 节中 $S_{\mathrm{batch}}$ 同型,用于阈值判断); + \item $H$:滚动摘要状态,初值 $H_0$、终值 $H_n$(递推规则见第~\ref{sec:rolling-hash} 节,在主循环中按每条输入单元更新)。 \end{itemize} \textbf{主循环图与子节对应:} 图~\ref{fig:batch-flush-flow} 为全量顺序封装的一种参考主流程;图源 \texttt{doc/diagrams/seal-batch-flush-flow.mmd}。 图中流程名词与独立子节的对应关系如下(图中\textbf{不}标注章节号): \begin{itemize} - \item 「输入规范化」「滚动摘要更新 $H$」:分别见第~\ref{sec:input-normalization}、\ref{sec:rolling-hash} 节;「\ttf{nt_input} 入队 \ttf{Q}」指将完整 \ttf{nt_input} 入 \ttf{Q}; + \item 「输入单元入队」「滚动摘要更新 $H$」:输入单元须满足第~\ref{sec:input-normalization} 节;摘要规则见第~\ref{sec:rolling-hash} 节;「入队 \ttf{Q}」指将完整输入单元入 \ttf{Q}; \item 「批刷写阈值」:见第~\ref{sec:batch-flush-threshold} 节(图中 $S(Q)$ 与 \ttf{T_batch} 同该节); \item 「Item 写出」:见第~\ref{sec:item-generation} 节;「Block 元信息更新」:见第~\ref{sec:block-meta} 节; \item 「收尾落盘」:见第~\ref{sec:seal-finalize} 节。 @@ -978,7 +949,7 @@ \subsection{封装流程总览}\label{sec:seal-total-algorithm}\label{sec:batch- 以下按图~\ref{fig:batch-flush-flow} 自上而下解读;各步具体操作以对应子节为准。 \begin{enumerate} \item \textbf{开始与初始化}(图:\texttt{开始}):\ttf{version_number} 按约定取值;\ttf{block_number} $= 0$,\ttf{item_number} $= 0$;$H \leftarrow H_0$;\ttf{Q} 为空。 - \item \textbf{主循环入口}(图:判断「宿主已声明输入结束且无新载荷」,取\textbf{否}):每收到一段业务载荷,执行图中「输入规范化 / 滚动摘要更新 $H$ / \ttf{nt_input} 入队 \ttf{Q}」框(第~\ref{sec:input-normalization}、\ref{sec:rolling-hash} 节)。 + \item \textbf{主循环入口}(图:判断「宿主已声明输入结束且无新载荷」,取\textbf{否}):每收到一段业务载荷,产生满足第~\ref{sec:input-normalization} 节的输入单元并入 \ttf{Q},按第~\ref{sec:rolling-hash} 节更新 $H$(见图「输入单元入队 / 滚动摘要更新 $H$」)。 \item \textbf{批刷写判断}(图:判断 $S(Q) \ge T_{\mathrm{batch}}$):若为\textbf{是},执行「Item 写出 / Block 元信息更新 / 清空 \ttf{Q}」(第~\ref{sec:item-generation}、\ref{sec:block-meta} 节),返回步骤 2 的判断;若为\textbf{否},直接返回步骤 2 的判断。 \item \textbf{输入结束}(图:步骤 2 判断取\textbf{是}后,判断「\ttf{Q} 非空」):若为\textbf{是},再执行一次 Item 写出与 Block 元信息更新并清空 \ttf{Q};若为\textbf{否},进入步骤 5。 \item \textbf{收尾落盘}(图:「收尾落盘」$\rightarrow$ \texttt{结束}):按第~\ref{sec:seal-finalize} 节写出 Block Infos 与 Header,\ttf{data_hash} $\leftarrow H_n$。 @@ -995,7 +966,7 @@ \subsection{封装一致性要求}\label{sec:seal-consistency} \section{解封流程(参考)}\label{sec:unseal-chapter} 本章给出从 \texttt{.sealed} 文件按序还原业务载荷的\textbf{参考流程}。 -\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;下文各节为信息性步骤说明,除非某条明确要求校验或布局。 +\textbf{文件格式与可验证约束}以第~\ref{sec:format-chapter}、\ref{sec:crypto-chapter} 章为准;解封以 Item 为最小读取与认证单元,\ttf{payload} 由集成层从批包内输入单元还原,且须满足第~\ref{sec:input-normalization} 节与封装侧同一集成语义。第~\ref{sec:format-chapter} 章「规范性解析流程」中的 MUST 条款在全量顺序解封时与本章一致,本章侧重步骤组合与图示。下文各节为信息性步骤说明,除非某条明确要求校验或布局。 \textbf{子流程(独立说明):} \begin{itemize} @@ -1048,7 +1019,7 @@ \subsection{内容区顺序读取}\label{sec:unseal-item-parse} 与第~\ref{sec:unseal-total-algorithm} 节主循环共享,本节负责在读每条 Item 后更新: \begin{itemize} \item $p$:内容区读指针(相对内容区起点,单位:字节;初值 $p \leftarrow 0$ 见第~\ref{sec:unseal-total-algorithm} 节); - \item $n$:已处理 Item 计数(供主循环终止判断及完整性比对;初值 $n \leftarrow 0$ 见同上)。 + \item $n$:已处理 Item 计数(供主循环终止判断及完整性比对;初值 $n \leftarrow 0$ 见同上;\textbf{勿}与第~\ref{sec:rolling-hash} 节中输入单元条数 $N$ 混淆)。 \end{itemize} \textbf{输出:} @@ -1064,8 +1035,8 @@ \subsection{内容区顺序读取}\label{sec:unseal-item-parse} \item 若 \ttf{content_size} $-$ $p < 8$,MUST 终止本条 Item 读取,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_TRUNCATED}; \item 自偏移 $p$ 起读取 8 字节,解码为 $L$;若 $L$ 非法(如为 \texttt{0} 或超出实现上限),MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_SIZE_INVALID}; \item 若 \ttf{content_size} $-$ $p < 8 + L$,MUST 终止本条 Item 读取,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_TRUNCATED}; - \item 自偏移 $p + 8$ 起读取 $L$ 字节,得到 \ttf{cipher_payload};若其布局不满足第~\ref{sec:cipher-payload-layout} 节,MUST 失败(格式错误); - \item 更新 $p \leftarrow p + 8 + L$、$n \leftarrow n + 1$;当 $n = \ttf{item_number}$ 时,若 $p \ne \ttf{content_size}$,MUST 失败(格式错误)。 + \item 自偏移 $p + 8$ 起读取 $L$ 字节,得到 \ttf{cipher_payload};若其布局不满足第~\ref{sec:cipher-payload-layout} 节,MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_ITEM_SIZE_INVALID}; + \item 更新 $p \leftarrow p + 8 + L$、$n \leftarrow n + 1$;当 $n = \ttf{item_number}$ 时,若 $p \ne \ttf{content_size}$,MUST 终止解封(格式错误,如 \ttf{E_ITEM_TRUNCATED} 或实现自定义码)。 \end{enumerate} \textbf{说明:} @@ -1096,33 +1067,32 @@ \subsection{单条 Item 认证解密}\label{sec:unseal-item-crypto} 算法细节见第~\ref{sec:decrypt-crypto} 节;本章不重复模块内部步骤。 \subsection{批包拆分、业务输出与滚动摘要}\label{sec:unseal-batch-output}\label{sec:unseal-rolling-hash} -将批包 \ttf{B} 拆为若干条 \ttf{nt_input},在输出每条 \ttf{payload} 之前用对应 \ttf{nt_input} 更新滚动摘要 $H$,并按序输出 \ttf{payload}。 - -\textbf{维护的状态:} -$H$:滚动摘要状态($H_0$ 见第~\ref{sec:rolling-hash} 节;初值与调度见第~\ref{sec:unseal-total-algorithm} 节)。每处理完本条 Item 内全部 \ttf{nt_input} 后,按实施步骤更新 $H$。 +将一条 Item 解密得到的批包 \ttf{B} 拆为若干条输入单元,在输出每条 \ttf{payload} 之前用对应输入单元(与封装侧记录体逐字节一致)更新滚动摘要 $H$,并按序输出 \ttf{payload}。 \textbf{输入(每次调用):} 批包 \ttf{B}(由第~\ref{sec:unseal-item-crypto} 节得到)。 +\textbf{维护的状态:} +$H$:滚动摘要状态($H_0$ 见第~\ref{sec:rolling-hash} 节;初值与调度见第~\ref{sec:unseal-total-algorithm} 节)。每处理完本条 Item 的 \ttf{B} 内全部输入单元后,按实施步骤更新 $H$。 + \textbf{输出(每次调用):} 按批内顺序输出的各条 \ttf{payload}。 \textbf{相关格式:} -\ttf{B} 布局见第~\ref{sec:batch-package} 节;\ttf{nt_input} 与 \ttf{payload} 提取规则见第~\ref{sec:input-normalization} 节;$H$ 递推规则与 $H_0$ 定义见第~\ref{sec:rolling-hash}、\ref{sec:rolling-hash-final} 节。 +\ttf{B} 布局见第~\ref{sec:batch-package} 节;输入单元须满足第~\ref{sec:input-normalization} 节;$H$ 递推规则与 $H_0$ 定义见第~\ref{sec:rolling-hash}、\ref{sec:rolling-hash-final} 节。 \textbf{实施步骤:} \begin{enumerate} - \item 按第~\ref{sec:batch-package} 节将 \ttf{B} 拆为若干条 \ttf{nt_input};若 \ttf{package_id}、长度字段与剩余字节不一致,MUST 终止解封(格式错误),并 MUST NOT 输出 \ttf{payload}; - \item 对批内每条 \ttf{nt_input} 按序执行下列子步骤(\textbf{不得}在输出 \ttf{payload} 后再用该条 \ttf{nt_input} 更新 $H$): + \item 按第~\ref{sec:batch-package} 节将 \ttf{B} 拆为若干条输入单元(\texttt{unit});若 \ttf{package_id}、长度字段与剩余字节不一致,MUST 终止解封(格式错误),并 MUST NOT 输出 \ttf{payload}; + \item 对批内每条输入单元按序执行下列子步骤(\textbf{不得}在输出 \ttf{payload} 后再用该条单元更新 $H$): \begin{enumerate} - \item 按第~\ref{sec:input-normalization} 节校验布局;失败时 MUST 终止解封(格式错误); - \item 在输出 \ttf{payload} 之前,用本条 \ttf{nt_input} 完整字节串(含 12 字节头)按第~\ref{sec:rolling-hash} 节更新 $H$; - \item 提取 \ttf{payload} 并输出。 + \item 在输出 \ttf{payload} 之前,用本条输入单元完整字节串按第~\ref{sec:rolling-hash} 节更新 $H$; + \item 由集成层将输入单元还原为 \ttf{payload} 并输出(语义须与封装侧一致,见第~\ref{sec:input-normalization} 节);失败时 MUST 终止解封(格式错误)。 \end{enumerate} \end{enumerate} \textbf{说明:} -\ttf{nt_input} 仅在本节步骤 2 内存在;$H$ 的累计顺序 MUST 与封装侧 \ttf{nt_input} 全局顺序一致。完整性比对见第~\ref{sec:unseal-total-algorithm} 节。 +输入单元仅在本节步骤 2 内存在;$H$ 的累计顺序 MUST 与封装侧输入单元全局顺序一致。完整性比对见第~\ref{sec:unseal-total-algorithm} 节。 \subsection{解封流程总览}\label{sec:unseal-total-algorithm}\label{sec:unseal-main-flow} 本节定义解封侧\textbf{如何组合}前述子流程。各子节仅描述单步职责;本节给出调度变量、主循环参考图及\textbf{按图解读}的实施步骤(含完整性比对)。 @@ -1139,9 +1109,9 @@ \subsection{解封流程总览}\label{sec:unseal-total-algorithm}\label{sec:unse 图中流程名词与独立子节的对应关系如下(图中\textbf{不}标注章节号;\ttf{data_hash} 等字段语义以正文为准): \begin{itemize} \item 「读 Header 并校验」「计算 \ttf{content_size}」:见第~\ref{sec:unseal-header-model} 节;图中「初始化 $H = H_0$,$n = 0$」另需 $p \leftarrow 0$($H_0$ 见第~\ref{sec:rolling-hash} 节); - \item 「读 \ttf{item_size} 与 \ttf{cipher_payload}」:见第~\ref{sec:unseal-item-parse} 节; + \item 「读 \ttf{item_size} 与 \ttf{cipher_payload}」:见第~\ref{sec:unseal-item-parse} 节(含 $n$ 递增,图中无单独「$n$ 加 1」节点); \item 「认证解密得批包 \ttf{B}」:见第~\ref{sec:unseal-item-crypto} 节; - \item 「拆分 \ttf{B}」「逐条输出 \ttf{payload} 并用 \ttf{nt_input} 更新 $H$」:见第~\ref{sec:unseal-batch-output} 节(须在输出 \ttf{payload} 之前更新 $H$); + \item 「拆分 \ttf{B}」「逐条输出 \ttf{payload} 并用输入单元更新 $H$」:见第~\ref{sec:unseal-batch-output} 节(须在输出 \ttf{payload} 之前更新 $H$); \item 「$H$ 等于 \ttf{data_hash}」:见下文实施步骤第 3 项(完整性比对)。 \end{itemize} @@ -1156,7 +1126,7 @@ \subsection{解封流程总览}\label{sec:unseal-total-algorithm}\label{sec:unse 以下按图~\ref{fig:unseal-main-flow} 自上而下解读;各步具体操作以对应子节为准。 \begin{enumerate} \item \textbf{开始与 Header}(图:\texttt{开始} $\rightarrow$ 「读 Header 并校验」$\rightarrow$ 「计算 \ttf{content_size} / 初始化」):按第~\ref{sec:unseal-header-model} 节取得并校验 Header;$H \leftarrow H_0$,$p \leftarrow 0$,$n \leftarrow 0$。 - \item \textbf{主循环}(图:判断 $n$ 小于 \ttf{item_number},取\textbf{是}):以当前 $p$、$n$ 调用第~\ref{sec:unseal-item-parse} 节得 \ttf{cipher_payload} 及更新后的 $p$、$n$;调用第~\ref{sec:unseal-item-crypto} 节得 \ttf{B};以当前 $H$ 调用第~\ref{sec:unseal-batch-output} 节输出 \ttf{payload} 并得到更新后的 $H$(对应图中「读 Item $\rightarrow$ 认证解密 $\rightarrow$ 拆分并输出」链);$n$ 加 1 后返回本步判断。认证或格式失败时 MUST 终止(图虚线至错误结束)。 + \item \textbf{主循环}(图:判断 $n$ 小于 \ttf{item_number},取\textbf{是}):以当前 $p$、$n$ 调用第~\ref{sec:unseal-item-parse} 节得 \ttf{cipher_payload} 及更新后的 $p$、$n$($n$ 仅在该节末项递增);调用第~\ref{sec:unseal-item-crypto} 节得 \ttf{B};以当前 $H$ 调用第~\ref{sec:unseal-batch-output} 节输出 \ttf{payload} 并得到更新后的 $H$(对应图中「读 Item $\rightarrow$ 认证解密 $\rightarrow$ 拆分并输出」链);然后返回本步判断。认证或格式失败时 MUST 终止(图虚线至错误结束)。 \item \textbf{完整性比对}\label{sec:integrity-check}(图:$n$ 判断取\textbf{否} $\rightarrow$ 「$H$ 等于 \ttf{data_hash}」):将 $H$ 与 \ttf{data_hash} 逐字节比较;相等则成功结束,不等则 MUST 终止解封,错误码 SHOULD 为附录 C 中的 \ttf{E_INTEGRITY_MISMATCH},并视已输出的 \ttf{payload} 为不可信;流式输出时 SHOULD 停止下游消费并上报错误。 \end{enumerate} @@ -1166,7 +1136,7 @@ \subsection{错误处理}\label{sec:unseal-errors} \item \ttf{magic_number} 或 \ttf{version_number} 校验失败(第~\ref{sec:header-compat} 节); \item \ttf{content_size} 非法,或第~\ref{sec:unseal-item-parse} 节读取 Item 时字节不足或布局非法; \item 第~\ref{sec:decrypt-crypto} 节内容解密还原模块认证失败(第~\ref{sec:auth-failure} 节); - \item 第~\ref{sec:unseal-batch-output} 节批包拆分或 \ttf{nt_input} 校验失败; + \item 第~\ref{sec:unseal-batch-output} 节批包拆分或输入单元解码失败; \item 第~\ref{sec:unseal-total-algorithm} 节完整性比对中 $H$ 与 \ttf{data_hash} 不一致。 \end{itemize} @@ -1359,9 +1329,9 @@ \subsection{B.1 封装伪代码(规范性参考)} block_infos = [] batch = [] -for each normalized_record nt_input from plaintext_stream: - batch.append(nt_input) - H = keccak256(H || nt_input) +for each input_unit from plaintext_stream: + batch.append(input_unit) + H = keccak256(H || input_unit) if batch_size_reach_threshold(): pkg = batch2ntpackage(batch) cipher_payload = encrypt_item(pkg, receiver_public_key, prefix=0x02) @@ -1392,9 +1362,9 @@ \subsection{B.2 解封伪代码(规范性参考)} cipher_payload = read_exact(item_size) B = decrypt_item_to_batch(cipher_payload, private_key) // 见第 6.5 节,输出 B batch = ntpackage2batch(B) - for each nt_input in batch: - emit fromNtInput(nt_input) - H = keccak256(H || nt_input) + for each input_unit in batch: + emit decode_input_unit(input_unit) // 集成层解码,见第 7.1 节 + H = keccak256(H || input_unit) read_item_count += 1 if H != header.data_hash: @@ -1480,10 +1450,10 @@ \subsection{D.2 封装流程表述与源码符号对照(信息性)}\label{ap 本标准表述(第~\ref{sec:seal-chapter} 章) & 参考实现符号(Node) & 说明 \\ \midrule \endhead - 封装侧编码 & \ttf{toNtInput} & 载荷字节串 $\to$ 一条 \ttf{nt_input} \\ - 解封侧解码 & \ttf{fromNtInput} & 一条 \ttf{nt_input} $\to$ \ttf{payload} \\ - 批包构造 & \ttf{batch2ntpackage} & 若干 \ttf{nt_input} $\to$ 批包 \ttf{B} \\ - 批包拆分 & \ttf{ntpackage2batch} & 批包 \ttf{B} $\to$ \ttf{nt_input} 列表 \\ + 集成层表示(示例) & \ttf{toNtInput} & \ttf{payload} $\to$ 输入单元;\textbf{非}本标准必选布局 \\ + 集成层还原(示例) & \ttf{fromNtInput} & 输入单元 $\to$ \ttf{payload};\textbf{非}本标准必选布局 \\ + 批包构造 & \ttf{batch2ntpackage} & 若干输入单元 $\to$ 批包 \ttf{B}(第~\ref{sec:batch-package} 节) \\ + 批包拆分 & \ttf{ntpackage2batch} & 批包 \ttf{B} $\to$ 输入单元列表 \\ 流式封装入口(信息性) & \ttf{Sealer} & \ttf{src/Sealer.js};按 \ttf{T_chunk} 切分载荷 \\ 批刷写阈值 \ttf{T_batch} & \ttf{MaxItemSize} & \ttf{src/limits.js},参考取值 65536 \\ Block 内 Item 数上限 \ttf{N_block} & \ttf{ItemNumPerBlockLimit} & \ttf{src/blockfile.js} 构造参数,参考取值 256 \\ From 5dce9edb104100991af381fda074c4ba8b371c57 Mon Sep 17 00:00:00 2001 From: freeof123 Date: Mon, 25 May 2026 11:30:47 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/diagrams/seal-batch-flush-flow.mmd | 16 ++++++++++++++++ doc/diagrams/seal-batch-flush-flow.png | Bin 0 -> 58696 bytes doc/diagrams/unseal-main-flow.mmd | 19 +++++++++++++++++++ doc/diagrams/unseal-main-flow.png | Bin 0 -> 69470 bytes 4 files changed, 35 insertions(+) create mode 100644 doc/diagrams/seal-batch-flush-flow.mmd create mode 100644 doc/diagrams/seal-batch-flush-flow.png create mode 100644 doc/diagrams/unseal-main-flow.mmd create mode 100644 doc/diagrams/unseal-main-flow.png diff --git a/doc/diagrams/seal-batch-flush-flow.mmd b/doc/diagrams/seal-batch-flush-flow.mmd new file mode 100644 index 0000000..c3d113e --- /dev/null +++ b/doc/diagrams/seal-batch-flush-flow.mmd @@ -0,0 +1,16 @@ +%% 封装主循环(第 7.8 节 封装流程总览) +%% 生成: npm exec --yes --package=@mermaid-js/mermaid-cli -- mmdc -i seal-batch-flush-flow.mmd -o seal-batch-flush-flow.png -b white -w 1200 +flowchart TD + start([开始]) --> A{"宿主已声明输入结束
且无新载荷?"} + + A -->|否| B["输入单元入队
滚动摘要更新 H"] + B --> C{"批刷写阈值
S(Q) ≥ T_batch?"} + C -->|是| D["Item 写出
Block 元信息更新
清空 Q"] + C -->|否| A + D --> A + + A -->|是| E{"Q 非空?"} + E -->|是| D2["Item 写出
Block 元信息更新
清空 Q"] + E -->|否| F["收尾落盘"] + D2 --> F + F --> endnode([结束]) diff --git a/doc/diagrams/seal-batch-flush-flow.png b/doc/diagrams/seal-batch-flush-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..cd0131d114e34b889e9ae563e314b7c661bd4e6a GIT binary patch literal 58696 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 literal 0 HcmV?d00001 diff --git a/doc/diagrams/unseal-main-flow.mmd b/doc/diagrams/unseal-main-flow.mmd new file mode 100644 index 0000000..ac7f7d7 --- /dev/null +++ b/doc/diagrams/unseal-main-flow.mmd @@ -0,0 +1,19 @@ +%% 解封主循环(第 8.5 节 解封流程总览) +%% npx @mermaid-js/mermaid-cli -i unseal-main-flow.mmd -o unseal-main-flow.png -b white -w 1200 +flowchart TD + start([开始]) --> A["读 Header 并校验
magic / version"] + A --> B["计算 content_size
初始化 H = H0,n = 0"] + B --> C{"n 小于 item_number?"} + + C -->|否| D{"H 等于 data_hash?"} + D -->|是| endnode([成功结束]) + D -->|否| err1([完整性错误]) + + C -->|是| E["读 item_size 与 cipher_payload"] + E --> F["认证解密得批包 B"] + F --> G["拆分 B 得输入单元"] + G --> H["逐条输出 payload
并用输入单元更新 H"] + H --> C + + F -.->|失败| err2([认证或格式错误]) + G -.->|失败| err2 diff --git a/doc/diagrams/unseal-main-flow.png b/doc/diagrams/unseal-main-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..ec35a0d2618b72f507366b8d8383c47ac50cbab4 GIT binary patch literal 69470 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