diff --git a/src/index.ts b/src/index.ts index b2bcd20..1098cc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -785,6 +785,7 @@ export { I32, F32, F64, + type TypedArray, type Encoding, type HexEncoding, type Base64Encoding, diff --git a/src/utils.ts b/src/utils.ts index 58936bb..2ca75f7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,49 +1,121 @@ +// 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; @@ -54,6 +126,48 @@ export const assertBytes = (bytes: Uint8Array, length?: number, name: string = " } } +/** + * 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' && @@ -71,8 +185,25 @@ 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]; @@ -80,6 +211,14 @@ export const bytesToHex = (bytes: Uint8Array, length?: number): string => { 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}`); @@ -96,6 +235,20 @@ export const hexToBytes = (hex: string): Uint8Array => { 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); @@ -113,6 +266,16 @@ export const bytesToBase64 = (bytes: Uint8Array, encoding: Base64Encoding | Base } } +/** + * 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}`); @@ -134,6 +297,22 @@ export const base64ToBytes = (b64: string): Uint8Array => { 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': @@ -148,6 +327,22 @@ export const decodeBytes = (encoded: string, encoding: Encoding): Uint8Array => } } +/** + * 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':