import { hmacSign, sha256 } from "@wrnexus/encryption"; const encoder = new TextEncoder(); const MAX_RANDOM_ROUNDS = 128; export function bytesToBase64Url(bytes: Uint8Array): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } export function base64UrlToBytes(value: string): Uint8Array { if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) { throw new TypeError("Invalid base64url value"); } const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); let binary: string; try { binary = atob(padded); } catch { throw new TypeError("Invalid base64url value"); } const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); if (bytesToBase64Url(bytes) !== value) throw new TypeError("Invalid base64url value"); return bytes; } export function randomToken(random: (length: number) => Uint8Array, bytes = 32): string { if (!Number.isInteger(bytes) || bytes <= 0) { throw new RangeError("random token byte length must be a positive integer"); } const value = random(bytes); if (!(value instanceof Uint8Array) || value.length !== bytes) { throw new TypeError(`random byte provider must return exactly ${bytes} bytes`); } return bytesToBase64Url(value); } export function randomDigits(random: (length: number) => Uint8Array, length = 6): string { return randomFromAlphabet(random, "0123456789", length); } export function randomReadableCode(random: (length: number) => Uint8Array, length = 10): string { return randomFromAlphabet(random, "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", length); } function randomFromAlphabet( random: (length: number) => Uint8Array, alphabet: string, length: number, ): string { if (!Number.isInteger(length) || length < 0) throw new RangeError("length must be non-negative"); if (!alphabet.length || alphabet.length > 256 || new Set(alphabet).size !== alphabet.length) { throw new TypeError("alphabet must contain 1 to 256 unique characters"); } if (length === 0) return ""; const limit = Math.floor(256 / alphabet.length) * alphabet.length; let output = ""; for (let round = 0; output.length < length && round < MAX_RANDOM_ROUNDS; round += 1) { const requested = Math.max(16, (length - output.length) * 2); const bytes = random(requested); if (!(bytes instanceof Uint8Array) || bytes.length !== requested) { throw new TypeError(`random byte provider must return exactly ${requested} bytes`); } for (const byte of bytes) { if (byte >= limit) continue; output += alphabet[byte % alphabet.length]; if (output.length === length) break; } } if (output.length !== length) { throw new Error("WRN-AUTH-RANDOM-SOURCE-REJECTED"); } return output; } export async function hashSecret(value: string, secret: string): Promise { return hmacSign(value, secret); } export async function fingerprint(value: string): Promise { return sha256(value); } export async function constantTimeEqual(left: string, right: string): Promise { const leftBytes = encoder.encode(left); const rightBytes = encoder.encode(right); const length = Math.max(leftBytes.length, rightBytes.length); let diff = leftBytes.length ^ rightBytes.length; for (let index = 0; index < length; index += 1) { diff |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0); } return diff === 0; }