- 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>
159 lines
5.4 KiB
JavaScript
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);
|
|
}
|