- 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)
204 lines
5.7 KiB
JavaScript
204 lines
5.7 KiB
JavaScript
'use strict';
|
||
/**
|
||
* jsonl-hmac.js — JSONL 完整性链 (P1-JSONL-HMAC-V1)
|
||
*
|
||
* 防 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: 'P1-JSONL-HMAC-V1',
|
||
};
|