diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..1bb6a2b --- /dev/null +++ b/index.test.ts @@ -0,0 +1,36 @@ +import {expect, test} from 'bun:test' +import Random from '.' + +test('same result', async () => { + const random1 = new Random(new Uint8Array(64)) + const a = await random1.random(128n) + const a2 = await random1.random(128n) + expect(a2).not.toBe(a) + + const random2 = new Random(new Uint8Array(64)) + const b = await random2.random(128n) + + expect(a).toBe(b) +}) + +test('Different state, different number', async () => { + const random1 = new Random(new Uint8Array(64)) + const a = await random1.random(128n) + + const state = new Uint8Array(64) + state[0] = 1 + const random2= new Random(state) + const b = await random2.random(128n) + + expect(a).not.toBe(b) +}) + +test('New random is random', async () => { + const r1 = new Random() + const r2 = new Random() + + const a = await r1.random(100n) + const b = await r2.random(100n) + + expect(b).not.toBe(a) +}) diff --git a/index.ts b/index.ts index dd68c9e..36352fb 100644 --- a/index.ts +++ b/index.ts @@ -1 +1,69 @@ -export default class Random {} +import {bytesToHex, hexToNumber} from '@noble/curves/abstract/utils' + +const useless = new Uint8Array(0) + +export default class Random { + private readonly state: Uint8Array; + + public constructor( + state?: Uint8Array + ) { + if (state === undefined) { + this.state = crypto.getRandomValues(new Uint8Array(64)) + } else { + if (state.length !== 64) throw "State must have a length of 64" + this.state = state + } + } + + public async random(max: bigint) : Promise { + if (max <= 0) throw "Only works for range [0, max[, with max > 0" + + const size = log2(max) + const bitsize = ((size % 8) === 0) ? size : (size - (size % 8) + 8) + + while (true) { + const bytes = await this.random_bytes(bitsize) + const n = hexToNumber(bytesToHex(bytes)) >> BigInt(bitsize - size) + if (n < max) return n + } + } + + async random_bytes(bitsize: number) : Promise { + const hash_size = Math.max(512, bitsize) + + const state = await crypto.subtle.importKey( + "raw", + this.state, + {name: "HKDF"}, + false, + ["deriveBits"], + ) + const buffer = await crypto.subtle.deriveBits( + { + name: "HKDF", + hash: "SHA-512", + info: useless, + salt: useless, + }, + state, + hash_size, + ) + const w = new Uint8Array(buffer) + + for (let i = 0;i < 64;++i) this.state[i] ^= w[i] + + return w.slice(0, Math.max(bitsize >> 3, 1)) + } +} + +function log2(n: bigint) : number { + let l = 0 + + while (n > 0) { + n = n >> 1n + l += 1 + } + + return l +} diff --git a/package.json b/package.json index 374a0dc..3977be8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "test": "bun test" }, + "dependencies": { + "@noble/curves": "^1.4.0" + }, "devDependencies": { "@types/bun": "^1.1.2" },