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