Spaces:
Runtime error
Runtime error
File size: 4,027 Bytes
077865a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
let cachedKey = null;
/**
* AES-256-GCM uses a 32-byte key, hex-encoded as 64 chars.
* A typo'd ENCRYPTION_KEY (e.g. "abc") would historically fall through
* the placeholder check, get truncated to 1.5 bytes, and only fail at
* the first encrypt() call with a cryptic node:crypto error. Validate
* the length up front and fail fast with an actionable message.
*/
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');
}
// Outside production we auto-generate and persist a key so a fresh clone
// (`npm run dev`) boots without manual setup — the placeholder ENCRYPTION_KEY
// in .env.example would otherwise crash the server on boot, which surfaces in
// the client as "Can't reach the server". Production still requires an explicit
// env key: a generated key lives only in the local DB and silently losing it
// would make every stored API key undecryptable.
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.');
}
/**
* Initialize encryption key from env or an explicit local-dev fallback.
* Must be called after DB is initialized.
*/
export function initEncryptionKey(db) {
// 1. Check env var
const envKey = process.env.ENCRYPTION_KEY;
if (envKey && envKey !== PLACEHOLDER_KEY) {
cachedKey = parseHexKey(envKey, 'env');
return;
}
if (!isDevFallbackAllowed()) {
throw missingKeyError();
}
// 2. Check DB for persisted key
const row = db.prepare("SELECT value FROM settings WHERE key = 'encryption_key'").get();
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;
}
// 3. Generate and persist
cachedKey = crypto.randomBytes(KEY_BYTES);
db.prepare("INSERT INTO settings (key, value) VALUES ('encryption_key', ?)").run(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);
}
//# sourceMappingURL=crypto.js.map |