bookworm-boot/crypto-helper.js
bookworm 5e0ff18aa1 feat: Bookworm Portable v1.5 — 8 fixes (P0 NDA + P1 banners + P2 perf)
- P1: Banner v1.3→v1.5, Hooks 29→34
- P1: 卸载脚本补删 更新Bookworm.lnk
- P1: git stash pop 安全检查
- P2: Playwright 检测改用 npm list
- P2: 代理端口扫描 500ms async 超时

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:34:27 +08:00

159 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
'use strict';
/**
* Bookworm Portable - Node.js 凭证加解密工具
* 替代 openssl enc, 跨平台跨版本 100% 一致
*
* 加密: echo "KEY=VALUE" | node crypto-helper.js encrypt <password> > secrets.enc
* 解密: node crypto-helper.js decrypt <password> < secrets.enc
* 交互解密: node crypto-helper.js decrypt-interactive secrets.enc
*/
const crypto = require('crypto');
const fs = require('fs');
const readline = require('readline');
const ALGO = 'aes-256-cbc';
const ITERATIONS = 600000;
const DIGEST = 'sha256';
const SALT_LEN = 16;
const IV_LEN = 16;
const KEY_LEN = 32;
function deriveKey(password, salt) {
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN + IV_LEN, DIGEST);
}
function encrypt(plaintext, password) {
const salt = crypto.randomBytes(SALT_LEN);
const derived = deriveKey(password, salt);
const key = derived.slice(0, KEY_LEN);
const iv = derived.slice(KEY_LEN, KEY_LEN + IV_LEN);
const cipher = crypto.createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
// 格式: BWENC1 + salt(16) + encrypted
return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]);
}
function decrypt(data, password) {
const magic = data.slice(0, 6).toString();
if (magic !== 'BWENC1') {
throw new Error('WRONG_FORMAT');
}
const salt = data.slice(6, 6 + SALT_LEN);
const encrypted = data.slice(6 + SALT_LEN);
const derived = deriveKey(password, salt);
const key = derived.slice(0, KEY_LEN);
const iv = derived.slice(KEY_LEN, KEY_LEN + IV_LEN);
const decipher = crypto.createDecipheriv(ALGO, key, iv);
try {
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted.toString('utf8');
} catch (e) {
throw new Error('WRONG_PASSWORD');
}
}
// ─── CLI ───
const [,, cmd, arg1, arg2] = process.argv;
if (cmd === 'encrypt') {
const password = arg1;
if (!password) { console.error('Usage: node crypto-helper.js encrypt <password>'); process.exit(1); }
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
const enc = encrypt(input.trim(), password);
process.stdout.write(enc);
});
} else if (cmd === 'decrypt') {
const password = arg1;
const file = arg2; // 可选: 文件路径, 否则从 stdin
if (!password) { console.error('Usage: node crypto-helper.js decrypt <password> [file]'); process.exit(1); }
let data;
if (file) {
data = fs.readFileSync(file);
} else {
// Windows 兼容: 从 stdin 读取 buffer
const chunks = [];
const fd = fs.openSync(0, 'r');
const buf = Buffer.alloc(4096);
let n;
while ((n = fs.readSync(fd, buf)) > 0) chunks.push(buf.slice(0, n));
fs.closeSync(fd);
data = Buffer.concat(chunks);
}
try {
console.log(decrypt(data, password));
} catch (e) {
if (e.message === 'WRONG_PASSWORD') { console.error('PASSWORD_ERROR'); process.exit(2); }
if (e.message === 'WRONG_FORMAT') { console.error('FORMAT_ERROR'); process.exit(3); }
throw e;
}
} else if (cmd === 'decrypt-interactive' || cmd === 'decrypt-file') {
// 从文件解密, 交互输入密码, 输出 KEY=VALUE 到 stdout
const filePath = arg1;
if (!filePath || !fs.existsSync(filePath)) { console.error('File not found: ' + filePath); process.exit(1); }
const data = fs.readFileSync(filePath);
const maxRetries = parseInt(arg2) || 3;
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
let attempt = 0;
function ask() {
attempt++;
const prompt = attempt > 1 ? ` 重新输入主密码 (第 ${attempt}/${maxRetries} 次): ` : ' 输入主密码解密凭证: ';
// 隐藏输入 (Windows 兼容)
if (process.platform === 'win32') {
process.stderr.write(prompt);
const { execSync } = require('child_process');
try {
const pwd = execSync('powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"', { stdio: ['inherit', 'pipe', 'pipe'] }).toString().trim();
tryDecrypt(pwd);
} catch (e) {
rl.question(prompt, tryDecrypt);
}
} else {
rl.question(prompt, tryDecrypt);
}
}
function tryDecrypt(password) {
try {
const result = decrypt(data, password);
rl.close();
console.log(result); // stdout: KEY=VALUE lines
process.exit(0);
} catch (e) {
if (e.message === 'WRONG_PASSWORD') {
const remaining = maxRetries - attempt;
if (remaining > 0) {
process.stderr.write(` [!!] 密码错误,剩余重试: ${remaining}\n`);
ask();
} else {
process.stderr.write(' [ABORT] 密码错误次数过多\n');
rl.close();
process.exit(2);
}
} else if (e.message === 'WRONG_FORMAT') {
process.stderr.write(' [ERROR] secrets.enc 格式不兼容, 请联系管理员重新生成\n');
rl.close();
process.exit(3);
} else {
throw e;
}
}
}
ask();
} else {
console.error('Bookworm Crypto Helper');
console.error(' encrypt: echo "K=V" | node crypto-helper.js encrypt <password> > secrets.enc');
console.error(' decrypt: node crypto-helper.js decrypt <password> < secrets.enc');
console.error(' interactive: node crypto-helper.js decrypt-file secrets.enc');
process.exit(1);
}