1import { getRandomBytes } from 'expo-random';
2
3const MAX_RANDOM_BYTES = 65536;
4
5type IntegerArray =
6  | Int8Array
7  | Uint8Array
8  | Int16Array
9  | Uint16Array
10  | Int32Array
11  | Uint32Array
12  | Uint8ClampedArray;
13
14type IntegerArrayConstructor =
15  | Int8ArrayConstructor
16  | Uint8ArrayConstructor
17  | Int16ArrayConstructor
18  | Uint16ArrayConstructor
19  | Int32ArrayConstructor
20  | Uint32ArrayConstructor
21  | Uint8ClampedArrayConstructor;
22
23/**
24 * An implementation of Crypto.getRandomValues that uses expo-random's secure random generator if
25 * available and falls back to Math.random (cryptographically insecure) when synchronous bridged
26 * methods are unavailable.
27 *
28 * See https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues
29 */
30export default function getRandomValues<TArray extends ArrayBufferView>(values: TArray): TArray {
31  if (arguments.length < 1) {
32    throw new TypeError(
33      `An ArrayBuffer view must be specified as the destination for the random values`
34    );
35  }
36
37  if (
38    !(values instanceof Int8Array) &&
39    !(values instanceof Uint8Array) &&
40    !(values instanceof Int16Array) &&
41    !(values instanceof Uint16Array) &&
42    !(values instanceof Int32Array) &&
43    !(values instanceof Uint32Array) &&
44    !(values instanceof Uint8ClampedArray)
45  ) {
46    throw new TypeError(`The provided ArrayBuffer view is not an integer-typed array`);
47  }
48
49  if (values.byteLength > MAX_RANDOM_BYTES) {
50    throw new QuotaExceededError(
51      `The ArrayBuffer view's byte length (${values.byteLength}) exceeds the number of bytes of entropy available via this API (${MAX_RANDOM_BYTES})`
52    );
53  }
54
55  let randomBytes: Uint8Array;
56  try {
57    // NOTE: Consider implementing `fillRandomBytes` to populate the given TypedArray directly
58    randomBytes = getRandomBytes(values.byteLength);
59  } catch {
60    // TODO: rethrow the error if it's not due to a lack of synchronous methods
61    console.warn(`Random.getRandomBytes is not supported; falling back to insecure Math.random`);
62    return getRandomValuesInsecure(values);
63  }
64
65  // Create a new TypedArray that is of the same type as the given TypedArray but is backed with the
66  // array buffer containing random bytes. This is cheap and copies no data.
67  const TypedArrayConstructor = values.constructor as IntegerArrayConstructor;
68  const randomValues = new TypedArrayConstructor(
69    randomBytes.buffer,
70    randomBytes.byteOffset,
71    values.length
72  );
73
74  // Copy the data into the given TypedArray, letting the VM optimize the copy if possible
75  values.set(randomValues);
76  return values;
77}
78
79export function getRandomValuesInsecure<TArray extends IntegerArray>(values: TArray): TArray {
80  // Write random bytes to the given TypedArray's underlying ArrayBuffer
81  const byteView = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
82  for (let i = 0; i < byteView.length; i++) {
83    // The range of Math.random() is [0, 1) and the ToUint8 abstract operation rounds down
84    byteView[i] = Math.random() * 256;
85  }
86  return values;
87}
88
89class QuotaExceededError extends Error {
90  name = 'QuotaExceededError';
91  code = 22; // QUOTA_EXCEEDED_ERR
92}
93