bookworm-smart-assistant/hooks/lib/state-integrity.js

157 lines
5.4 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/** L4: 状态文件 HMAC 完整性模块 (v1.2 — 随机密钥 + Markdown + sanitization) */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const os = require('os');
const SALT = 'bookworm-state-integrity-v1';
// R5: HMAC 密钥改为随机生成,首次运行时持久化到 .hmac-key (权限 600)
// 回退: 密钥文件不可用时仍使用 hostname+username 派生 (向后兼容)
var _keyFile = path.join(os.homedir(), '.claude', '.hmac-key');
function deriveKey() {
// 优先使用随机密钥文件
try {
if (fs.existsSync(_keyFile)) {
return Buffer.from(fs.readFileSync(_keyFile, 'utf8').trim(), 'hex');
}
// 首次运行: 生成 32 字节随机密钥
var key = crypto.randomBytes(32);
var dir = path.dirname(_keyFile);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(_keyFile, key.toString('hex') + '\n', { mode: 0o600 });
return key;
} catch {
// 回退: 可预测密钥 (向后兼容)
return crypto.createHash('sha256')
.update(os.hostname() + ':' + os.userInfo().username + ':' + SALT)
.digest();
}
}
function computeHMAC(content) {
return crypto.createHmac('sha256', deriveKey()).update(content).digest('hex');
}
function sigPath(fp) { return fp + '.sig'; }
// ─── JSON 文件签名读写 ──────────────────────────────────
function writeWithSignature(filePath, data) {
var json = JSON.stringify(data, null, 2) + '\n';
var tmp = filePath + '.tmp.' + process.pid;
fs.writeFileSync(tmp, json);
fs.renameSync(tmp, filePath);
fs.writeFileSync(sigPath(filePath), computeHMAC(json) + '\n');
}
function readWithVerification(filePath, opts) {
opts = opts || {};
try {
var raw = fs.readFileSync(filePath, 'utf8');
var sp = sigPath(filePath);
if (!fs.existsSync(sp)) {
return { data: JSON.parse(raw), verified: false, error: 'sig-missing' };
}
var expected = fs.readFileSync(sp, 'utf8').trim();
var actual = computeHMAC(raw);
if (expected !== actual) {
if (opts.strict) return { data: null, verified: false, error: 'sig-mismatch' };
return { data: JSON.parse(raw), verified: false, error: 'sig-mismatch' };
}
return { data: JSON.parse(raw), verified: true, error: null };
} catch (e) {
return { data: null, verified: false, error: e.message };
}
}
// ─── 通用文件原子写入 + 签名 (支持任意文本文件) ──────────
/**
* 原子写入文本文件 + HMAC 签名
* @param {string} filePath - 目标文件路径
* @param {string} content - 文本内容
*/
function writeTextWithSignature(filePath, content) {
var dir = path.dirname(filePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
var tmp = filePath + '.tmp.' + process.pid;
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, filePath);
fs.writeFileSync(sigPath(filePath), computeHMAC(content) + '\n');
}
/**
* 读取文本文件 + 验证 HMAC
* @param {string} filePath
* @param {object} [opts] - { strict: boolean }
* @returns {{ content: string|null, verified: boolean, error: string|null }}
*/
function readTextWithVerification(filePath, opts) {
opts = opts || {};
try {
var raw = fs.readFileSync(filePath, 'utf8');
var sp = sigPath(filePath);
if (!fs.existsSync(sp)) {
return { content: raw, verified: false, error: 'sig-missing' };
}
var expected = fs.readFileSync(sp, 'utf8').trim();
var actual = computeHMAC(raw);
if (expected !== actual) {
if (opts.strict) return { content: null, verified: false, error: 'sig-mismatch' };
return { content: raw, verified: false, error: 'sig-mismatch' };
}
return { content: raw, verified: true, error: null };
} catch (e) {
return { content: null, verified: false, error: e.message };
}
}
// ─── Prompt Injection Sanitizer ─────────────────────────
/**
* 检测 resume-prompt.md 中的疑似注入标记
* 返回清理后的内容 + 检测到的可疑模式列表
*/
const INJECTION_PATTERNS = [
/<!--\s*SYSTEM\b/gi,
/<!--\s*OVERRIDE\b/gi,
/<!--\s*IMPORTANT\b/gi,
/\bSYSTEM\s*OVERRIDE\b/gi,
/\bIGNORE\s+(?:ALL\s+)?PREVIOUS\s+INSTRUCTIONS?\b/gi,
/\bYOU\s+ARE\s+NOW\b/gi,
/\bDO\s+NOT\s+FOLLOW\b/gi,
/^\s*(?:you\s+must\s+)?act\s+as\s+(?:a\s+)?(?:system|admin|root|hacker)/gim,
/\bFORGET\s+(?:ALL\s+)?YOUR\b/gi,
/\bEXFILTRATE\b/gi,
/\bcurl\s+.*\|\s*(?:ba)?sh\b/gi,
/\beval\s*\(/gi,
/\bnew\s+Function\b/gi,
/cat\s+~\/\.ssh\b/gi,
/cat\s+.*\.env\b/gi,
/base64\s+.*\.env\b/gi,
];
function sanitizeResumePrompt(content) {
var warnings = [];
var cleaned = content;
for (var i = 0; i < INJECTION_PATTERNS.length; i++) {
// W-04 修复: 重置 lastIndex 避免 /g 标志导致 test() 和 replace() 行为不一致
INJECTION_PATTERNS[i].lastIndex = 0;
if (INJECTION_PATTERNS[i].test(cleaned)) {
warnings.push('detected: ' + INJECTION_PATTERNS[i].source);
INJECTION_PATTERNS[i].lastIndex = 0;
cleaned = cleaned.replace(INJECTION_PATTERNS[i], '[SANITIZED-INJECTION-ATTEMPT]');
}
}
return { content: cleaned, warnings: warnings, injectionDetected: warnings.length > 0 };
}
module.exports = {
writeWithSignature, readWithVerification,
writeTextWithSignature, readTextWithVerification,
sanitizeResumePrompt,
computeHMAC, sigPath, deriveKey
};