Files
WRNexusJS/packages/auth/src/crypto.ts
T
2026-07-29 12:51:10 +05:30

97 lines
3.6 KiB
TypeScript

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<string> {
return hmacSign(value, secret);
}
export async function fingerprint(value: string): Promise<string> {
return sha256(value);
}
export async function constantTimeEqual(left: string, right: string): Promise<boolean> {
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;
}