All files / api/src/utils api-key-encryption.ts

93.93% Statements 31/33
75% Branches 3/4
100% Functions 3/3
93.93% Lines 31/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 551x 1x                         1x 1x   1x 7x 7x 7x 7x     1x 3x 3x 3x   3x 3x   3x 3x   1x 4x 4x       4x 4x 4x 4x 4x 4x 4x   4x 4x   4x 4x  
import crypto from 'node:crypto';
import { getAiProviderApiKeyEncryptionKey } from '@api/constants';
import type { AppContext } from '@api/types/hono';
 
/**
 * AES-256-GCM encryption for stored AI provider API keys.
 *
 * Extracted from the Supabase connector so every storage backend produces and
 * reads the same ciphertext: a key written by one backend has to stay readable
 * after a migration to the other.
 *
 * `node:crypto` resolves on Workers through `nodejs_compat_v2`.
 */
 
const ALGORITHM = 'aes-256-gcm';
const AAD = 'api-key';
 
const derivedKey = (c: AppContext): Buffer =>
  crypto
    .createHash('sha256')
    .update(getAiProviderApiKeyEncryptionKey(c))
    .digest();
 
/** Returns `iv:authTag:ciphertext`, all hex. */
export const encryptAPIKey = (c: AppContext, plaintext: string): string => {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, derivedKey(c), iv);
  cipher.setAAD(Buffer.from(AAD));
 
  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');
 
  return `${iv.toString('hex')}:${cipher.getAuthTag().toString('hex')}:${encrypted}`;
};
 
export const decryptAPIKey = (c: AppContext, encryptedData: string): string => {
  const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
  if (!ivHex || !authTagHex || !encrypted) {
    throw new Error('Invalid encrypted data format');
  }
 
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    derivedKey(c),
    Buffer.from(ivHex, 'hex'),
  );
  decipher.setAAD(Buffer.from(AAD));
  decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
 
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
 
  return decrypted;
};