#!/usr/bin/env node 'use strict'; /** * Bookworm 授权码生成工具 (管理员使用) * * 用法: * node gen-authcode.js [secrets.txt路径] * node gen-authcode.js 30 * node gen-authcode.js 90 /path/to/secrets.txt * * 原理: * 1. 生成随机 24位Hex Token (96bit 熵) * 2. 将 Token 作为密码重新加密 secrets.enc (BWENC1 格式) * 3. 输出授权码 BW-YYYYMMDD-TOKEN (发给用户) * 4. 安全说明: Token = 解密密钥, YYYYMMDD = 客户端到期校验 */ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ALGO = 'aes-256-cbc'; const ITERATIONS = 600000; const DIGEST = 'sha256'; const SALT_LEN = 16; const KEY_LEN = 32; const IV_LEN = 16; 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()]); // 与 crypto-helper.js 保持 BWENC1 格式兼容 return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]); } // ─── CLI ──────────────────────────────────────────────────────── const DAYS = parseInt(process.argv[2]); if (!DAYS || DAYS < 1 || DAYS > 3650) { console.error('用法: node gen-authcode.js <有效天数> [secrets.txt路径]'); console.error('示例: node gen-authcode.js 30'); console.error('示例: node gen-authcode.js 90 /path/to/secrets.txt'); process.exit(1); } const SCRIPT_DIR = path.dirname(path.resolve(__filename)); const SECRETS_TXT = process.argv[3] || path.join(SCRIPT_DIR, 'secrets.txt'); const SECRETS_ENC = path.join(SCRIPT_DIR, 'secrets.enc'); if (!fs.existsSync(SECRETS_TXT)) { console.error(`[错误] 找不到 secrets.txt: ${SECRETS_TXT}`); console.error('请先创建 secrets.txt, 每行格式: KEY=VALUE'); process.exit(1); } // 生成到期日 (YYYYMMDD) const expiry = new Date(); expiry.setDate(expiry.getDate() + DAYS); const pad = n => String(n).padStart(2, '0'); const expiryStr = `${expiry.getFullYear()}${pad(expiry.getMonth()+1)}${pad(expiry.getDate())}`; // 生成随机 Token (12 bytes = 24 hex, 96bit 熵) const token = crypto.randomBytes(12).toString('hex'); // 小写 // 构造授权码 (Token 展示为大写, 解密时转小写) const authCode = `BW-${expiryStr}-${token.toUpperCase()}`; // 读取并加密 secrets.txt const secretsPlain = fs.readFileSync(SECRETS_TXT, 'utf8').trim(); const encBuffer = encrypt(secretsPlain, token); // 用小写 token 加密 fs.writeFileSync(SECRETS_ENC, encBuffer); const expiryDisplay = `${expiryStr.slice(0,4)}-${expiryStr.slice(4,6)}-${expiryStr.slice(6,8)}`; console.log('\n═══════════════════════════════════════════════════'); console.log(' Bookworm 授权码生成完毕'); console.log('═══════════════════════════════════════════════════'); console.log(''); console.log(` 授权码: ${authCode}`); console.log(` 有效期: ${DAYS} 天 (至 ${expiryDisplay})`); console.log(''); console.log(' ▶ 操作步骤:'); console.log(' 1. 将授权码通过微信/邮件发给用户'); console.log(' 2. 推送新 secrets.enc 到 Gitea:'); console.log(' git add secrets.enc && git commit -m "update secrets" && git push'); console.log(''); console.log(' ⚠ 安全提醒:'); console.log(' - 授权码即解密密钥, 请勿通过不安全渠道明文发送'); console.log(` - 到期日 (${expiryDisplay}) 后自动失效, 无需撤销操作`); console.log('═══════════════════════════════════════════════════\n');