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();
|