/** * Cryptographic utilities for security-sensitive operations * * These utilities are designed to prevent timing attacks and other * side-channel vulnerabilities in authentication and validation code. */ /** * Performs a constant-time comparison of two strings. * * This function is designed to prevent timing attacks where an attacker * could measure response times to determine how many characters of a * secret value they have guessed correctly. * * The comparison always takes the same amount of time regardless of: * - How many characters match * - Where the first difference occurs * - The length difference between strings * * @param a - First string to compare * @param b - Second string to compare * @returns true if the strings are equal, false otherwise * * @example * ```typescript * // Use for token validation * const isValid = constantTimeCompare(userToken, storedToken) * * // Use for API key validation * const isValidKey = constantTimeCompare(providedKey, expectedKey) * ``` */ export function constantTimeCompare(a: string, b: string): boolean { // Convert strings to Uint8Array for byte-level comparison const encoder = new TextEncoder() const bufA = encoder.encode(a) const bufB = encoder.encode(b) // If lengths differ, we still need to do constant-time work // to avoid leaking length information through timing const lengthsMatch = bufA.length === bufB.length // Use the longer length to ensure we always do the same amount of work // regardless of which string is longer const maxLength = Math.max(bufA.length, bufB.length) // Perform XOR comparison on all bytes // Using XOR ensures each byte is compared in constant time let result = 0 for (let i = 0; i < maxLength; i++) { // Use 0 as default for out-of-bounds access // This ensures we always iterate maxLength times const byteA = i < bufA.length ? bufA[i] : 0 const byteB = i < bufB.length ? bufB[i] : 0 // XOR the bytes - any difference will set bits in result result |= byteA! ^ byteB! } // Both conditions must be true: // 1. Lengths must match (checked separately to avoid short-circuit) // 2. All bytes must be equal (result === 0) return lengthsMatch && result === 0 } /** * Performs a constant-time comparison of two Uint8Arrays. * * Similar to constantTimeCompare but for binary data. * * @param a - First buffer to compare * @param b - Second buffer to compare * @returns true if the buffers are equal, false otherwise */ export function constantTimeCompareBuffers(a: Uint8Array, b: Uint8Array): boolean { // If lengths differ, we still need to do constant-time work const lengthsMatch = a.length === b.length const maxLength = Math.max(a.length, b.length) let result = 0 for (let i = 0; i < maxLength; i++) { const byteA = i < a.length ? a[i] : 0 const byteB = i < b.length ? b[i] : 0 result |= byteA! ^ byteB! } return lengthsMatch && result === 0 } /** * Generates a cryptographically secure random token. * * Uses the Web Crypto API for secure random number generation, * which is available in both browser and Cloudflare Workers environments. * * @param length - The number of random bytes (output will be 2x this in hex) * @returns A hex-encoded random string * * @example * ```typescript * const sessionToken = generateSecureToken(32) // 64 hex characters * const apiKey = generateSecureToken(16) // 32 hex characters * ``` */ export function generateSecureToken(length: number = 32): string { const bytes = new Uint8Array(length) crypto.getRandomValues(bytes) // Convert to hex string return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join('') } /** * Generates a URL-safe base64-encoded secure random token. * * @param length - The number of random bytes * @returns A URL-safe base64-encoded random string * * @example * ```typescript * const token = generateSecureTokenBase64(32) * ``` */ export function generateSecureTokenBase64(length: number = 32): string { const bytes = new Uint8Array(length) crypto.getRandomValues(bytes) // Convert to base64 and make URL-safe const base64 = btoa(String.fromCharCode(...bytes)) return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') }