1- import { createCipheriv , createDecipheriv , createHash , randomBytes } from 'crypto'
21import { createLogger } from '@sim/logger'
2+ import { decrypt , encrypt } from '@sim/security/encryption'
3+ import { sha256Hex } from '@sim/security/hash'
4+ import { generateSecureToken } from '@sim/security/tokens'
5+ import { toError } from '@sim/utils/errors'
36import { env } from '@/lib/core/config/env'
47
58const logger = createLogger ( 'ApiKeyCrypto' )
69
7- /**
8- * Get the API encryption key from the environment
9- * @returns The API encryption key
10- */
1110function getApiEncryptionKey ( ) : Buffer | null {
1211 const key = env . API_ENCRYPTION_KEY
1312 if ( ! key ) {
@@ -23,77 +22,38 @@ function getApiEncryptionKey(): Buffer | null {
2322}
2423
2524/**
26- * Encrypts an API key using the dedicated API encryption key
27- * @param apiKey - The API key to encrypt
28- * @returns A promise that resolves to an object containing the encrypted API key and IV
25+ * Encrypts an API key using the dedicated API encryption key. Falls back to
26+ * returning the plain key when `API_ENCRYPTION_KEY` is unset, for backward
27+ * compatibility with deployments that predate encryption-at-rest.
2928 */
3029export async function encryptApiKey ( apiKey : string ) : Promise < { encrypted : string ; iv : string } > {
3130 const key = getApiEncryptionKey ( )
32-
33- // If no API encryption key is set, return the key as-is for backward compatibility
3431 if ( ! key ) {
3532 return { encrypted : apiKey , iv : '' }
3633 }
37-
38- const iv = randomBytes ( 16 )
39- const cipher = createCipheriv ( 'aes-256-gcm' , key , iv , { authTagLength : 16 } )
40- let encrypted = cipher . update ( apiKey , 'utf8' , 'hex' )
41- encrypted += cipher . final ( 'hex' )
42-
43- const authTag = cipher . getAuthTag ( )
44- const ivHex = iv . toString ( 'hex' )
45-
46- // Format: iv:encrypted:authTag
47- return {
48- encrypted : `${ ivHex } :${ encrypted } :${ authTag . toString ( 'hex' ) } ` ,
49- iv : ivHex ,
50- }
34+ return encrypt ( apiKey , key )
5135}
5236
5337/**
54- * Decrypts an API key using the dedicated API encryption key
55- * @param encryptedValue - The encrypted value in format "iv:encrypted:authTag" or plain text
56- * @returns A promise that resolves to an object containing the decrypted API key
38+ * Decrypts an API key previously produced by { @link encryptApiKey}. Values
39+ * that lack the `iv:ciphertext:authTag` shape are assumed to be legacy plain
40+ * text and returned unchanged.
5741 */
5842export async function decryptApiKey ( encryptedValue : string ) : Promise < { decrypted : string } > {
5943 const parts = encryptedValue . split ( ':' )
60-
61- // Check if this is actually encrypted (contains colons)
6244 if ( parts . length !== 3 ) {
63- // This is a plain text key, return as-is
6445 return { decrypted : encryptedValue }
6546 }
6647
6748 const key = getApiEncryptionKey ( )
68-
69- // If no API encryption key is set, assume it's plain text
7049 if ( ! key ) {
7150 return { decrypted : encryptedValue }
7251 }
7352
74- const ivHex = parts [ 0 ]
75- const authTagHex = parts [ 2 ]
76- const encrypted = parts [ 1 ]
77-
78- if ( ! ivHex || ! encrypted || ! authTagHex ) {
79- throw new Error ( 'Invalid encrypted API key format. Expected "iv:encrypted:authTag"' )
80- }
81-
82- const iv = Buffer . from ( ivHex , 'hex' )
83- const authTag = Buffer . from ( authTagHex , 'hex' )
84-
8553 try {
86- const decipher = createDecipheriv ( 'aes-256-gcm' , key , iv , { authTagLength : 16 } )
87- decipher . setAuthTag ( authTag )
88-
89- let decrypted = decipher . update ( encrypted , 'hex' , 'utf8' )
90- decrypted += decipher . final ( 'utf8' )
91-
92- return { decrypted }
93- } catch ( error : unknown ) {
94- logger . error ( 'API key decryption error:' , {
95- error : error instanceof Error ? error . message : 'Unknown error' ,
96- } )
54+ return await decrypt ( encryptedValue , key )
55+ } catch ( error ) {
56+ logger . error ( 'API key decryption error:' , { error : toError ( error ) . message } )
9757 throw error
9858 }
9959}
@@ -103,15 +63,15 @@ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted
10363 * @returns A new API key string
10464 */
10565export function generateApiKey ( ) : string {
106- return `sim_${ randomBytes ( 24 ) . toString ( 'base64url' ) } `
66+ return `sim_${ generateSecureToken ( 24 ) } `
10767}
10868
10969/**
11070 * Generates a new encrypted API key with the 'sk-sim-' prefix
11171 * @returns A new encrypted API key string
11272 */
11373export function generateEncryptedApiKey ( ) : string {
114- return `sk-sim-${ randomBytes ( 24 ) . toString ( 'base64url' ) } `
74+ return `sk-sim-${ generateSecureToken ( 24 ) } `
11575}
11676
11777/**
@@ -142,5 +102,5 @@ export function isLegacyApiKeyFormat(apiKey: string): boolean {
142102 * @returns The hex-encoded SHA-256 digest
143103 */
144104export function hashApiKey ( plainKey : string ) : string {
145- return createHash ( 'sha256' ) . update ( plainKey , 'utf8' ) . digest ( 'hex' )
105+ return sha256Hex ( plainKey )
146106}
0 commit comments