import crypto from "crypto"; interface EncryptedData { data: string; iv: string; tag: string; salt: string; recordId: string; } class FieldCrypto { private static readonly ALGORITHM = "aes-256-gcm"; private static readonly KEY_LENGTH = 32; private static readonly IV_LENGTH = 16; private static readonly SALT_LENGTH = 32; private static readonly ENCRYPTED_FIELDS = { users: new Set([ "password_hash", "passwordHash", "client_secret", "clientSecret", "totp_secret", "totpSecret", "totp_backup_codes", "totpBackupCodes", "oidc_identifier", "oidcIdentifier", ]), ssh_data: new Set([ "password", "key", "key_password", "keyPassword", "autostartPassword", "autostartKey", "autostartKeyPassword", ]), ssh_credentials: new Set([ "password", "private_key", "privateKey", "key_password", "keyPassword", "key", "public_key", "publicKey", ]), }; static encryptField( plaintext: string, masterKey: Buffer, recordId: string, fieldName: string, ): string { if (!plaintext) return ""; const salt = crypto.randomBytes(this.SALT_LENGTH); const context = `${recordId}:${fieldName}`; const fieldKey = Buffer.from( crypto.hkdfSync("sha256", masterKey, salt, context, this.KEY_LENGTH), ); const iv = crypto.randomBytes(this.IV_LENGTH); const cipher = crypto.createCipheriv( this.ALGORITHM, fieldKey, iv, ) as crypto.CipherGCM; let encrypted = cipher.update(plaintext, "utf8", "hex"); encrypted += cipher.final("hex"); const tag = cipher.getAuthTag(); const encryptedData: EncryptedData = { data: encrypted, iv: iv.toString("hex"), tag: tag.toString("hex"), salt: salt.toString("hex"), recordId: recordId, }; return JSON.stringify(encryptedData); } static decryptField( encryptedValue: string, masterKey: Buffer, recordId: string, fieldName: string, ): string { if (!encryptedValue) return ""; const encrypted: EncryptedData = JSON.parse(encryptedValue); const salt = Buffer.from(encrypted.salt, "hex"); if (!encrypted.recordId) { throw new Error( `Encrypted field missing recordId context - data corruption or legacy format not supported`, ); } const context = `${encrypted.recordId}:${fieldName}`; const fieldKey = Buffer.from( crypto.hkdfSync("sha256", masterKey, salt, context, this.KEY_LENGTH), ); const decipher = crypto.createDecipheriv( this.ALGORITHM, fieldKey, Buffer.from(encrypted.iv, "hex"), ) as crypto.DecipherGCM; decipher.setAuthTag(Buffer.from(encrypted.tag, "hex")); let decrypted = decipher.update(encrypted.data, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } static shouldEncryptField(tableName: string, fieldName: string): boolean { const fields = this.ENCRYPTED_FIELDS[tableName as keyof typeof this.ENCRYPTED_FIELDS]; return fields ? fields.has(fieldName) : false; } } export { FieldCrypto, type EncryptedData };