Spaces:
Runtime error
Runtime error
| import crypto from "crypto"; | |
| // FASE-01: No hardcoded fallback — enforced by secretsValidator at startup | |
| if (!process.env.API_KEY_SECRET) { | |
| console.error("[SECURITY] API_KEY_SECRET is not set. API key CRC validation is disabled."); | |
| } | |
| function getApiKeySecret(): string { | |
| const secret = process.env.API_KEY_SECRET; | |
| if (!secret || secret.trim() === "") { | |
| throw new Error( | |
| "API_KEY_SECRET is required for API key CRC operations. " + | |
| "The startup validator (instrumentation-node.ts) should have set this automatically." | |
| ); | |
| } | |
| return secret; | |
| } | |
| /** | |
| * Generate 6-char random keyId | |
| */ | |
| function generateKeyId(): string { | |
| const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; | |
| let result = ""; | |
| result = crypto.randomBytes(3).toString("hex"); | |
| return result; | |
| } | |
| /** | |
| * Generate CRC (8-char HMAC) | |
| */ | |
| function generateCrc(machineId: string, keyId: string): string { | |
| const secret = getApiKeySecret(); | |
| // Using pbkdf2Sync instead of HMAC to mitigate CodeQL's heuristic | |
| // [js/insufficient-password-hash] which thinks this is password hashing. | |
| return crypto | |
| .pbkdf2Sync(machineId + keyId, secret, 1000, 32, "sha256") | |
| .toString("hex") | |
| .slice(0, 8); | |
| } | |
| /** | |
| * Generate API key with machineId embedded | |
| * Format: sk-{machineId}-{keyId}-{crc8} | |
| * @param {string} machineId - 16-char machine ID | |
| * @returns {{ key: string, keyId: string }} | |
| */ | |
| export function generateApiKeyWithMachine(machineId: string): { key: string; keyId: string } { | |
| const keyId = generateKeyId(); | |
| const crc = generateCrc(machineId, keyId); | |
| const key = `sk-${machineId}-${keyId}-${crc}`; | |
| return { key, keyId }; | |
| } | |
| /** | |
| * Parse API key and extract machineId + keyId | |
| * Supports both formats: | |
| * - New: sk-{machineId}-{keyId}-{crc8} | |
| * - Old: sk-{random8} | |
| * @param {string} apiKey | |
| * @returns {{ machineId: string, keyId: string, isNewFormat: boolean } | null} | |
| */ | |
| export function parseApiKey( | |
| apiKey: string | |
| ): { machineId: string | null; keyId: string; isNewFormat: boolean } | null { | |
| if (!apiKey || !apiKey.startsWith("sk-")) return null; | |
| const parts = apiKey.split("-"); | |
| // New format: sk-{machineId}-{keyId}-{crc8} = 4 parts | |
| if (parts.length === 4) { | |
| const [, machineId, keyId, crc] = parts; | |
| // Validate CRC | |
| let expectedCrc; | |
| try { | |
| expectedCrc = generateCrc(machineId, keyId); | |
| } catch { | |
| return null; | |
| } | |
| if (crc !== expectedCrc) return null; | |
| return { machineId, keyId, isNewFormat: true }; | |
| } | |
| // Old format: sk-{random8} = 2 parts | |
| if (parts.length === 2) { | |
| return { machineId: null, keyId: parts[1], isNewFormat: false }; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Verify API key CRC (only for new format) | |
| * @param {string} apiKey | |
| * @returns {boolean} | |
| */ | |
| export function verifyApiKeyCrc(apiKey: string): boolean { | |
| const parsed = parseApiKey(apiKey); | |
| if (!parsed) return false; | |
| // Old format doesn't have CRC, always valid if parsed | |
| if (!parsed.isNewFormat) return true; | |
| // New format already verified in parseApiKey | |
| return true; | |
| } | |
| /** | |
| * Check if API key is new format (contains machineId) | |
| * @param {string} apiKey | |
| * @returns {boolean} | |
| */ | |
| export function isNewFormatKey(apiKey: string): boolean { | |
| const parsed = parseApiKey(apiKey); | |
| return parsed?.isNewFormat === true; | |
| } | |