HarmonyOS Node.js crypto:上架隐私文档的多语版本哈希对齐【鸿蒙心迹】
李游
PolicySeal / PUB-2505。
一、把上架素材当作一个版本集合
发布前最容易漏掉的往往不是代码,而是那些被多人协作编辑过的文案文件。产品把中文隐私说明改到了新版本,英文翻译仍停在上一轮,日文文件尚未同步;三份文件都在目录里,看起来十分齐全,但版本并不一致。传统“文件存在就算通过”的检查在这种场景下几乎没有价值。PolicySeal 的目标不是判断条款是否合法,更不是替代 AppGallery 的审核,而是在提交应用信息前对团队指定的三个语言版本做一轮字节级版本对齐预检。它解决的是交付纪律,而不是法规解释。
二、定义可追踪的最小演示
本例内部编号 PUB-2505。团队要求 zh-CN、en-US、ja-JP 三个本地 HTML 文件和一份 privacy-manifest.json 清单对应。预期 zh-CN 与清单哈希一致,en-US 文件存在却与登记哈希不符,ja-JP 文件不存在,因此报告依次为 PASS、HASH_MISMATCH、FILE_MISSING,汇总 1/3 PASS · BLOCKED。这里的三种语言是项目自己的发布范围,不表示平台对所有应用强制规定这三种语言。HTML 只是方便编辑、备份和浏览的团队文件格式;最终上架时应根据当前 AppGallery Connect 界面的真实表单配置隐私政策链接和信息。

图01:文章主题示意封面,合成设计。
三、清单应当先校验自身
一个校验器若连自己读取的清单都没有约束,输出就很难被信任。首先要求 JSON 顶层必须是数组,条目数量匹配本次内部约定的语言集。然后检查 locale 无重复,path 只能是一个由英文字母、数字和连字符组成的 HTML 文件名,sha256 则应当是六十四位小写十六进制。这样做能拦截从其他目录误指过来的文件、重复语言造成的覆盖,也能减少路径穿越风险。严格格式是团队策略而非华为接口规范;如果项目需要子目录或不同资源类型,应重新设计白名单和路径检查,不应该偷偷放宽正则。
下面是第一段核心代码,先划清数据入口和格式责任。
// scripts/check-privacy.mjs: 清单结构与路径约束
import { readFile, lstat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { resolve, relative, sep } from 'node:path';
const root = resolve('release/privacy');
const manifestPath = resolve('release/privacy-manifest.json');
const allowed = new Set(['zh-CN', 'en-US', 'ja-JP']);
async function loadManifest() {
const rows = JSON.parse(await readFile(manifestPath, 'utf8'));
if (!Array.isArray(rows) || rows.length !== allowed.size) {
throw new Error('MANIFEST_SHAPE');
}
const seen = new Set();
for (const row of rows) {
if (!allowed.has(row.locale) || seen.has(row.locale)) {
throw new Error('LOCALE_DUPLICATE_OR_UNKNOWN');
}
if (typeof row.path !== 'string' ||
!/^[a-zA-Z0-9-]+\.html$/.test(row.path) ||
!/^[a-f0-9]{64}$/.test(row.sha256)) {
throw new Error('MANIFEST_FIELD_INVALID');
}
seen.add(row.locale);
}
return rows;
}
allowed 是内部语种范围;正则限制路径和摘要格式。loadManifest 会抛出结构异常,入口捕获后以 BLOCKED 停止,而非使用不完整的清单继续放行。
四、哈希比较究竟证明什么
SHA-256 能回答的只是“文件字节是否与清单登记时相同”,不能回答“条款是否准确、翻译是否完整”。哪怕只改一个空格,也会导致摘要改变,这对发布物料版本锁定很有用,对文案审查却不能当作质量评分。第二段代码先限定实际文件必须仍在根目录以内,再使用 lstat 排除软链接并确认它确实是普通文件,接着 readFile 获取 Buffer,通过 createHash(‘sha256’) 得到十六进制摘要。这里选择原始字节比较,不做换行符和编码规范化,因为规范化会掩盖真正打包内容的差异。若团队要求跨系统统一换行,应在生成清单之前统一格式。
五、报告必须让开发和运营都能看懂
第三段代码负责将单文件判定转换为命令行可读的报告,并通过 process.exitCode 把结果交给内部 CI。输出按固定语言顺序排列,避免并发读取造成日志每次顺序不同。存在任意一项不通过时返回非零退出码,流水线可阻断团队自己的发布准备阶段。这里的“门禁”没有调用 AppGallery 审核系统,也不会改变平台上的发布状态。报告保留明确失败原因比简单打印“失败”更有价值:文件丢失应找到资源负责人,哈希不一致则需要确认修改是否已经审阅并重新登记摘要。
第二段代码收拢跨时序的判断,避免把比较逻辑散落在多个 UI 回调中。
// 对清单的一行执行字节级校验;不解析隐私条款语义
async function verifyRow(row) {
const full = resolve(root, row.path);
const rel = relative(root, full);
if (rel.startsWith('..' + sep) || rel === '..' ||
rel.startsWith(sep)) {
return { locale: row.locale, status: 'PATH_ESCAPE' };
}
try {
const info = await lstat(full);
if (info.isSymbolicLink()) return { locale: row.locale, status: 'SYMLINK_BLOCKED' };
if (!info.isFile()) return { locale: row.locale, status: 'NOT_FILE' };
const data = await readFile(full);
const actual = createHash('sha256').update(data).digest('hex');
return { locale: row.locale,
status: actual === row.sha256 ? 'PASS' : 'HASH_MISMATCH' };
} catch (err) {
if (err?.code === 'ENOENT') {
return { locale: row.locale, status: 'FILE_MISSING' };
}
return { locale: row.locale, status: 'READ_ERROR' };
}
}
verifyRow 返回稳定状态码,不会将权限错误冒充为文件缺失。Buffer 与哈希对象由函数作用域管理,无须长期保留文件内容。
六、清单生成流程也要有责任人
实际协作建议把 manifest 作为经过审阅的发布基线:文案完成后由指定同事生成摘要,另一人核对文件内容及语种,最后和本次版本号一并进入代码评审。若允许任何提交者在 CI 里自动重算并覆盖 sha256,校验器就失去作用,因为被误修改的内容会与刚生成的清单永远相同。项目可以独立写一个“更新基线”工具,但必须由显式操作触发,不能藏在普通校验命令中。摘要证明的是一致性,不提供签名、身份认证或防篡改链;对恶意修改者同时改文件和清单的威胁,需要仓库权限与审批流程来处理。
第三段代码将独立校验组合为实际页面或命令行入口。
// check-privacy.mjs 入口;非零退出码用于内部流水线门禁
async function main() {
let reports;
try {
const rows = await loadManifest();
reports = await Promise.all(rows.map(verifyRow));
} catch (err) {
console.error('[PolicySeal] manifest: BLOCKED', err.message);
process.exitCode = 1;
return;
}
const order = ['zh-CN', 'en-US', 'ja-JP'];
reports.sort((a, b) => order.indexOf(a.locale) - order.indexOf(b.locale));
for (const row of reports) {
console.log(`[PolicySeal] ${row.locale} ${row.status}`);
}
const passed = reports.filter(r => r.status === 'PASS').length;
const finalState = passed === reports.length ? 'PASS' : 'BLOCKED';
console.log(`[PolicySeal] ${passed}/${reports.length} PASS · ${finalState}`);
if (finalState !== 'PASS') process.exitCode = 1;
}
await main();
排序提供固定诊断顺序,process.exitCode 不立即终止还没收尾的异步 I/O。状态码属于内部构建流程,并非 AppGallery 平台返回。

图02:DevEco Studio 风格界面。代码、目录与模拟器区域用于说明设计。
七、错误处理不宜合并成一个状态
FILE_MISSING 代表路径所指资源不存在,HASH_MISMATCH 代表文件存在却与基线不一致,READ_ERROR 可能是权限或 I/O 问题,NOT_FILE 则说明目标不是普通文件,SYMLINK_BLOCKED 意味着路径被替换成了软链接。清单格式不合法时应该立即停止全部校验,而不是悄悄忽略出问题的一行后报告 PASS。对于较大的文件,可考虑 createReadStream 以控制峰值内存;本例隐私文档较小,直接 readFile 更易追踪。异常信息对开发排查很重要,但日志里不应打印整份隐私政策内容;这里仅输出语言和内部状态码,不额外泄露发布内容。

图03:9:16 手机状态画面,展示预期业务结果。
八、把预期输出转化为检查步骤
准备演示时,可以先建立 release/privacy 目录,把三个语言条目写入清单,再把中文文件与摘要设为匹配。接着修改英文文件的一个已审阅单词但不更新清单,把日文文件移走。运行 node scripts/check-privacy.mjs 后,检查输出有 zh-CN PASS、en-US HASH_MISMATCH、ja-JP FILE_MISSING,最后为 1/3 PASS · BLOCKED;退出码预期为一。然后只修复英文并补齐日文,使用经过确认的新摘要更新基线,再检查三项都是 PASS。上述是可重复操作的测试方案;文中纯手机屏幕是生成报告的可视化草图,实际Node.js脚本只在开发机运行,不宣称本次在真实上架项目里获得审核结果。
九、与真实上架流程保持边界
华为开发者平台的发布资料需要依照当前 AppGallery Connect 页面填写,官方公开说明包含应用基本信息、本地化描述与隐私政策链接等工作。PolicySeal 检查的是团队准备阶段的离线源文件,并没有校验线上隐私政策 URL 是否可访问、网页是否允许爬取、是否包含必需的 SDK 数据披露,也不自动判断权限声明合法性。若上线依赖远程 URL,应追加 HTTPS 可达性、跳转路径、缓存与最终页面内容的独立巡检,并在人审环节核对每个地区的政策文本。离线哈希 PASS 和平台审核通过之间不存在必然联系。
十、维护成本和下一次迭代
很多工具第一次使用时都很好看,真正困难的是半年后还愿不愿意维护。PolicySeal 只保留三类固定资产:需要覆盖的语言集合、清单约定、校验报告。将来增加新的地区时,先更新 allowed,再补齐翻译文件及审阅记录,最后产生相应摘要;不应通过减少语言范围让红色报告变绿。把这套规则写进团队交接文档,发布时能更早发现版本分叉,也能让运营、翻译与开发对着同一份结果沟通。它的边界清楚,反而便于成为稳定的工程小工具。另一个经常被忽视的问题是文件编码:同样肉眼可读的中英文文字,采用不同的换行符、Unicode 规范化形式或文件头标记,也会得到不同摘要。团队应先约定 UTF-8、换行风格以及是否保留末尾空行,再在发布基线中登记。对于政策 URL,如果使用内容分发网络,还应确认线上缓存更新节奏,不要把仓库中的最新 HTML 文件错认为用户马上可见的页面。哈希不能代替人工校对、法务审核与线上可达性检查;只有三个环节各自负责,工具的绿色状态才有可解释的意义。
十一、参考资料与核验边界
- 华为 AppGallery 应用发布与隐私政策链接:https://developer.huawei.com/consumer/jp/appgallery/
- 华为隐私说明相关发布材料:https://developer.huawei.com/consumer/es/doc/app/agc-help-release-app-privacy-desc-0000002313477969
- Node.js 官方 crypto.createHash:https://nodejs.org/api/crypto.html
- 哈希比较仅为项目内版本对齐策略,不代表审核平台的一项强制技术规则。
更多推荐





所有评论(0)