// Device authentication and HMAC signing /** * Generates a random device secret (32 bytes, base64 encoded) */ export function generateSecret(): string { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return Buffer.from(bytes).toString("base64"); } /** * Creates an HMAC-SHA256 signature for a message */ export async function signMessage( secret: string, message: string, ): Promise { const key = await crypto.subtle.importKey( "raw", Buffer.from(secret, "base64"), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign( "HMAC", key, Buffer.from(message, "utf-8"), ); return Buffer.from(signature).toString("base64"); } /** * Verifies an HMAC-SHA256 signature using constant-time comparison */ export async function verifySignature( secret: string, message: string, signature: string, ): Promise { const expected = await signMessage(secret, message); // Constant-time comparison to prevent timing attacks if (expected.length !== signature.length) { return false; } const expectedBytes = Buffer.from(expected, "base64"); const signatureBytes = Buffer.from(signature, "base64"); if (expectedBytes.length !== signatureBytes.length) { return false; } let result = 0; for (let i = 0; i < expectedBytes.length; i++) { // biome-ignore lint/style/noNonNullAssertion: loop bounds ensure this exists result |= expectedBytes[i]! ^ signatureBytes[i]!; } return result === 0; } /** * Creates an auth header: "timestamp:signature" * The signature is HMAC-SHA256 of the timestamp string */ export async function createAuthHeader( secret: string, timestamp: number, ): Promise { const timestampStr = timestamp.toString(); const signature = await signMessage(secret, timestampStr); return `${timestampStr}:${signature}`; } /** * Verifies an auth header and checks timestamp freshness * @param secret - Device secret * @param header - Auth header in format "timestamp:signature" * @param maxAge - Maximum age in milliseconds (default: 5 minutes) * @returns true if valid and fresh, false otherwise */ export async function verifyAuthHeader( secret: string, header: string, maxAge = 5 * 60 * 1000, // 5 minutes default ): Promise { const parts = header.split(":"); if (parts.length !== 2) { return false; } const [timestampStr, signature] = parts; if (!timestampStr || !signature) { return false; } // Verify the signature const valid = await verifySignature(secret, timestampStr, signature); if (!valid) { return false; } // Check timestamp freshness const timestamp = Number.parseInt(timestampStr, 10); if (Number.isNaN(timestamp)) { return false; } const now = Date.now(); const age = now - timestamp; // Reject timestamps from the future (with 1 minute tolerance for clock skew) if (age < -60_000) { return false; } // Reject timestamps older than maxAge if (age > maxAge) { return false; } return true; }