UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

137 lines (127 loc) • 4.09 kB
// shared crypto helpers: pure and platform-free, so the formatting, the // fill semantics, and the install-only-when-missing rule unit-test in node. // the only platform part is the random source each entry injects. export const MAX_RANDOM_BYTES = 65536 // fill writes secure random bytes over all of `bytes` in place; randomUUID // returns a version 4 uuid. both are synchronous because the web apis are. export type RandomSource = { fill(bytes: Uint8Array<ArrayBuffer>): void randomUUID(): string } const HEX_DIGITS = '0123456789abcdef' // rfc 4122 section 4.4: 16 random bytes with the version nibble set to 4 // and the variant bits to 10. copies the input so caller buffers stay // untouched. export function formatUuidV4(bytes: Uint8Array): string { if (bytes.length !== 16) { throw new RangeError(`randomUUID: expected 16 random bytes, got ${bytes.length}.`) } const versioned = bytes.slice() versioned[6] = (versioned[6] & 0x0f) | 0x40 versioned[8] = (versioned[8] & 0x3f) | 0x80 const hex = (index: number) => HEX_DIGITS[versioned[index] >> 4] + HEX_DIGITS[versioned[index] & 15] return ( hex(0) + hex(1) + hex(2) + hex(3) + '-' + hex(4) + hex(5) + '-' + hex(6) + hex(7) + '-' + hex(8) + hex(9) + '-' + hex(10) + hex(11) + hex(12) + hex(13) + hex(14) + hex(15) ) } // integer TypedArrays only, matching the platform getRandomValues: float // arrays and DataView throw a TypeError, and fills over 64k throw a // QuotaExceededError-named Error (Hermes has no DOMException). fills // byte-wise through the view's own offset and length, so sub-array views // and big-int arrays both work. const INTEGER_ARRAY_TAGS = new Set([ '[object Int8Array]', '[object Uint8Array]', '[object Uint8ClampedArray]', '[object Int16Array]', '[object Uint16Array]', '[object Int32Array]', '[object Uint32Array]', '[object BigInt64Array]', '[object BigUint64Array]', ]) function quotaExceededError(): Error { const error = new Error( `crypto.getRandomValues: byteLength exceeds ${MAX_RANDOM_BYTES}.` ) error.name = 'QuotaExceededError' return error } export function fillRandomValues<T extends ArrayBufferView>( view: T, source: RandomSource ): T { if (!INTEGER_ARRAY_TAGS.has(Object.prototype.toString.call(view))) { throw new TypeError( `crypto.getRandomValues: expected an integer TypedArray, got ${Object.prototype.toString.call(view)}.` ) } if (view.byteLength > MAX_RANDOM_BYTES) { throw quotaExceededError() } if (view.byteLength === 0) { return view } // like the web api, a view over shared memory is refused. if (!(view.buffer instanceof ArrayBuffer)) { throw new TypeError('crypto.getRandomValues: shared memory is not supported.') } source.fill(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)) return view } // installs getRandomValues and randomUUID onto target.crypto, but only the // pieces that are missing: an existing native implementation always wins, // and each method is checked independently. the target shape is structural // so both the real global and plain test objects fit without a cast. export type CryptoPolyfillTarget = { crypto?: { getRandomValues?: unknown randomUUID?: unknown } | null } export function installCryptoPolyfill( source: RandomSource, target: CryptoPolyfillTarget = globalThis ): void { const existing = target.crypto if ( existing != null && typeof existing.getRandomValues === 'function' && typeof existing.randomUUID === 'function' ) { return } const getRandomValues = <T extends ArrayBufferView>(view: T): T => fillRandomValues(view, source) const randomUUID = (): string => source.randomUUID() if (existing == null) { target.crypto = { getRandomValues, randomUUID } return } if (typeof existing.getRandomValues !== 'function') { existing.getRandomValues = getRandomValues } if (typeof existing.randomUUID !== 'function') { existing.randomUUID = randomUUID } }