one
Version:
One is a React framework for web, iOS, and Android apps.
137 lines (127 loc) • 4.09 kB
text/typescript
// 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
}
}