// TypedArray is a union type of all the standard typed array types in JavaScript. This allows us to write // functions that can accept any typed array or an ArrayBuffer and convert it to the appropriate view. export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | Uint16Array | Int16Array | Uint32Array | Int32Array; /** * Converts the given ArrayBuffer or TypedArray to a Uint8Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns A Uint8Array representing the same data. */ export const U8 = (arr: ArrayBuffer | TypedArray): Uint8Array => { if (arr instanceof ArrayBuffer) return new Uint8Array(arr); return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); } /** * Converts the given ArrayBuffer or TypedArray to an Int8Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns An Int8Array representing the same data. */ export const I8 = (arr: ArrayBuffer | TypedArray): Int8Array => { if (arr instanceof ArrayBuffer) return new Int8Array(arr); return new Int8Array(arr.buffer, arr.byteOffset, arr.byteLength); } /** * Converts the given ArrayBuffer or TypedArray to a Uint16Array. If a TypedArray is provided, * the byte length is adjusted to account for the 2 bytes per value in a Uint16Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns A Uint16Array representing the same data. */ export const U16 = (arr: ArrayBuffer | TypedArray): Uint16Array => { if (arr instanceof ArrayBuffer) return new Uint16Array(arr); return new Uint16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2)); } /** * Converts the given ArrayBuffer or TypedArray to an Int16Array. If a TypedArray is provided, * the byte length is adjusted to account for the 2 bytes per value in an Int16Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns An Int16Array representing the same data. */ export const I16 = (arr: ArrayBuffer | TypedArray): Int16Array => { if (arr instanceof ArrayBuffer) return new Int16Array(arr); return new Int16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2)); } /** * Converts the given ArrayBuffer or TypedArray to a Uint32Array. If a TypedArray is provided, * the byte length is adjusted to account for the 4 bytes per value in a Uint32Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns A Uint32Array representing the same data. */ export const U32 = (arr: ArrayBuffer | TypedArray): Uint32Array => { if (arr instanceof ArrayBuffer) return new Uint32Array(arr); return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); } /** * Converts the given ArrayBuffer or TypedArray to an Int32Array. If a TypedArray is provided, * the byte length is adjusted to account for the 4 bytes per value in an Int32Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns An Int32Array representing the same data. */ export const I32 = (arr: ArrayBuffer | TypedArray): Int32Array => { if (arr instanceof ArrayBuffer) return new Int32Array(arr); return new Int32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); } /** * Converts the given ArrayBuffer or TypedArray to a Float32Array. If a TypedArray is provided, * the byte length is adjusted to account for the 4 bytes per value in a Float32Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns A Float32Array representing the same data. */ export const F32 = (arr: ArrayBuffer | TypedArray): Float32Array => { if (arr instanceof ArrayBuffer) return new Float32Array(arr); return new Float32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); } /** * Converts the given ArrayBuffer or TypedArray to a Float64Array. If a TypedArray is provided, * the byte length is adjusted to account for the 8 bytes per value in a Float64Array. * * @param arr The ArrayBuffer or TypedArray to convert. * @returns A Float64Array representing the same data. */ export const F64 = (arr: ArrayBuffer | TypedArray): Float64Array => { if (arr instanceof ArrayBuffer) return new Float64Array(arr); return new Float64Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 8)); } /** * Tests whether the provided value is a Uint8Array or a view of an ArrayBuffer that is a * Uint8Array. This is used to validate that a given value can be treated as raw bytes for * encoding/decoding operations. * * @param bytes The value to test. * @returns True if the value is a Uint8Array or a view of an ArrayBuffer that is a Uint8Array, false otherwise. */ export const isBytes = (bytes: unknown): boolean => { return bytes instanceof Uint8Array || (ArrayBuffer.isView(bytes) && bytes.constructor.name === "Uint8Array"); } /** * Asserts that the provided value is a Uint8Array of the specified length (if provided). Throws a * TypeError if the assertion fails. * * @param bytes The value to check. * @param length The expected length of the Uint8Array. * @param name The name of the variable being checked (for error messages). */ export const assertBytes = (bytes: Uint8Array, length?: number, name: string = ""): void => { const valid = isBytes(bytes); const sized = (typeof length !== 'undefined') ? (bytes.byteLength === length) : true; if (!valid || !sized) { const expected = typeof length !== 'undefined' ? `Uint8Array of length ${length}` : 'Uint8Array'; const actual = valid ? `Uint8Array of length ${bytes.byteLength}` : typeof bytes; throw new TypeError(`Expected ${name} to be ${expected}, got ${actual}`); } } /** * Test for equality of two Uint8Arrays. Returns true if the arrays are equal, false otherwise. * * Note: This function is not designed to be resistant to timing attacks. For security-sensitive * comparisons (e.g., cryptographic keys, hashes), use `constantTimeEqualBytes` instead. * * @param a The first Uint8Array to compare. * @param b The second Uint8Array to compare. * @returns True if the arrays are equal, false otherwise. */ export const equalBytes = (a: Uint8Array, b: Uint8Array): boolean => { if (a.byteLength !== b.byteLength) return false; for (let i = 0; i < a.byteLength; i++) { if (a[i] !== b[i]) return false; } return true; } /** * Performs a constant-time comparison of two Uint8Arrays to prevent timing attacks. Returns true * if the arrays are equal, false otherwise. * * This function should be used for security-sensitive comparisons (e.g., cryptographic keys, * hashes) to mitigate timing attacks. For general use where performance is a concern and security * is not an issue, `equalBytes` may be more efficient. * * NB: This function is not truly constant-time in JavaScript due to the nature of the language and * runtime, but it is designed to minimize timing differences based on the content of the arrays. * * @param a The first Uint8Array to compare. * @param b The second Uint8Array to compare. * @returns True if the arrays are equal, false otherwise. */ export const constantTimeEqualBytes = (a: Uint8Array, b: Uint8Array): boolean => { if (a.byteLength !== b.byteLength) return false; let result = 0; for (let i = 0; i < a.byteLength; i++) { result |= a[i] ^ b[i]; } return result === 0; } const hasHexMethods = (() => // @ts-ignore testing for builtins 'toHex' in Uint8Array.from([]) && typeof Uint8Array.from([])['toHex'] === 'function' && 'fromHex' in Uint8Array && typeof Uint8Array.fromHex === 'function')(); // Array where index 0xf0 (240) is mapped to string 'f0' const hexes = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0') ); export type HexEncoding = 'hex'; export type Base64Encoding = 'base64'; export type Base64RawEncoding = 'base64raw'; export type Base64URLEncoding = 'base64url'; export type Base64RawURLEncoding = 'base64urlraw'; export type Encoding = HexEncoding | Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding; /** * Converts a Uint8Array of bytes to a hexadecimal string. If the environment supports built-in * hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual * conversion is performed. * * If a length is provided, the function will assert that the input bytes have the expected * length (in bytes). The length parameter is optional and can be used to enforce that the * input bytes match an expected size for encoding/decoding operations. * * @param bytes The Uint8Array of bytes to convert to a hexadecimal string. * @param length Optional expected length of the input bytes (in bytes). * @returns The hexadecimal string representation of the input bytes. */ export const bytesToHex = (bytes: Uint8Array, length?: number): string => { assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes"); if (hasHexMethods) { // @ts-ignore using built-in hex methods if available return bytes.toHex(); } let hex = ''; for (const byte of bytes) { hex += hexes[byte]; } return hex; }; /** * Converts a hexadecimal string to a Uint8Array of bytes. If the environment supports built-in * hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual * conversion is performed. * * @param hex The hexadecimal string to convert to a Uint8Array of bytes. * @returns The Uint8Array of bytes represented by the hexadecimal string. */ export const hexToBytes = (hex: string): Uint8Array => { if (typeof hex !== 'string' || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) { throw new TypeError(`Expected hex string of even length, got ${hex}`); } if (hasHexMethods) { // @ts-ignore using built-in hex methods if available return Uint8Array.fromHex(hex); } const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); } return bytes; } /** * Converts a Uint8Array of bytes to a base64 string using the specified encoding variant. The function * asserts that the input is a valid Uint8Array and optionally checks for an expected length (in bytes). * * The `encoding` parameter specifies the base64 variant to use: * - 'base64': Standard base64 encoding with padding. * - 'base64raw': Standard base64 encoding without padding. * - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_'). * - 'base64urlraw': URL-safe base64 encoding without padding. * * @param bytes The Uint8Array of bytes to convert to a base64 string. * @param encoding The base64 encoding variant to use. * @returns The base64 string representation of the input bytes. */ export const bytesToBase64 = (bytes: Uint8Array, encoding: Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding): string => { assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes"); const binary = String.fromCharCode(...bytes); switch (encoding) { case 'base64': return btoa(binary); case 'base64raw': return btoa(binary).replace(/=+$/, ''); case 'base64url': return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_'); case 'base64urlraw': return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); default: throw new TypeError(`Unsupported encoding: ${encoding}`); } } /** * Converts a base64 string to a Uint8Array of bytes. The function accepts both standard and URL-safe * base64 variants, and normalizes the input by replacing URL-safe characters and adding padding if necessary. * * The function asserts that the input is a valid base64 string after normalization. It then decodes the * base64 string to binary and converts it to a Uint8Array of bytes. * * @param b64 The base64 string to convert to a Uint8Array of bytes. * @returns The Uint8Array of bytes represented by the base64 string. */ export const base64ToBytes = (b64: string): Uint8Array => { if (typeof b64 !== "string") { throw new TypeError(`Expected base64 string, got ${b64}`); } // Accept URL-safe base64 by replacing '-' with '+' and '_' with '/' let normalized = b64.replace(/-/g, "+").replace(/_/g, "/"); // Pad with '=' to make length a multiple of 4 if (normalized.length % 4 !== 0) { normalized += "=".repeat(4 - (normalized.length % 4)); } if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) { throw new TypeError(`Expected base64 string, got ${b64}`); } const binary = atob(normalized); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; }; /** * Decodes a string encoded in the specified encoding variant to a Uint8Array of bytes. The function * supports the following encoding variants: * - 'hex': Hexadecimal encoding (e.g., "deadbeef"). * - 'base64': Standard base64 encoding with padding. * - 'base64raw': Standard base64 encoding without padding. * - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_'). * - 'base64urlraw': URL-safe base64 encoding without padding. * * The function asserts that the input string is valid for the specified encoding and then decodes * it to a Uint8Array of bytes. If the encoding is not supported, a TypeError is thrown. * * @param encoded The encoded string to decode. * @param encoding The encoding variant of the input string. * @returns The Uint8Array of bytes represented by the encoded string. */ export const decodeBytes = (encoded: string, encoding: Encoding): Uint8Array => { switch (encoding) { case 'hex': return hexToBytes(encoded); case 'base64': case 'base64raw': case 'base64url': case 'base64urlraw': return base64ToBytes(encoded); default: throw new TypeError(`Unsupported encoding: ${encoding}`); } } /** * Encodes a Uint8Array of bytes to a string using the specified encoding variant. The function supports * the following encoding variants: * - 'hex': Hexadecimal encoding (e.g., "deadbeef"). * - 'base64': Standard base64 encoding with padding. * - 'base64raw': Standard base64 encoding without padding. * - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_'). * - 'base64urlraw': URL-safe base64 encoding without padding. * * The function asserts that the input is a valid Uint8Array of bytes and then encodes it to a string * using the specified encoding variant. If the encoding is not supported, a TypeError is thrown. * * @param bytes The Uint8Array of bytes to encode. * @param encoding The encoding variant to use. * @returns The encoded string. */ export const encodeBytes = (bytes: Uint8Array, encoding: Encoding): string => { switch (encoding) { case 'hex': return bytesToHex(bytes); case 'base64': case 'base64raw': case 'base64url': case 'base64urlraw': return bytesToBase64(bytes, encoding); default: throw new TypeError(`Unsupported encoding: ${encoding}`); } }