157 lines
5.4 KiB
JavaScript
157 lines
5.4 KiB
JavaScript
#!/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
|
|
};
|