import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateShortId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { decryptApiKey, encryptApiKey, generateApiKey, generateEncryptedApiKey, hashApiKey, isEncryptedApiKeyFormat, isLegacyApiKeyFormat, } from '@/lib/api-key/crypto' import { env } from '@/lib/core/config/env' const logger = createLogger('ApiKeyAuth') /** * API key authentication utilities supporting both legacy plain text keys * and modern encrypted keys for gradual migration without breaking existing keys */ /** * Checks if a stored key is in the new encrypted format * @param storedKey - The key stored in the database * @returns true if the key is encrypted, false if it's plain text */ export function isEncryptedKey(storedKey: string): boolean { // Check if it follows the encrypted format: iv:encrypted:authTag return storedKey.includes(':') && storedKey.split(':').length === 3 } /** * Encrypts an API key for secure storage * @param apiKey - The plain text API key to encrypt * @returns Promise - The encrypted key */ async function encryptApiKeyForStorage(apiKey: string): Promise { try { const { encrypted } = await encryptApiKey(apiKey) return encrypted } catch (error) { logger.error('API key encryption error:', { error }) throw new Error('Failed to encrypt API key') } } /** * Creates a new API key * @param useStorage - Whether to encrypt the key before storage (default: true) * @returns Promise<{key: string, encryptedKey?: string}> - The plain key and optionally encrypted version */ export async function createApiKey(useStorage = true): Promise<{ key: string encryptedKey?: string }> { try { const hasEncryptionKey = env.API_ENCRYPTION_KEY !== undefined const plainKey = hasEncryptionKey ? generateEncryptedApiKey() : generateApiKey() if (useStorage) { const encryptedKey = await encryptApiKeyForStorage(plainKey) return { key: plainKey, encryptedKey } } return { key: plainKey } } catch (error) { logger.error('API key creation error:', { error }) throw new Error('Failed to create API key') } } /** * Decrypts an API key from storage for display purposes * @param encryptedKey - The encrypted API key from the database * @returns Promise - The decrypted API key */ async function decryptApiKeyFromStorage(encryptedKey: string): Promise { try { const { decrypted } = await decryptApiKey(encryptedKey) return decrypted } catch (error) { logger.error('API key decryption error:', { error }) throw new Error('Failed to decrypt API key') } } /** * Gets the last 4 characters of an API key for display purposes * @param apiKey - The API key (plain text) * @returns string - The last 4 characters */ export function getApiKeyLast4(apiKey: string): string { return apiKey.slice(-4) } /** * Gets the display format for an API key showing prefix and last 4 characters * @param encryptedKey - The encrypted API key from the database * @returns Promise - The display format like "sk-sim-...r6AA" */ export async function getApiKeyDisplayFormat(encryptedKey: string): Promise { try { if (isEncryptedKey(encryptedKey)) { const decryptedKey = await decryptApiKeyFromStorage(encryptedKey) return formatApiKeyForDisplay(decryptedKey) } // For plain text keys (legacy), format directly return formatApiKeyForDisplay(encryptedKey) } catch (error) { logger.error('Failed to format API key for display:', { error }) return '****' } } /** * Formats an API key for display showing prefix and last 4 characters * @param apiKey - The API key (plain text) * @returns string - The display format like "sk-sim-...r6AA" or "sim_...r6AA" */ export function formatApiKeyForDisplay(apiKey: string): string { if (isEncryptedApiKeyFormat(apiKey)) { // For sk-sim- format: "sk-sim-...r6AA" const last4 = getApiKeyLast4(apiKey) return `sk-sim-...${last4}` } if (isLegacyApiKeyFormat(apiKey)) { // For sim_ format: "sim_...r6AA" const last4 = getApiKeyLast4(apiKey) return `sim_...${last4}` } // Unknown format, just show last 4 const last4 = getApiKeyLast4(apiKey) return `...${last4}` } /** * Gets the last 4 characters of an encrypted API key by decrypting it first * @param encryptedKey - The encrypted API key from the database * @returns Promise - The last 4 characters */ async function getEncryptedApiKeyLast4(encryptedKey: string): Promise { try { if (isEncryptedKey(encryptedKey)) { const decryptedKey = await decryptApiKeyFromStorage(encryptedKey) return getApiKeyLast4(decryptedKey) } // For plain text keys (legacy), return last 4 directly return getApiKeyLast4(encryptedKey) } catch (error) { logger.error('Failed to get last 4 characters of API key:', { error }) return '****' } } /** * Validates API key format (basic validation) * @param apiKey - The API key to validate * @returns boolean - true if the format appears valid */ export function isValidApiKeyFormat(apiKeyValue: string): boolean { return typeof apiKeyValue === 'string' && apiKeyValue.length > 10 && apiKeyValue.length < 200 } export async function createWorkspaceApiKey(params: { workspaceId: string userId: string name: string }) { const existingKey = await db .select({ id: apiKey.id }) .from(apiKey) .where( and( eq(apiKey.workspaceId, params.workspaceId), eq(apiKey.name, params.name), eq(apiKey.type, 'workspace') ) ) .limit(1) if (existingKey.length > 0) { throw new Error( `A workspace API key named "${params.name}" already exists. Choose a different name.` ) } const { key: plainKey, encryptedKey } = await createApiKey(true) if (!encryptedKey) { throw new Error('Failed to encrypt API key for storage') } const [newKey] = await db .insert(apiKey) .values({ id: generateShortId(), workspaceId: params.workspaceId, userId: params.userId, createdBy: params.userId, name: params.name, key: encryptedKey, keyHash: hashApiKey(plainKey), type: 'workspace', createdAt: new Date(), updatedAt: new Date(), }) .returning({ id: apiKey.id, name: apiKey.name, createdAt: apiKey.createdAt }) return { id: newKey.id, name: newKey.name, key: plainKey, createdAt: newKey.createdAt } }