- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
81 lines
2.6 KiB
JavaScript
81 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* patch-p1-settings-hmac-sign.js
|
||
*
|
||
* P1.4: 给 settings.json 加 HMAC 签名 (settings.json.sig)
|
||
*
|
||
* 镜像 feature-flags.json.sig 模式 (64 字节 HMAC-SHA256)。
|
||
* 用 .hmac-key 作为密钥(已在 P0.1 收紧 ACL)。
|
||
*
|
||
* 设计:
|
||
* - 不修改 settings.json 本身(避免破坏现有结构)
|
||
* - 仅在并行写出 settings.json.sig
|
||
* - 提供 verify-settings-sig.js 验证工具
|
||
*
|
||
* 后续: 启动期 hook 应读 .sig 比对,发现篡改告警/拒载
|
||
*/
|
||
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
|
||
const ROOT = path.join(__dirname, '..', '..');
|
||
const SETTINGS = path.join(ROOT, 'settings.json');
|
||
const SIG_FILE = path.join(ROOT, 'settings.json.sig');
|
||
const HMAC_KEY = path.join(ROOT, '.hmac-key');
|
||
|
||
function main() {
|
||
if (!fs.existsSync(SETTINGS)) {
|
||
process.stderr.write('[ERROR] settings.json not found: ' + SETTINGS + '\n');
|
||
process.exit(1);
|
||
}
|
||
if (!fs.existsSync(HMAC_KEY)) {
|
||
process.stderr.write('[ERROR] .hmac-key not found. Run hmac-init first.\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
const key = fs.readFileSync(HMAC_KEY, 'utf8').trim();
|
||
if (key.length < 32) {
|
||
process.stderr.write('[ERROR] .hmac-key too short (' + key.length + ' chars). Need >=32.\n');
|
||
process.exit(1);
|
||
}
|
||
|
||
const content = fs.readFileSync(SETTINGS);
|
||
const sig = crypto.createHmac('sha256', key).update(content).digest('hex');
|
||
|
||
// 幂等
|
||
if (fs.existsSync(SIG_FILE)) {
|
||
const old = fs.readFileSync(SIG_FILE, 'utf8').trim();
|
||
if (old === sig) {
|
||
process.stdout.write('[SKIP] signature unchanged\n');
|
||
process.exit(0);
|
||
}
|
||
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
fs.copyFileSync(SIG_FILE, SIG_FILE + '.bak.' + ts);
|
||
process.stdout.write('[BACKUP] ' + SIG_FILE + '.bak.' + ts + '\n');
|
||
}
|
||
|
||
// 原子写
|
||
const tmpPath = SIG_FILE + '.tmp.' + process.pid;
|
||
fs.writeFileSync(tmpPath, sig);
|
||
fs.renameSync(tmpPath, SIG_FILE);
|
||
|
||
// ACL 收紧(仅当前用户)
|
||
try {
|
||
const { execSync } = require('child_process');
|
||
if (process.platform === 'win32') {
|
||
const user = process.env.USERNAME;
|
||
execSync('icacls "' + SIG_FILE + '" /inheritance:r /grant:r "' + user + ':F"', { stdio: 'ignore' });
|
||
} else {
|
||
fs.chmodSync(SIG_FILE, 0o600);
|
||
}
|
||
} catch (_) { /* best-effort */ }
|
||
|
||
process.stdout.write('[OK] settings.json.sig written (sha256-hmac, ' + sig.length + ' chars)\n');
|
||
process.stdout.write(' ' + sig.slice(0, 16) + '...' + sig.slice(-16) + '\n');
|
||
process.stdout.write(' verify with: node scripts/patches/verify-settings-sig.js\n');
|
||
}
|
||
|
||
if (require.main === module) main();
|