97 lines
3.6 KiB
TypeScript
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;
|
|
}
|