bookworm-smart-assistant/scripts/patches/patch-p1-2-jsonl-hmac-lib.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- 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)
2026-04-27 17:59:44 +08:00

297 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* patch-p1-2-jsonl-hmac-lib.js
*
* P1.2 step1: 创建 hooks/lib/jsonl-hmac.js
*
* 提供 jsonl 完整性链 APIHMAC 链 + seq 单调)。
* 不修改 safe-append.js受 tamper protection作为独立可选模块。
*
* 链结构: 每行 line 的 HMAC = SHA256(.hmac-key, prevHmac + lineRawWithoutHmacField)
* baseline 存档: <jsonl>.hmac-baseline.json { sig, lastHmac, lineCount, builtAt }
*
* 使用模式:
* 1) baseline 模式: computeChain(jsonlPath) → 返回链 + 末态
* 2) verify 模式: verifyChain(jsonlPath, baselinePath) → 返回 ok/drift
* 3) seq 校验: assertSeqMonotonic(jsonlPath, seqField='seq')
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', '..', 'hooks', 'lib', 'jsonl-hmac.js');
const SENTINEL = 'P1-JSONL-HMAC-V1';
const CONTENT = `'use strict';
/**
* jsonl-hmac.js — JSONL 完整性链 (${SENTINEL})
*
* 防 evolution-log.jsonl / route-feedback.jsonl 离线投毒。
* 红队识别风险: 攻击者可写 ~/.claude/,无 HMAC 链导致投毒后下个进程消费时被引导。
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ROOT = path.join(__dirname, '..', '..');
const HMAC_KEY_FILE = path.join(ROOT, '.hmac-key');
let _cachedKey = null;
function getKey() {
if (_cachedKey) return _cachedKey;
try {
_cachedKey = fs.readFileSync(HMAC_KEY_FILE, 'utf8').trim();
if (_cachedKey.length < 32) {
throw new Error('.hmac-key too short (' + _cachedKey.length + ' chars)');
}
} catch (e) {
return null;
}
return _cachedKey;
}
/**
* 计算单行 HMAC含前一行 HMAC 形成链)
*/
function hmacLine(prevHmac, line) {
const key = getKey();
if (!key) return null;
return crypto.createHmac('sha256', key).update(prevHmac + '\\n' + line).digest('hex');
}
/**
* 扫描 jsonl 文件计算 HMAC 链
* @returns { ok, lastHmac, lineCount, sig, error?, badLines? }
*/
function computeChain(jsonlPath) {
if (!fs.existsSync(jsonlPath)) {
return { ok: false, error: 'file not found', lineCount: 0 };
}
const key = getKey();
if (!key) return { ok: false, error: 'hmac-key unavailable' };
let prev = '';
let lineCount = 0;
const badLines = [];
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line) continue;
try {
JSON.parse(line);
prev = hmacLine(prev, line);
lineCount++;
} catch (_) {
badLines.push(i + 1);
}
}
// 整体签名 = 最后一行 HMAC (即末态)
return {
ok: badLines.length === 0,
lastHmac: prev,
lineCount: lineCount,
sig: prev,
badLines: badLines.length ? badLines : undefined,
};
}
/**
* 把当前 jsonl 文件的链状态写入 baseline 文件
*/
function writeBaseline(jsonlPath, baselinePath) {
const result = computeChain(jsonlPath);
if (!result.ok && result.error !== 'file not found') {
return { ok: false, error: result.error || 'compute failed' };
}
const baseline = {
schema: 'bookworm-jsonl-hmac-baseline-v1',
target: path.relative(ROOT, jsonlPath),
sig: result.sig,
lineCount: result.lineCount,
builtAt: new Date().toISOString(),
hmacKeyFingerprint: hmacKeyFingerprint(),
};
const dir = path.dirname(baselinePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const tmp = baselinePath + '.tmp.' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(baseline, null, 2));
fs.renameSync(tmp, baselinePath);
return { ok: true, baseline: baseline };
}
/**
* 校验当前 jsonl 与 baseline
* @returns { ok, drift, expected, actual, baselineLineCount, currentLineCount }
*/
function verifyChain(jsonlPath, baselinePath) {
if (!fs.existsSync(baselinePath)) {
return { ok: false, drift: 'no-baseline', expected: null, actual: null };
}
let baseline;
try {
baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
} catch (e) {
return { ok: false, drift: 'baseline-malformed', error: e.message };
}
const current = computeChain(jsonlPath);
if (!current.ok && current.error) {
return { ok: false, drift: 'compute-error', error: current.error };
}
// 仅当 currentLineCount >= baselineLineCount 才视为追加增长(合法)
// 此时 baseline.sig 应该出现在 current 链的某一中间状态
// 简化:如果 current.lineCount >= baseline.lineCount 且 baseline 末态可在 current 复现 → ok
if (current.lineCount < baseline.lineCount) {
return {
ok: false,
drift: 'truncated',
baselineLineCount: baseline.lineCount,
currentLineCount: current.lineCount,
};
}
// 重算到 baseline.lineCount 的末态
const partial = computeChainPartial(jsonlPath, baseline.lineCount);
if (partial.lastHmac !== baseline.sig) {
return {
ok: false,
drift: 'tampered',
expected: baseline.sig,
actual: partial.lastHmac,
atLine: baseline.lineCount,
};
}
return {
ok: true,
drift: null,
baselineLineCount: baseline.lineCount,
currentLineCount: current.lineCount,
appended: current.lineCount - baseline.lineCount,
currentSig: current.sig,
};
}
function computeChainPartial(jsonlPath, maxLines) {
let prev = '';
let n = 0;
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\\n');
for (let i = 0; i < lines.length && n < maxLines; i++) {
const line = lines[i];
if (!line) continue;
prev = hmacLine(prev, line);
n++;
}
return { lastHmac: prev, lineCount: n };
}
/**
* seq 单调性检查(如果 jsonl 有 seq 字段)
*/
function assertSeqMonotonic(jsonlPath, seqField) {
if (!seqField) seqField = 'seq';
const lines = fs.readFileSync(jsonlPath, 'utf8').split('\\n');
let lastSeq = -Infinity;
const violations = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line) continue;
try {
const obj = JSON.parse(line);
if (typeof obj[seqField] === 'number') {
if (obj[seqField] < lastSeq) {
violations.push({ lineNo: i + 1, seq: obj[seqField], prevSeq: lastSeq });
}
lastSeq = obj[seqField];
}
} catch (_) {}
}
return { ok: violations.length === 0, violations: violations };
}
function hmacKeyFingerprint() {
const key = getKey();
if (!key) return null;
return crypto.createHash('sha256').update(key).digest('hex').slice(0, 12);
}
module.exports = {
computeChain,
writeBaseline,
verifyChain,
assertSeqMonotonic,
hmacKeyFingerprint,
__sentinel: '${SENTINEL}',
};
`;
function main() {
const dir = path.dirname(TARGET);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
if (fs.existsSync(TARGET)) {
const cur = fs.readFileSync(TARGET, 'utf8');
if (cur.includes(SENTINEL)) {
process.stdout.write('[SKIP] already deployed\n');
process.exit(0);
}
const ts = new Date().toISOString().replace(/[:.]/g, '-');
fs.copyFileSync(TARGET, TARGET + '.bak.' + ts);
process.stdout.write('[BACKUP] ' + TARGET + '.bak.' + ts + '\n');
}
const tmpPath = TARGET + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, CONTENT);
try {
delete require.cache[require.resolve(tmpPath)];
const mod = require(tmpPath);
// 自检 1: hmac-key 可加载
const fp = mod.hmacKeyFingerprint();
if (!fp) throw new Error('hmac-key unavailable');
// 自检 2: computeChain 对一个临时 jsonl 工作
const ROOT = path.join(__dirname, '..', '..');
const testFile = path.join(ROOT, 'debug', '.hmac-self-test.jsonl');
fs.mkdirSync(path.dirname(testFile), { recursive: true });
fs.writeFileSync(testFile, '{"a":1}\n{"a":2}\n{"a":3}\n');
const chain = mod.computeChain(testFile);
if (!chain.ok) throw new Error('computeChain failed: ' + JSON.stringify(chain));
if (chain.lineCount !== 3) throw new Error('expected 3 lines got ' + chain.lineCount);
// 自检 3: writeBaseline + verifyChain (无篡改) → ok
const baselinePath = testFile + '.hmac-baseline.json';
const wr = mod.writeBaseline(testFile, baselinePath);
if (!wr.ok) throw new Error('writeBaseline: ' + wr.error);
const v1 = mod.verifyChain(testFile, baselinePath);
if (!v1.ok) throw new Error('clean verify failed: ' + JSON.stringify(v1));
// 自检 4: 篡改后 verify → drift=tampered
fs.writeFileSync(testFile, '{"a":1}\n{"a":99}\n{"a":3}\n');
const v2 = mod.verifyChain(testFile, baselinePath);
if (v2.ok || v2.drift !== 'tampered') throw new Error('tamper not detected: ' + JSON.stringify(v2));
// 自检 5: 截断后 verify → drift=truncated
fs.writeFileSync(testFile, '{"a":1}\n');
const v3 = mod.verifyChain(testFile, baselinePath);
if (v3.ok || v3.drift !== 'truncated') throw new Error('truncate not detected');
// 清理
try { fs.unlinkSync(testFile); fs.unlinkSync(baselinePath); } catch (_) {}
fs.renameSync(tmpPath, TARGET);
process.stdout.write('[OK] hooks/lib/jsonl-hmac.js deployed (5 self-tests pass)\n');
process.stdout.write(' hmac-key fingerprint: ' + fp + '\n');
} catch (e) {
fs.unlinkSync(tmpPath);
process.stderr.write('[ERROR] self-test failed: ' + e.message + '\n');
process.exit(1);
}
}
if (require.main === module) main();