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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | /** * API Key Security Implementation * * Provides secure API key generation, hashing, validation, and enumeration prevention. * Uses Argon2id with 64MB memory cost (OWASP 2025 standard). * * OWASP Standards: A02:2021 – Cryptographic Failures, A07:2021 – Identification and Authentication Failures */ import { randomBytes } from "node:crypto"; import { hash, verify } from "@node-rs/argon2"; import { logger } from "@snapback/infrastructure"; /** * API Key security configuration */ export interface APIKeySecurityConfig { /** Prefix for live keys (default: 'sk_live_') */ livePrefix: string; /** Prefix for test keys (default: 'sk_test_') */ testPrefix: string; /** Key entropy length in bytes (default: 32 = 256 bits) */ keyLength: number; /** Enable API key scopes (default: true) */ enableScopes: boolean; /** Enable API key expiration (default: true) */ enableExpiration: boolean; /** Default expiration in days (default: 365) */ expirationDays: number; } /** * Default API key security configuration */ export const defaultAPIKeySecurityConfig: APIKeySecurityConfig = { livePrefix: "sk_live_", testPrefix: "sk_test_", keyLength: 32, // 256-bit entropy enableScopes: true, enableExpiration: true, expirationDays: 365, }; /** * Argon2id hashing configuration (OWASP 2025 standard) */ export interface Argon2idConfig { /** Memory limit in KB (default: 65536 = 64MB) */ memoryLimit: number; /** Number of iterations (default: 3) */ iterations: number; /** Parallelism factor (default: 2) */ parallelism: number; /** Hash type: 'argon2id' (default: 'argon2id') */ type: "argon2id" | "argon2i" | "argon2d"; } /** * OWASP 2025 recommended Argon2id configuration */ export const defaultArgon2idConfig: Argon2idConfig = { memoryLimit: 65536, // 64MB (prevents GPU/ASIC attacks) iterations: 3, // Minimum OWASP standard parallelism: 2, // Creates 2x128MB memory cost type: "argon2id", // Best for all scenarios }; /** * API Key metadata */ export interface APIKeyMetadata { /** API key ID (database reference) */ id: string; /** User ID that owns this key */ userId: string; /** Key creation timestamp */ createdAt: number; /** Key expiration timestamp (or null if no expiration) */ expiresAt: number | null; /** Last used timestamp */ lastUsedAt: number | null; /** IP address that created the key */ createdIpAddress?: string; /** Current usage count */ usageCount: number; /** Key scopes/permissions */ scopes: string[]; /** Is key revoked */ isRevoked: boolean; } /** * Generate a secure API key with prefix * * Requirements: * - Uses crypto.getRandomValues() (cryptographically secure) * - 256-bit entropy minimum * - Base64url encoding (URL-safe) * - Environment prefix (sk_live_ or sk_test_) * - Unique across all time * * @param isLive Is this a live key (true) or test key (false) * @param config API key configuration * @returns Base64url-encoded API key with prefix */ export function generateAPIKey(isLive: boolean, config: Partial<APIKeySecurityConfig> = {}): string { const finalConfig = { ...defaultAPIKeySecurityConfig, ...config }; // Generate cryptographically secure random bytes const randomBuffer = randomBytes(finalConfig.keyLength); // Convert to base64url (URL-safe: - and _ instead of + and /) const base64url = randomBuffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); // Add environment prefix const prefix = isLive ? finalConfig.livePrefix : finalConfig.testPrefix; const apiKey = prefix + base64url; logger.debug("API key generated", { keyLength: apiKey.length, prefix, environment: isLive ? "live" : "test", }); return apiKey; } /** * Hash API key using Argon2id * * Creates a strong hash that prevents rainbow table attacks: * - Random salt generated per key * - 64MB memory cost prevents GPU/ASIC attacks * - Multiple iterations increase time cost * - Suitable for password/key storage (NOT passwords themselves) * * In production, use: * - Node.js crypto.pbkdf2 with Argon2 * - Or @node-rs/argon2 for native performance * - Or argon2 package with proper configuration * * @param plainKey The API key to hash * @param config Argon2id configuration * @returns Promise<hashed key> */ export async function hashAPIKey(plainKey: string, config: Partial<Argon2idConfig> = {}): Promise<string> { const finalConfig = { ...defaultArgon2idConfig, ...config }; // ✅ SB-245 FIXED: Real Argon2id implementation using @node-rs/argon2 // OWASP 2025 compliant with memory-hard protection const secureHash = await hash(plainKey, { memoryCost: finalConfig.memoryLimit, timeCost: finalConfig.iterations, parallelism: finalConfig.parallelism, }); logger.debug("API key hashed with Argon2id", { memoryLimit: finalConfig.memoryLimit, iterations: finalConfig.iterations, parallelism: finalConfig.parallelism, }); return secureHash; } /** * Verify API key hash using constant-time comparison * * Prevents timing attacks by always taking the same time * regardless of where the mismatch occurs * * @param plainKey Plain key to verify * @param hash Stored hash to compare against * @returns Promise<true if matches, false otherwise> */ export async function verifyAPIKeyHash(plainKey: string, hash: string): Promise<boolean> { try { // ✅ SB-245 FIXED: Real Argon2id verification using @node-rs/argon2 // Uses constant-time comparison internally to prevent timing attacks return await verify(hash, plainKey); } catch (error) { logger.warn("API key verification error", { error: error instanceof Error ? error.message : String(error), }); return false; } } /** * Validate API key format * * Checks: * - Correct prefix (sk_live_ or sk_test_) * - Valid base64url characters * - Minimum length * - No special characters * * @param key API key to validate * @returns Validation result */ export function validateAPIKeyFormat(key: string): { valid: boolean; reason?: string; } { // Check prefix const isLiveKey = key.startsWith("sk_live_"); const isTestKey = key.startsWith("sk_test_"); if (!isLiveKey && !isTestKey) { return { valid: false, reason: "Invalid API key prefix (expected sk_live_ or sk_test_)", }; } // Get the actual key part (after prefix) const prefix = isLiveKey ? "sk_live_" : "sk_test_"; const keyPart = key.substring(prefix.length); // Check minimum length if (keyPart.length < 20) { return { valid: false, reason: "API key too short", }; } // Check for valid base64url characters (a-z, A-Z, 0-9, -, _) const base64urlRegex = /^[A-Za-z0-9_-]+$/; if (!base64urlRegex.test(keyPart)) { return { valid: false, reason: "API key contains invalid characters", }; } return { valid: true }; } /** * Extract public key ID for enumeration prevention * * Returns only last 4 characters for display purposes * This prevents full key enumeration while allowing partial identification * * @param key Full API key * @returns Masked key for display (first 8 chars + ...last 4 chars) */ export function maskAPIKeyForDisplay(key: string): string { const prefix = key.substring(0, 8); // "sk_live_" or "sk_test_" const lastFour = key.substring(key.length - 4); return `${prefix}...${lastFour}`; } /** * Check for API key enumeration attempts * * Tracks failed validation attempts to detect brute force: * - Per IP address * - Per key (if partially known) * - Global rate limiting * * @param ipAddress Client IP address * @param failureCount Number of failures so far * @param timeWindowMs Time window in milliseconds * @returns Object with enumeration detection result */ export function detectKeyEnumerationAttempt( ipAddress: string, failureCount: number, timeWindowMs = 60000, // 1 minute ): { isEnumeration: boolean; shouldBlock: boolean; reason?: string; } { // More than 10 failures per minute = enumeration attempt if (failureCount > 10) { logger.warn("Possible key enumeration detected", { ipAddress, failureCount, timeWindow: `${timeWindowMs / 1000}s`, }); return { isEnumeration: true, shouldBlock: failureCount > 50, // Block after 50 attempts reason: "Excessive key validation failures", }; } return { isEnumeration: false, shouldBlock: false, }; } /** * Generate constant response time for key validation * * Helps prevent timing attacks by ensuring similar response times * for successful and failed key validations * * @param baseDelayMs Minimum delay in milliseconds * @param variance Variance in milliseconds (±) * @returns Actual delay to use */ export function getConstantTimeDelay(baseDelayMs = 100, variance = 20): number { // Add random variance to prevent timing attacks const randomVariance = Math.random() * variance - variance / 2; return Math.max(1, baseDelayMs + randomVariance); } /** * Validate API key scopes * * Checks if key has required permissions for action * * @param keyScopes Scopes on the API key * @param requiredScope Scope needed for this action * @returns true if key has required scope, false otherwise */ export function validateAPIKeyScope(keyScopes: string[], requiredScope: string): boolean { // Admin scope grants all access if (keyScopes.includes("admin:all")) { return true; } // Check for exact scope match if (keyScopes.includes(requiredScope)) { return true; } // Check for wildcard scopes (e.g., "snapshots:*") const [resource] = requiredScope.split(":"); if (keyScopes.includes(`${resource}:*`)) { return true; } logger.warn("API key scope check failed", { keyScopes, requiredScope, }); return false; } /** * Log API key security event * * Records security-relevant events for audit trail * * @param event Event type * @param details Event details */ export function logAPIKeyEvent( event: "created" | "used" | "rotated" | "revoked" | "access_denied" | "enumeration_detected", details: { keyId?: string; userId?: string; ipAddress?: string; reason?: string; }, ): void { const logData = { event: `API_KEY_${event.toUpperCase()}`, keyId: details.keyId?.substring(0, 8), // Log first 8 chars only userId: details.userId, ipAddress: details.ipAddress, reason: details.reason, timestamp: new Date().toISOString(), }; const severity = event === "created" || event === "used" ? "info" : "warn"; if (severity === "info") { logger.info(`API key ${event}`, logData); } else { logger.warn(`API key ${event}`, logData); } } |