| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto"; |
|
|
| const ALGORITHM = "aes-256-gcm"; |
| const IV_LENGTH = 16; |
| const KEY_LENGTH = 32; |
| const PREFIX = "enc:v1:"; |
|
|
| let _derivedKey: Buffer | null = null; |
|
|
| |
| export interface ConnectionFields { |
| apiKey?: string | null; |
| accessToken?: string | null; |
| refreshToken?: string | null; |
| idToken?: string | null; |
| [key: string]: unknown; |
| } |
|
|
| |
| |
| |
| |
| function getKey(): Buffer | null { |
| if (_derivedKey !== null) return _derivedKey; |
|
|
| const secret = process.env.STORAGE_ENCRYPTION_KEY; |
| if (!secret) return null; |
|
|
| |
| const salt = "omniroute-field-encryption-v1"; |
| _derivedKey = scryptSync(secret, salt, KEY_LENGTH); |
| return _derivedKey; |
| } |
|
|
| |
| export function isEncryptionEnabled(): boolean { |
| return !!process.env.STORAGE_ENCRYPTION_KEY; |
| } |
|
|
| |
| |
| |
| |
| export function encrypt(plaintext: string | null | undefined): string | null | undefined { |
| if (!plaintext || typeof plaintext !== "string") return plaintext; |
|
|
| const key = getKey(); |
| if (!key) return plaintext; |
|
|
| |
| if (plaintext.startsWith(PREFIX)) return plaintext; |
|
|
| const iv = randomBytes(IV_LENGTH); |
| const cipher = createCipheriv(ALGORITHM, key, iv); |
|
|
| let encrypted = cipher.update(plaintext, "utf8", "hex"); |
| encrypted += cipher.final("hex"); |
| const authTag = cipher.getAuthTag().toString("hex"); |
|
|
| return `${PREFIX}${iv.toString("hex")}:${encrypted}:${authTag}`; |
| } |
|
|
| |
| |
| |
| export function decrypt(ciphertext: string | null | undefined): string | null | undefined { |
| if (!ciphertext || typeof ciphertext !== "string") return ciphertext; |
|
|
| |
| if (!ciphertext.startsWith(PREFIX)) return ciphertext; |
|
|
| const key = getKey(); |
| if (!key) { |
| console.warn( |
| "[Encryption] Found encrypted data but STORAGE_ENCRYPTION_KEY is not set. Cannot decrypt." |
| ); |
| return ciphertext; |
| } |
|
|
| const body = ciphertext.slice(PREFIX.length); |
| const parts = body.split(":"); |
| if (parts.length !== 3) { |
| console.error("[Encryption] Malformed encrypted value"); |
| return ciphertext; |
| } |
|
|
| const [ivHex, encryptedHex, authTagHex] = parts; |
|
|
| try { |
| const iv = Buffer.from(ivHex, "hex"); |
| const authTag = Buffer.from(authTagHex, "hex"); |
| const decipher = createDecipheriv(ALGORITHM, key, iv); |
| decipher.setAuthTag(authTag); |
|
|
| let decrypted = decipher.update(encryptedHex, "hex", "utf8"); |
| decrypted += decipher.final("utf8"); |
| return decrypted; |
| } catch (err: unknown) { |
| const message = err instanceof Error ? err.message : String(err); |
| console.error("[Encryption] Decryption failed:", message); |
| return ciphertext; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function encryptConnectionFields(conn: any): any { |
| if (!isEncryptionEnabled()) return conn; |
|
|
| if (conn.apiKey) conn.apiKey = encrypt(conn.apiKey); |
| if (conn.accessToken) conn.accessToken = encrypt(conn.accessToken); |
| if (conn.refreshToken) conn.refreshToken = encrypt(conn.refreshToken); |
| if (conn.idToken) conn.idToken = encrypt(conn.idToken); |
| return conn; |
| } |
|
|
| |
| |
| |
| |
| export function decryptConnectionFields(row: any): any { |
| if (!row) return row; |
| if (!isEncryptionEnabled()) return row; |
|
|
| return { |
| ...row, |
| apiKey: decrypt(row.apiKey), |
| accessToken: decrypt(row.accessToken), |
| refreshToken: decrypt(row.refreshToken), |
| idToken: decrypt(row.idToken), |
| }; |
| } |
|
|