| |
| |
| |
| |
| |
| |
|
|
| import crypto from 'crypto'; |
|
|
| const ALGORITHM = 'aes-256-gcm'; |
| const KEY_LENGTH = 32; |
|
|
| export interface EncryptedSecret { |
| encryptedValue: string; |
| iv: string; |
| authTag: string; |
| } |
|
|
| |
| |
| |
| |
| export function getEncryptionKey(): Buffer { |
| const keyBase64 = process.env.SECRETS_ENCRYPTION_KEY; |
| if (!keyBase64) { |
| throw new Error('SECRETS_ENCRYPTION_KEY environment variable not set'); |
| } |
|
|
| const key = Buffer.from(keyBase64, 'base64'); |
| if (key.length !== KEY_LENGTH) { |
| throw new Error( |
| `Invalid SECRETS_ENCRYPTION_KEY: expected ${KEY_LENGTH} bytes, got ${key.length}` |
| ); |
| } |
|
|
| return key; |
| } |
|
|
| |
| |
| |
| export function isEncryptionConfigured(): boolean { |
| try { |
| getEncryptionKey(); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function encryptSecret(plaintext: string): EncryptedSecret { |
| const key = getEncryptionKey(); |
| const iv = crypto.randomBytes(16); |
| const cipher = crypto.createCipheriv(ALGORITHM, key, iv); |
|
|
| let encrypted = cipher.update(plaintext, 'utf8', 'base64'); |
| encrypted += cipher.final('base64'); |
|
|
| return { |
| encryptedValue: encrypted, |
| iv: iv.toString('base64'), |
| authTag: cipher.getAuthTag().toString('base64'), |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function decryptSecret( |
| encryptedValue: string, |
| iv: string, |
| authTag: string |
| ): string { |
| const key = getEncryptionKey(); |
| const decipher = crypto.createDecipheriv( |
| ALGORITHM, |
| key, |
| Buffer.from(iv, 'base64') |
| ); |
| decipher.setAuthTag(Buffer.from(authTag, 'base64')); |
|
|
| let decrypted = decipher.update(encryptedValue, 'base64', 'utf8'); |
| decrypted += decipher.final('utf8'); |
|
|
| return decrypted; |
| } |
|
|
| |
| |
| |
| |
| export function generateEncryptionKey(): string { |
| return crypto.randomBytes(KEY_LENGTH).toString('base64'); |
| } |
|
|