bookworm-smart-assistant/hooks/lib/jsonl-hmac.js

204 lines
5.7 KiB
JavaScript
Raw Permalink Normal View History

'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',
};