#!/usr/bin/env node 'use strict'; /** * Bookworm 授权码生成工具 (管理员使用) * * 用法: * node gen-authcode.js [选项] * * 选项: * --relay-key, -k 中转站限额子 Key (替换 ANTHROPIC_API_KEY) * --user, -u 用户标识 (仅用于显示, 不影响加密) * [secrets.txt路径] 明文凭证文件 (默认: ./secrets.txt) * * 示例: * node gen-authcode.js 30 # 共享 Key (单用户) * node gen-authcode.js 30 --relay-key sk-relay-xxx # 独立限额 Key * node gen-authcode.js 90 -k sk-relay-xxx -u alice # 指定用户名 * * 原理: * 1. 生成随机 24位Hex Token (96bit 熵) * 2. Token 前8位 = 文件 ID → 输出 secrets-XXXXXXXX.enc (多用户模式) * 无 --relay-key 时 → 输出 secrets.enc (单用户/共享模式) * 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()]); return Buffer.concat([Buffer.from('BWENC1'), salt, encrypted]); } // ─── 解析 CLI 参数 ─────────────────────────────────────── const rawArgs = process.argv.slice(2); const DAYS = parseInt(rawArgs[0]); if (!DAYS || DAYS < 1 || DAYS > 3650) { console.error('用法: node gen-authcode.js <有效天数> [选项]'); console.error('选项:'); console.error(' --relay-key, -k 中转站限额子 Key'); console.error(' --user, -u 用户标识 (仅显示)'); console.error(' [secrets.txt路径] 默认: ./secrets.txt'); console.error(''); console.error('示例:'); console.error(' node gen-authcode.js 30 # 共享模式'); console.error(' node gen-authcode.js 30 -k sk-relay-xxx -u alice # 独立限额'); process.exit(1); } let relayKey = null; let userName = null; let secretsTxtArg = null; for (let i = 1; i < rawArgs.length; i++) { const a = rawArgs[i]; if (a === '--relay-key' || a === '-k') { relayKey = rawArgs[++i]; } else if (a === '--user' || a === '-u') { userName = rawArgs[++i]; } else if (!a.startsWith('-')) { secretsTxtArg = a; } } // pkg 打包后 __filename 指向虚拟快照路径,需用 process.execPath 获取真实目录 const SCRIPT_DIR = path.dirname( typeof process.pkg !== 'undefined' ? process.execPath : path.resolve(__filename) ); const SECRETS_TXT = secretsTxtArg || path.join(SCRIPT_DIR, 'secrets.txt'); if (!fs.existsSync(SECRETS_TXT)) { console.error(`[错误] 找不到 secrets.txt: ${SECRETS_TXT}`); console.error('请先创建 secrets.txt, 每行格式: KEY=VALUE'); process.exit(1); } if (relayKey && !/^[A-Za-z0-9\-_.]+$/.test(relayKey)) { console.error('[错误] --relay-key 格式不合法'); process.exit(1); } // ─── 生成到期日 ────────────────────────────────────────── 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())}`; const expiryDisplay = `${expiryStr.slice(0,4)}-${expiryStr.slice(4,6)}-${expiryStr.slice(6,8)}`; // ─── 生成随机 Token ────────────────────────────────────── const token = crypto.randomBytes(12).toString('hex'); // 小写 24位 const authCode = `BW-${expiryStr}-${token.toUpperCase()}`; const fileId = token.slice(0, 8); // 前8位作为文件 ID // ─── 构建待加密内容 ────────────────────────────────────── let secretsPlain = fs.readFileSync(SECRETS_TXT, 'utf8').trim(); const multiUser = !!relayKey; if (multiUser) { // 用中转站 relay key 替换 ANTHROPIC_API_KEY if (/^ANTHROPIC_API_KEY=/m.test(secretsPlain)) { secretsPlain = secretsPlain.replace( /^ANTHROPIC_API_KEY=.*/m, `ANTHROPIC_API_KEY=${relayKey}` ); } else { secretsPlain = `ANTHROPIC_API_KEY=${relayKey}\n${secretsPlain}`; } } // ─── 加密并写出 ────────────────────────────────────────── const encBuffer = encrypt(secretsPlain, token); let outFileName, outFilePath; if (multiUser) { outFileName = `secrets-${fileId}.enc`; } else { outFileName = 'secrets.enc'; } outFilePath = path.join(SCRIPT_DIR, outFileName); fs.writeFileSync(outFilePath, encBuffer); // ─── 输出 ──────────────────────────────────────────────── const modeLabel = multiUser ? `多用户独立 Key (${userName || '未命名'})` : '共享 Key (单/多用户共享)'; console.log('\n═══════════════════════════════════════════════════'); console.log(' Bookworm 授权码生成完毕'); console.log('═══════════════════════════════════════════════════'); console.log(''); console.log(` 模式: ${modeLabel}`); if (userName) console.log(` 用户: ${userName}`); console.log(` 授权码: ${authCode}`); console.log(` 有效期: ${DAYS} 天 (至 ${expiryDisplay})`); if (multiUser) { console.log(` Relay Key: ${relayKey.slice(0,12)}... (已替换 ANTHROPIC_API_KEY)`); console.log(` 文件 ID: ${fileId} → ${outFileName}`); } console.log(''); console.log(' ▶ 操作步骤:'); console.log(` 1. 将授权码发给用户: ${authCode}`); console.log(` 2. 推送 ${outFileName} 到 Gitea:`); console.log(` git add ${outFileName} && git commit -m "add user ${userName || fileId}" && git push`); console.log(''); console.log(' ⚠ 安全提醒:'); console.log(' - 授权码即解密密钥, 请勿通过不安全渠道明文发送'); console.log(` - 到期日 (${expiryDisplay}) 后自动失效`); if (multiUser) { console.log(` - 各用户 secrets-XXXXXXXX.enc 独立, 轮换互不影响`); } console.log('═══════════════════════════════════════════════════\n');