// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server, // window.crypto.getRandomValues() in the browser) when available. If these // PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically // strong, and we seed it with various sources such as the date, Math.random, // and window size on the client. When using crypto.getRandomValues(), our // primitive is hexString(), from which we construct fraction(). When using // window.crypto.getRandomValues() or alea, the primitive is fraction and we use // that to construct hex string. const UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'; const BASE64_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'; // `type` is one of `RandomGenerator.Type` as defined below. // // options: // - seeds: (required, only for RandomGenerator.Type.ALEA) an array // whose items will be `toString`ed and used as the seed to the Alea // algorithm export abstract class RandomGenerator { /** * @name Random.fraction * @summary Return a number between 0 and 1, like `Math.random`. * @locus Anywhere */ abstract fraction(): number; /** * Create a non-cryptographically secure PRNG with a given seed (using * the Alea algorithm) */ createWithSeeds(...seeds: readonly unknown[]): RandomGenerator { if (seeds.length === 0) { throw new Error('No seeds were provided'); } return this.safelyCreateWithSeeds(...seeds); } protected abstract safelyCreateWithSeeds(...seeds: readonly unknown[]): RandomGenerator; /** * Used like `Random`, but much faster and not cryptographically secure */ abstract insecure: RandomGenerator; /** * @name Random.hexString * @summary Return a random string of `n` hexadecimal digits. * @locus Anywhere * @param digits Length of the string */ hexString(digits: number) { return this._randomString(digits, '0123456789abcdef'); } _randomString(charsCount: number, alphabet: string) { let result = ''; for (let i = 0; i < charsCount; i++) { result += this.choice(alphabet); } return result; } /** * @name Random.id * @summary Return a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is * likely to be unique in the whole world. * @locus Anywhere * @param charsCount Optional length of the identifier in characters * (defaults to 17) */ id(charsCount = 17) { // 17 characters is around 96 bits of entropy, which is the amount of state in the Alea PRNG. return this._randomString(charsCount, UNMISTAKABLE_CHARS); } /** * @name Random.secret * @summary Return a random string of printable characters with 6 bits of * entropy per character. Use `Random.secret` for security-critical secrets * that are intended for machine, rather than human, consumption. * @locus Anywhere * @param charsCount Optional length of the secret string (defaults to 43 * characters, or 256 bits of entropy) */ secret(charsCount = 43) { // Default to 256 bits of entropy, or 43 characters at 6 bits per character. return this._randomString(charsCount, BASE64_CHARS); } /** * @name Random.choice * @summary Return a random element of the given array or string. * @locus Anywhere * @param {Array|String} arrayOrString Array or string to choose from */ choice(arrayOrString: TArrayLike) { const index = Math.floor(this.fraction() * arrayOrString.length); if (typeof arrayOrString === 'string') { return arrayOrString.substr(index, 1); } return arrayOrString[index]; } }