import crypto from 'crypto'; /** * Checksum length in hex characters (16 chars = 64 bits of entropy) */ export const CHECKSUM_LENGTH = 16; /** * Generate share link checksum for location * Uses HMAC-SHA256 for cryptographic integrity * * SECURITY: Prevents location ID enumeration while allowing stateless validation */ export function generateShareChecksum(locationId: string): string { const secret = process.env.SHARE_LINK_SECRET; if (!secret) { throw new Error('SHARE_LINK_SECRET environment variable not configured'); } return crypto .createHmac('sha256', secret) .update(locationId) .digest('hex') .substring(0, CHECKSUM_LENGTH); } /** * Validate share link checksum * Uses constant-time comparison to prevent timing attacks * * @param locationId - The location ID from URL * @param providedChecksum - The checksum from URL * @returns true if checksum is valid */ export function validateShareChecksum( locationId: string, providedChecksum: string ): boolean { try { const expectedChecksum = generateShareChecksum(locationId); // Convert to buffers for timing-safe comparison const expected = Buffer.from(expectedChecksum); const provided = Buffer.from(providedChecksum); // Length check (prevents timing attack on different lengths) if (expected.length !== provided.length) { return false; } // Constant-time comparison (prevents timing attacks) return crypto.timingSafeEqual(expected, provided); } catch { return false; } } /** * Generate combined location ID with checksum appended * @param locationId - The MongoDB location ID (24 chars) * @returns Combined ID: locationId + checksum (40 chars total) */ export function generateShareId(locationId: string): string { const checksum = generateShareChecksum(locationId); return locationId + checksum; } /** * Extract location ID and checksum from combined share ID * @param shareId - Combined ID (locationId + checksum) * @returns Object with locationId and checksum, or null if invalid format */ export function extractShareId(shareId: string): { locationId: string; checksum: string } | null { // MongoDB ObjectID is 24 chars, checksum is 16 chars = 40 total const expectedLength = 24 + CHECKSUM_LENGTH; if (shareId.length !== expectedLength) { return null; } const locationId = shareId.substring(0, 24); const checksum = shareId.substring(24); return { locationId, checksum }; }