| import crypto from 'crypto'; |
| const ALGORITHM = 'aes-256-gcm'; |
| let cachedKey = null; |
| |
| |
| |
| |
| |
| |
| |
| const KEY_BYTES = 32; |
| const KEY_HEX_LEN = KEY_BYTES * 2; |
| const PLACEHOLDER_KEY = 'your-64-char-hex-key-here'; |
| function parseHexKey(value, source) { |
| if (value.length !== KEY_HEX_LEN || !/^[0-9a-fA-F]+$/.test(value)) { |
| throw new Error(`Invalid ENCRYPTION_KEY (${source}): expected ${KEY_HEX_LEN} hex chars (32 bytes), got ${value.length} chars. ` + |
| `Generate one with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`); |
| } |
| return Buffer.from(value, 'hex'); |
| } |
| |
| |
| |
| |
| |
| |
| function isDevFallbackAllowed() { |
| return process.env.NODE_ENV !== 'production'; |
| } |
| function missingKeyError() { |
| return new Error('ENCRYPTION_KEY is required in production for API key encryption. ' + |
| `Set a ${KEY_HEX_LEN}-char hex key (generate one with: ` + |
| `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"\\). ` + |
| 'Outside production a local DB-stored key is auto-generated.'); |
| } |
| |
| |
| |
| |
| export async function initEncryptionKey(db) { |
| |
| const envKey = process.env.ENCRYPTION_KEY; |
| if (envKey && envKey !== PLACEHOLDER_KEY) { |
| cachedKey = parseHexKey(envKey, 'env'); |
| return; |
| } |
| if (!isDevFallbackAllowed()) { |
| throw missingKeyError(); |
| } |
| |
| const row = await db.get("SELECT value FROM settings WHERE key = 'encryption_key'"); |
| if (row) { |
| cachedKey = parseHexKey(row.value, 'db'); |
| console.warn('[crypto] No ENCRYPTION_KEY set — using auto-generated key from the local DB (dev only).'); |
| return; |
| } |
| |
| cachedKey = crypto.randomBytes(KEY_BYTES); |
| await db.run("INSERT INTO settings (key, value) VALUES ('encryption_key', ?)", [cachedKey.toString('hex')]); |
| console.warn('[crypto] No ENCRYPTION_KEY set — generated and persisted a local dev key. Set ENCRYPTION_KEY for production.'); |
| } |
| function getEncryptionKey() { |
| if (!cachedKey) { |
| throw new Error('Encryption key not initialized. Call initEncryptionKey() first.'); |
| } |
| return cachedKey; |
| } |
| export function encrypt(text) { |
| const key = getEncryptionKey(); |
| const iv = crypto.randomBytes(16); |
| const cipher = crypto.createCipheriv(ALGORITHM, key, iv); |
| let encrypted = cipher.update(text, 'utf8', 'hex'); |
| encrypted += cipher.final('hex'); |
| const authTag = cipher.getAuthTag().toString('hex'); |
| return { |
| encrypted, |
| iv: iv.toString('hex'), |
| authTag, |
| }; |
| } |
| export function decrypt(encrypted, iv, authTag) { |
| const key = getEncryptionKey(); |
| const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'hex')); |
| decipher.setAuthTag(Buffer.from(authTag, 'hex')); |
| let decrypted = decipher.update(encrypted, 'hex', 'utf8'); |
| decrypted += decipher.final('utf8'); |
| return decrypted; |
| } |
| export function maskKey(key) { |
| if (key.length <= 8) |
| return '****' + key.slice(-4); |
| return key.slice(0, 4) + '...' + key.slice(-4); |
| } |
| |