| import crypto from 'crypto';
|
| import type { TursoDb } from '../db/client.js';
|
|
|
| const ALGORITHM = 'aes-256-gcm';
|
|
|
| let cachedKey: Buffer | null = null;
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| const KEY_BYTES = 32;
|
| const KEY_HEX_LEN = KEY_BYTES * 2;
|
| const PLACEHOLDER_KEY = 'your-64-char-hex-key-here';
|
|
|
| function parseHexKey(value: string, source: 'env' | 'db'): Buffer {
|
| 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(): boolean {
|
| return process.env.NODE_ENV !== 'production';
|
| }
|
|
|
| function missingKeyError(): Error {
|
| 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: TursoDb): Promise<void> {
|
|
|
| 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<{ value: string }>("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(): Buffer {
|
| if (!cachedKey) {
|
| throw new Error('Encryption key not initialized. Call initEncryptionKey() first.');
|
| }
|
| return cachedKey;
|
| }
|
|
|
| export function encrypt(text: string): { encrypted: string; iv: string; authTag: string } {
|
| 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: string, iv: string, authTag: string): string {
|
| 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: string): string {
|
| if (key.length <= 8) return '****' + key.slice(-4);
|
| return key.slice(0, 4) + '...' + key.slice(-4);
|
| }
|
|
|