#!/usr/bin/env node 'use strict'; /** * Bookworm Portable - Node.js 凭证加解密工具 * 替代 openssl enc, 跨平台跨版本 100% 一致 * * 加密: echo "KEY=VALUE" | node crypto-helper.js encrypt > secrets.enc * 解密: node crypto-helper.js decrypt < 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 '); 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 [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 > secrets.enc'); console.error(' decrypt: node crypto-helper.js decrypt < secrets.enc'); console.error(' interactive: node crypto-helper.js decrypt-file secrets.enc'); process.exit(1); }