Remove decryption and api key storage

This commit is contained in:
Joey Yakimowich-Payne 2026-01-22 07:32:11 -07:00
commit 15b76f330b
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
8 changed files with 326 additions and 233 deletions

View file

@ -1,123 +0,0 @@
import { createCipheriv, createDecipheriv, randomBytes, createHash, hkdfSync } from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
const SALT_LENGTH = 16;
let encryptionWarningShown = false;
function getMasterKey(): Buffer | null {
const key = process.env.ENCRYPTION_KEY;
if (!key) {
if (!encryptionWarningShown) {
console.warn('[SECURITY WARNING] ENCRYPTION_KEY not set. User data will NOT be encrypted.');
console.warn('Generate one with: openssl rand -base64 36');
encryptionWarningShown = true;
}
return null;
}
return createHash('sha256').update(key).digest();
}
function deriveUserKey(userSub: string, salt: Buffer): Buffer | null {
const masterKey = getMasterKey();
if (!masterKey) return null;
return Buffer.from(
hkdfSync('sha256', masterKey, salt, `kaboot-user-data:${userSub}`, 32)
);
}
export function encryptForUser(plaintext: string | null | undefined, userSub: string): string | null {
if (plaintext === null || plaintext === undefined || plaintext === '') {
return null;
}
const salt = randomBytes(SALT_LENGTH);
const key = deriveUserKey(userSub, salt);
if (!key) {
return plaintext;
}
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const combined = Buffer.concat([salt, iv, authTag, encrypted]);
return combined.toString('base64');
}
export function decryptForUser(ciphertext: string | null | undefined, userSub: string): string | null {
if (ciphertext === null || ciphertext === undefined || ciphertext === '') {
return null;
}
const masterKey = getMasterKey();
if (!masterKey) {
return ciphertext;
}
try {
const combined = Buffer.from(ciphertext, 'base64');
if (combined.length < SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH + 1) {
return ciphertext;
}
const salt = combined.subarray(0, SALT_LENGTH);
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
const key = Buffer.from(
hkdfSync('sha256', masterKey, salt, `kaboot-user-data:${userSub}`, 32)
);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.toString('utf8');
} catch {
return ciphertext;
}
}
export function isEncrypted(value: string | null | undefined): boolean {
if (!value) return false;
try {
const decoded = Buffer.from(value, 'base64');
return decoded.length >= SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH + 1;
} catch {
return false;
}
}
export function encryptJsonForUser(data: object | null | undefined, userSub: string): string | null {
if (data === null || data === undefined) {
return null;
}
return encryptForUser(JSON.stringify(data), userSub);
}
export function decryptJsonForUser<T>(ciphertext: string | null | undefined, userSub: string): T | null {
const plaintext = decryptForUser(ciphertext, userSub);
if (!plaintext) return null;
try {
return JSON.parse(plaintext) as T;
} catch {
return null;
}
}