diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..d9613ff7 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,12 @@ +{ + "extension": ["ts"], + "spec": "hwe/test-ts/**/*.test.ts", + "require": [ + "ts-node/register", + "tsconfig-paths/register" + ], + "node-option": [ + "experimental-specifier-resolution=node", + "loader=ts-node/esm" + ] + } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c6ffd72c..9d330958 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "/nya":true, "/pya":true, "/twe":true, - + }, "search.exclude": { "e_lib/*":true, @@ -28,5 +28,5 @@ "cSpell.words": [ "josa", "sammo" - ], + ] } \ No newline at end of file diff --git a/hwe/test-ts/rng.test.ts b/hwe/test-ts/rng.test.ts new file mode 100644 index 00000000..2461a680 --- /dev/null +++ b/hwe/test-ts/rng.test.ts @@ -0,0 +1,370 @@ +import chai, { assert } from 'chai'; +import chaiBytes from 'chai-bytes'; +import { bufferByteSize, LiteHashDRBG } from '../ts/util/LiteHashDRBG'; +import { RandUtil } from '../ts/util/RandUtil'; +import { convertBytesLikeToArrayBuffer } from '../ts/util/convertBytesLikeToArrayBuffer'; +import { convertBytesLikeToUint8Array as toBytes } from '../ts/util/convertBytesLikeToUint8Array'; +import _ from 'lodash'; + +chai.use(chaiBytes); + +type Bytes = ArrayBuffer | DataView | Uint8Array; +type MaybeBytes = Bytes | string; + +function fillBlock(body: MaybeBytes, filler: MaybeBytes = '\0', length = bufferByteSize): Uint8Array { + const u8Body = toBytes(body); + const u8Filler = toBytes(filler, false); + + if (u8Filler.byteLength < 1) { + throw new Error('filler must have length'); + } + + const buffer = new Uint8Array(length); + buffer.set(u8Body, 0); + let bufferIdx = u8Body.byteLength; + + while (bufferIdx + u8Filler.byteLength < length) { + buffer.set(u8Filler, bufferIdx); + bufferIdx += u8Filler.byteLength; + } + + if (bufferIdx < length) { + const slice = new Uint8Array(u8Filler.buffer, u8Filler.byteOffset, length - bufferIdx); + buffer.set(slice, bufferIdx); + } + + return buffer; +} + + +class DummyBlockRNG extends LiteHashDRBG { + private repeatBlockCnt: number; + private repeatBlock: ArrayBuffer[]; + + public constructor(repeatBlock: MaybeBytes[], stateIdx = 0) { + super('x'); + + this.repeatBlock = []; + for (const rawBlock of repeatBlock) { + const block = convertBytesLikeToArrayBuffer(rawBlock); + if (block.byteLength !== bufferByteSize) { + throw new Error; + } + this.repeatBlock.push(block); + } + this.repeatBlockCnt = this.repeatBlock.length; + this.stateIdx = stateIdx; + this.bufferIdx = 0; + this.genNextBlock(); + } + + protected genNextBlock() { + if (!this.repeatBlock) { + return; + } + this.buffer = this.repeatBlock[this.stateIdx]; + this.bufferIdx = 0; + this.stateIdx = (this.stateIdx + 1) % this.repeatBlockCnt; + } +} + +const fixedKey = 'HelloWorld'; + + +describe('RNGtestDummy', () => { + const rng = new DummyBlockRNG([ + fillBlock('', "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff") + ]); + + it('BasicConvert', () => { + assert.equal(toBytes("\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", false).length, 16); + }); + it('SimpleByte', () => { + assert.equalBytes(toBytes("\x00", false), rng.nextBytes(1), 'b1'); + assert.equalBytes(toBytes("\x11\x22", false), rng.nextBytes(2), 'b2'); + assert.equalBytes(toBytes("\x33\x44\x55", false), rng.nextBytes(3), 'b3'); + assert.equalBytes(toBytes("\x66\x77\x88\x99", false), rng.nextBytes(4), 'b4'); + }); + + it('OverflowBlock', () => { + for (const idx in _.range(16)) { + assert.equalBytes( + toBytes("\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", false), + rng.nextBytes(16) + ); + } + }); + + it('MultiBlock', () => { + assert.equalBytes( + fillBlock('', "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", bufferByteSize * 2), + rng.nextBytes(bufferByteSize * 2) + ); + }); + + it('bitTest', () => { + assert.equalBytes(toBytes("\x00", false), rng.nextBits(1)); //aa + assert.equalBytes(toBytes("\x01", false), rng.nextBits(1)); //bb + assert.equalBytes(toBytes("\xcc", false), rng.nextBits(8)); //cc + assert.equalBytes(toBytes("\xdd\x02", false), rng.nextBits(10)); //ddee + assert.equalBytes(toBytes("\x7f", false), rng.nextBits(7)); //ff + assert.equalBytes(toBytes("\x00\x11\x22\x33\x44\x55\x06", false), rng.nextBits(53)); + }); + + it('int', () => { + assert.equal(0x77, rng.nextInt(0xff)); + assert.equal(0x9988, rng.nextInt((1 << 16) - 1)); + assert.equal(0xddccbbaa, rng.nextInt(0xffffffff)); + assert.equal(0x0433221100ffee, rng.nextInt()); + assert.equal(0x05, rng.nextInt(0x0f)); //55 + assert.equal(0x06, rng.nextInt(0x12)); //66 + assert.equal(0x08, rng.nextInt(99)); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8) + assert.equal(0x99, rng.nextInt(0x99)); //99 + assert.equal(0xaa, rng.nextInt(0xaa)); //aa (fit Max) + }); + + it('float', () => { + const floatMax = 2 ** 53; + const fa = rng.nextFloat1(); + assert.equal(0x1100ffeeddccbb / floatMax, fa); + assert.isTrue(0.5313720384 > fa); + assert.isTrue(0.5313720383 < fa); + const fb = rng.nextFloat1(); + assert.equal(0x08776655443322 / floatMax, fb); + }); +}); + +describe('RandUtilDummy', () => { + it('shuffle', () => { + const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]); + const randUtil = new RandUtil(rng); + /** + * 7, [7,1,2,3,4,5,6,0] + * 6, [7,0,2,3,4,5,6,1] + * 5, [7,0,1,3,4,5,6,2] + * 4, [7,0,1,2,4,5,6,3] + * 3, [7,0,1,2,3,5,6,4] + * 2, [7,0,1,2,3,4,6,5] + * 1, [7,0,1,2,3,4,5,6] + */ + assert.deepEqual( + randUtil.shuffle(Array.from(_.range(8))), + [7, 0, 1, 2, 3, 4, 5, 6] + ); + + /** + * 0, [0,1,2,3,4,5,6,7,8,9] + * 7, [0,8,2,3,4,5,6,7,1,9] + * 6, [0,8,1,3,4,5,6,7,2,9] + * 5, [0,8,1,2,4,5,6,7,3,9] + * 4, [0,8,1,2,3,5,6,7,4,9] + * 3, [0,8,1,2,3,4,6,7,5,9] + * 2, [0,8,1,2,3,4,5,7,6,9] + * 1, [0,8,1,2,3,4,5,6,7,9] + * 0, [0,8,1,2,3,4,5,6,7,9] + */ + assert.deepEqual( + randUtil.shuffle(Array.from(_.range(10))), + [0, 8, 1, 2, 3, 4, 5, 6, 7, 9] + ); + }); + + const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]); + const randUtil = new RandUtil(rng); + it('choice', () => { + + + //0x17(7), 0x16(6) + assert.equal(randUtil.choice([0, 1, 2, 3, 4, 5]), 5); + + //0x15(5), Set 순서 유지 + assert.equal(randUtil.choice(new Set([5, 3, 1, 2, 8, 0])), 8); + + //0x14(4), 정렬 순서상 숫자(소-대) > 문자열(삽입순) > 심볼 순서 + assert.equal(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', '3': 'q' }), 'c'); + + }); + + it('choiceUsingWeight', () => { + //0.6275740099377194 * 38.1 = 23.91 + assert.equal(randUtil.choiceUsingWeight({ + a: 0.1, + b: 10, + tt: 2, + x: -1, + c: 20, + d: 0, + e: 6 + }), 'c'); + + //0.658946544056166 + assert.equal(randUtil.choiceUsingWeightPair([ + ['xx', 10], + ]), 'xx'); + + //0.6903152783785083 * 27.3 = 18.84560709973328 + assert.equal(randUtil.choiceUsingWeightPair([ + ['e', 10], + ['d', 4], + ['c', 0.1], + ['baba', 0.2], + ['q', 9], + ['xt', 4] + ]), 'q'); + }); +}); + +describe('RNGexpectedError', () => { + const rng = new LiteHashDRBG(fixedKey); + it('nextBits0', () => { + assert.throw(() => rng.nextBits(0)); + }); + it('nextBits-1', () => { + assert.throw(() => rng.nextBits(-1)); + }); + + it('nextBytes0', () => { + assert.throw(() => rng.nextBytes(0)); + }); + it('nextBytes-1', () => { + assert.throw(() => rng.nextBytes(-1)); + }); + + const randUtil = new RandUtil(rng); + it('utilEmptyChoice', () => { + assert.throw(() => randUtil.choice([])); + }); + it('utilEmptyChoiceUsingWeight', () => { + assert.throw(() => randUtil.choiceUsingWeight({})); + }); + it('utilEmptyChoiceUsingWeightPair', () => { + assert.throw(() => randUtil.choiceUsingWeightPair([])); + }); +}); + +describe('RNGAcceptable', () => { + const rng = new LiteHashDRBG(fixedKey); + it('RNG', () => { + rng.nextInt(0); + rng.nextInt(2 ** 53 - 1); + rng.nextBytes(65); + rng.nextBits(512); + }); + + const randUtil = new RandUtil(rng); + it('RandUtil', () => { + randUtil.choice([0, 0, 0]); + randUtil.choiceUsingWeight({ + 0: 0, + 1: -1 + }); + randUtil.choiceUsingWeightPair([ + [0, 0], + [1, 0], + [2, -2] + ]); + randUtil.nextBool(1.1); + randUtil.nextBool(-0.1); + randUtil.shuffle([]); + randUtil.shuffle([1]); + randUtil.nextRange(0, 0); + randUtil.nextRangeInt(0, 0); + randUtil.nextRange(1, -1); + randUtil.nextRangeInt(1, -1); + }) + + it('RNGLong', () => { + const longKey = fixedKey; + for(const _a of _.range(8)){ + longKey.concat(longKey); + } + const rngLong = new LiteHashDRBG(longKey); + for(const _a of _.range(10)){ + rngLong.nextBytes(16); + } + }) +}); + +/* Python TestVector +import hashlib +import struct + +fixedKey = 'HelloWorld'.encode('utf-8') + +def hash(key, idx): + idxV = struct.pack(" { + + //JS - PHP 일치 확인 정도로. + + const testVector = Buffer.from([ + '24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4', + '2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165', + '8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a', + '398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47', + 'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb', + ].join(''), 'hex'); + + it('bytes', ()=>{ + const rng = new LiteHashDRBG(fixedKey); + + let offset = 0; + assert.equalBytes(rng.nextBytes(10), testVector.slice(offset, offset + 10), '1'); + offset += 10; + assert.equalBytes(rng.nextBytes(32), testVector.slice(offset, offset + 32), '2'); + offset += 32; + assert.equalBytes(rng.nextBytes(1), testVector.slice(offset, offset + 1), '3'); + offset += 1; + assert.equalBytes(rng.nextBytes(64), testVector.slice(offset, offset + 64), '4'); + offset += 64; + assert.equalBytes(rng.nextBytes(5), testVector.slice(offset, offset + 5), '5'); + offset += 5; + + const lastA = rng.nextBytes(16, 18); + const lastB = new Uint8Array(18); + lastB.set(testVector.slice(offset, offset + 16)); + assert.equalBytes(lastA, lastB); + }); + + it('bits', ()=>{ + const rng = new LiteHashDRBG(fixedKey); + + let offset = 0; + + const testBits = [10, 4, 15, 32, 7, 99, 512, 1, 2, 3]; + + for(const bits of testBits){ + const bytes = Math.ceil(bits / 8) + const A = rng.nextBits(bits); + const B = new Uint8Array(testVector.slice(offset, offset + bytes)); + offset += bytes; + + if(bits % 8 != 0){ + const bitMask = 0xff >> (8 - (bits % 8)); + B[bytes - 1] &= bitMask; + } + assert.equalBytes(A, B); + } + }); + + it('float', ()=>{ + const rng = new LiteHashDRBG(fixedKey); + const rng2 = new DummyBlockRNG([ + new Uint8Array(testVector.slice(bufferByteSize * 0, bufferByteSize * 1)), + new Uint8Array(testVector.slice(bufferByteSize * 1, bufferByteSize * 2)), + new Uint8Array(testVector.slice(bufferByteSize * 2, bufferByteSize * 3)), + new Uint8Array(testVector.slice(bufferByteSize * 3, bufferByteSize * 4)), + new Uint8Array(testVector.slice(bufferByteSize * 4, bufferByteSize * 5)), + ]); + + for(const idx of _.range(18)){ + assert.equal(rng.nextFloat1(), rng2.nextFloat1(), `float${idx}`); + } + + }); +}); \ No newline at end of file diff --git a/hwe/ts/util/BytesLike.ts b/hwe/ts/util/BytesLike.ts new file mode 100644 index 00000000..abcd5572 --- /dev/null +++ b/hwe/ts/util/BytesLike.ts @@ -0,0 +1,2 @@ +export type Bytes = ArrayBuffer | DataView | Uint8Array; +export type BytesLike = Bytes | string; \ No newline at end of file diff --git a/hwe/ts/util/LiteHashDRBG.ts b/hwe/ts/util/LiteHashDRBG.ts new file mode 100644 index 00000000..1eae489a --- /dev/null +++ b/hwe/ts/util/LiteHashDRBG.ts @@ -0,0 +1,232 @@ +import { RNG } from "./RNG"; + +import { sha512 } from 'js-sha512'; + +import { convertBytesLikeToUint8Array } from "./convertBytesLikeToUint8Array"; +import { BytesLike } from "./BytesLike"; + +const maxRngSupportBit = 53; +const maxInt = 0x1f_ffff_ffff_ffff; // NOTE: b 0, 10000110011, 11...11 +const maxIntMore1 = 0x20_0000_0000_0000n; //NOTE: b 0, 10000110100, 00...00 +const maxIntMore1f = Number(maxIntMore1); +export const bufferByteSize = 512 / 8; //SHA512 + +const intBitMapMask = new Map([ + [0x1n, 1], + [0x3n, 2], + [0x7n, 3], + [0xfn, 4], + [0x1fn, 5], + [0x3fn, 6], + [0x7fn, 7], + [0xffn, 8], + [0x1ffn, 9], + [0x3ffn, 10], + [0x7ffn, 11], + [0xfffn, 12], + [0x1fffn, 13], + [0x3fffn, 14], + [0x7fffn, 15], + [0xffffn, 16], + [0x1ffffn, 17], + [0x3ffffn, 18], + [0x7ffffn, 19], + [0xfffffn, 20], + [0x1fffffn, 21], + [0x3fffffn, 22], + [0x7fffffn, 23], + [0xffffffn, 24], + [0x1ffffffn, 25], + [0x3ffffffn, 26], + [0x7ffffffn, 27], + [0xfffffffn, 28], + [0x1fffffffn, 29], + [0x3fffffffn, 30], + [0x7fffffffn, 31], + [0xffffffffn, 32], + [0x1ffffffffn, 33], + [0x3ffffffffn, 34], + [0x7ffffffffn, 35], + [0xfffffffffn, 36], + [0x1fffffffffn, 37], + [0x3fffffffffn, 38], + [0x7fffffffffn, 39], + [0xffffffffffn, 40], + [0x1ffffffffffn, 41], + [0x3ffffffffffn, 42], + [0x7ffffffffffn, 43], + [0xfffffffffffn, 44], + [0x1fffffffffffn, 45], + [0x3fffffffffffn, 46], + [0x7fffffffffffn, 47], + [0xffffffffffffn, 48], + [0x1ffffffffffffn, 49], + [0x3ffffffffffffn, 50], + [0x7ffffffffffffn, 51], + [0xfffffffffffffn, 52], + [0x1fffffffffffffn, 53], +]); + +function calcBitMask(n: bigint): bigint { + n |= n >> 1n; + n |= n >> 2n; + n |= n >> 4n; + n |= n >> 8n; + n |= n >> 16n; + n |= n >> 32n; + + return n; +} +export class LiteHashDRBG implements RNG { + + protected buffer!: ArrayBuffer; + protected bufferIdx!: number; + protected hq: DataView; + protected hqIdxPos: number; + + public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) { + if(bufferIdx < 0){ + throw new Error(`bufferIdx ${bufferIdx} < 0`); + } + if(bufferIdx >= bufferByteSize){ + throw new Error(`bufferidx ${bufferIdx} >= ${bufferByteSize}`); + } + if(stateIdx < 0){ + throw new Error(`stateIdx ${stateIdx} < 0`); + } + + const seedU8 = convertBytesLikeToUint8Array(seed); + const hqBuffer = new ArrayBuffer(seedU8.byteLength + 4); + const hqU8 = new Uint8Array(hqBuffer); + + hqU8.set(seedU8, 0); + this.hq = new DataView(hqBuffer); + this.hqIdxPos = seedU8.byteLength; + + this.genNextBlock(); + this.bufferIdx = bufferIdx; + } + + protected genNextBlock(): void { + this.hq.setUint32(this.hqIdxPos, this.stateIdx, true); + const digest = sha512.arrayBuffer(this.hq.buffer); + this.buffer = digest; + this.bufferIdx = 0; + this.stateIdx += 1; + } + + public getMaxInt(): number { + return maxInt; + } + + public nextBytes(bytes: number, baseBytes?: number): Uint8Array { + bytes |= 0; + if (bytes <= 0) { + throw new Error(`${bytes} <= 0`); + } + + if (this.bufferIdx + bytes <= bufferByteSize) { + if(baseBytes === undefined || bytes >= baseBytes){ + const result = this.buffer.slice(this.bufferIdx, this.bufferIdx + bytes); + this.bufferIdx += bytes; + if (this.bufferIdx === bufferByteSize) { + this.genNextBlock(); + } + return new Uint8Array(result); + } + + const resultBuffer = new ArrayBuffer(Math.max(bytes, baseBytes)); + const result = new Uint8Array(resultBuffer); + result.set(new Uint8Array(this.buffer, this.bufferIdx, bytes)); + this.bufferIdx += bytes; + if( this.bufferIdx === bufferByteSize){ + this.genNextBlock(); + } + return result; + } + + const resultBuffer = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes); + const result = new Uint8Array(resultBuffer); + + result.set(new Uint8Array(this.buffer, this.bufferIdx)); + let offset = bufferByteSize - this.bufferIdx; + let remain = bytes - offset; + + while (remain > bufferByteSize) { + this.genNextBlock(); + result.set(new Uint8Array(this.buffer), offset); + offset += bufferByteSize; + remain -= bufferByteSize; + } + + this.genNextBlock(); + if (remain === 0) { + return result; + } + + result.set(new Uint8Array(this.buffer, 0, remain), offset); + this.bufferIdx = remain; + return result; + } + + public nextBits(bits: number, baseBytes?: number): Uint8Array { + bits |= 0; + const bytes = (bits + 7) >> 3; + const headBits = bits & 0x7; + + const result = this.nextBytes(bytes, baseBytes); + if (headBits === 0) { + return result; + } + + result[bytes - 1] &= 0xff >> (8 - headBits); + return result; + } + + protected _nextInt(bits: number): bigint{ + const buffer = this.nextBits(bits, 8); + const dataView = new DataView(buffer.buffer); + return dataView.getBigUint64(0, true); + } + + public nextInt(max?: number): number { + if (max === undefined || max === maxInt) { + return Number(this._nextInt(maxRngSupportBit)); + } + if (max > maxInt) { + throw new Error('Over max int'); + } + if (max === 0) { + return 0; + } + if (max < 0) { + return -this.nextInt(-max); + } + + const mask = calcBitMask(BigInt(max)); + const bits = intBitMapMask.get(mask) as number; + + let n = Number(this._nextInt(bits)); + while (n > max){ + n = Number(this._nextInt(bits)); + } + return n; + } + + public nextFloat1(): number { + // eslint-disable-next-line no-constant-condition + while(true){ + const nInt = this._nextInt(maxRngSupportBit + 1); + if(nInt < maxIntMore1){ + return Number(nInt) / maxIntMore1f; + } + if(nInt === maxIntMore1){ + return 1; + } + } + } + + public static build(seed: BytesLike, stateIdx = 0): LiteHashDRBG{ + return new LiteHashDRBG(seed, stateIdx); + } +} \ No newline at end of file diff --git a/hwe/ts/util/RNG.ts b/hwe/ts/util/RNG.ts new file mode 100644 index 00000000..d097f1e7 --- /dev/null +++ b/hwe/ts/util/RNG.ts @@ -0,0 +1,13 @@ +export interface RNG { + + /** + * nextInt()가 반환 가능한 최대값 + */ + getMaxInt(): number; + + nextBytes(bytes: number): Uint8Array; + nextBits(bits: number): Uint8Array; + + nextInt(max?: number): number; + nextFloat1(): number; +} \ No newline at end of file diff --git a/hwe/ts/util/RandUtil.ts b/hwe/ts/util/RandUtil.ts new file mode 100644 index 00000000..5c9c17b1 --- /dev/null +++ b/hwe/ts/util/RandUtil.ts @@ -0,0 +1,138 @@ +import { RNG } from './RNG'; + +export class RandUtil { + constructor(protected rng: RNG) { + + } + + public nextFloat1(): number { + return this.rng.nextFloat1(); + } + + public nextRange(min: number, max: number): number { + const range = max - min; + return this.nextFloat1() * (range) + min; + } + + public nextRangeInt(min: number, max: number): number { + const range = max - min; + return this.rng.nextInt(range) + min; + } + + public nextInt(max?: number): number { + return this.rng.nextInt(max); + } + + public nextBit(): boolean { + const view = new DataView(this.rng.nextBits(1)); + return view.getUint8(0) != 0; + } + + public nextBool(prob = 0.5): boolean { + if (prob >= 1) { + return true; + } + return this.nextFloat1() < prob; + } + + public shuffle(srcArray: T[]): T[] { + const cnt = srcArray.length; + if(cnt === 0){ + return []; + } + if (cnt > this.rng.getMaxInt()) { + throw 'Invalid random int range'; + } + + const result: T[] = Array.from(srcArray); + for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) { + const destIdx = this.rng.nextInt(cnt - srcIdx - 1) + srcIdx; + if(srcIdx === destIdx){ + continue; + } + [result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]]; + } + + return result; + } + + //Object는 integer key에 예외가 있어 shuffleAssoc은 없음 + + public choice(items: T[] | Record | Set): T { + if (items instanceof Array) { + if(items.length === 0){ + throw new Error('Empty items'); + } + const idx = this.rng.nextInt(items.length - 1); + return items[idx]; + } + + if (items instanceof Set) { + return this.choice(Array.from(items.values())); + } + + return items[this.choice(Array.from(Object.keys(items)))]; + } + + public choiceUsingWeight(items: Record): string | number { + if(Object.keys(items).length === 0){ + throw new Error('Empty items'); + } + let sum = 0; + for (const value of Object.values(items)) { + if (value <= 0) { + continue; + } + sum += value; + } + + let rd = this.nextFloat1() * sum; + + for (const [item, value] of Object.entries(items)) { + if (value <= 0) { + if (rd <= 0) { + return item; + } + continue; + } + + if (rd <= value) { + return item; + } + rd -= value; + } + + throw new Error('Unreacheable'); + } + + public choiceUsingWeightPair(items: [T, number][]): T { + if(items.length === 0){ + throw new Error('Empty items'); + } + let sum = 0; + for (const [, value] of items) { + if (value <= 0) { + continue; + } + sum += value; + } + + let rd = this.nextFloat1() * sum; + + for (const [item, value] of items) { + if (value <= 0) { + if (rd <= 0) { + return item; + } + continue; + } + + if (rd <= value) { + return item; + } + rd -= value; + } + + throw new Error('Unreacheable'); + } +} \ No newline at end of file diff --git a/hwe/ts/util/convertBytesLikeToArrayBuffer.ts b/hwe/ts/util/convertBytesLikeToArrayBuffer.ts new file mode 100644 index 00000000..82e8bab4 --- /dev/null +++ b/hwe/ts/util/convertBytesLikeToArrayBuffer.ts @@ -0,0 +1,17 @@ +import { BytesLike } from "./BytesLike"; + +export function convertBytesLikeToArrayBuffer(data: BytesLike, encodeUTF8 = true): ArrayBuffer{ + if (data instanceof ArrayBuffer) { + return data; + } + if (data instanceof Uint8Array) { + return data.buffer; + } + if (typeof(data) === 'string'){ + if(encodeUTF8){ + return (new TextEncoder()).encode(data); + } + return new Uint8Array(data.split('').map(s=>s.codePointAt(0) as number)); + } + return data.buffer; +} \ No newline at end of file diff --git a/hwe/ts/util/convertBytesLikeToUint8Array.ts b/hwe/ts/util/convertBytesLikeToUint8Array.ts new file mode 100644 index 00000000..7bd1074e --- /dev/null +++ b/hwe/ts/util/convertBytesLikeToUint8Array.ts @@ -0,0 +1,17 @@ +import { BytesLike } from "./BytesLike"; + +export function convertBytesLikeToUint8Array(data: BytesLike, encodeUTF8 = true): Uint8Array { + if (data instanceof Uint8Array) { + return data; + } + if (data instanceof ArrayBuffer) { + return new Uint8Array(data); + } + if (typeof (data) === 'string') { + if(encodeUTF8){ + return (new TextEncoder()).encode(data); + } + return new Uint8Array(data.split('').map(s=>s.codePointAt(0) as number)); + } + return new Uint8Array(data.buffer); +} \ No newline at end of file diff --git a/package.json b/package.json index 61b07723..7f72538b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "npm-run-all test-php-gateway test-ts", "test-php-gateway": "vendor/bin/phpunit --bootstrap vendor/autoload.php tests", - "test-ts": "node --loader ts-node/esm ./node_modules/.bin/mocha hwe/test-ts/**/*.test.ts", + "test-ts": "mocha", "build": "webpack", "buildDev": "webpack --mode=development", "watch": "webpack watch --mode=development", @@ -77,6 +77,7 @@ "babel-preset-modern-browsers": "^15.0.2", "bootswatch": "^5.1.3", "chai": "^4.3.6", + "chai-bytes": "^0.1.2", "clean-terminal-webpack-plugin": "^3.0.0", "css-loader": "^6.6.0", "cssnano": "^5.0.17", @@ -95,6 +96,7 @@ "sass-loader": "^12.6.0", "style-loader": "^3.3.1", "ts-node": "^10.6.0", + "tsconfig-paths": "^3.13.0", "typescript": "^4.5.5", "url-loader": "^4.1.1", "vue-eslint-parser": "^8.2.0", diff --git a/src/sammo/LiteHashDRBG.php b/src/sammo/LiteHashDRBG.php new file mode 100644 index 00000000..35aaa3f5 --- /dev/null +++ b/src/sammo/LiteHashDRBG.php @@ -0,0 +1,225 @@ += self::BUFFER_BYTE_SIZE){ + throw new \InvalidArgumentException("bufferIdx {$bufferIdx} >= ".self::BUFFER_BYTE_SIZE); + } + if($stateIdx < 0){ + throw new \InvalidArgumentException("stateIdx {$stateIdx} < 0"); + } + $this->genNextBlock(); + $this->bufferIdx = $bufferIdx; + } + + protected function genNextBlock(): void + { + $hq = $this->seed . pack('V', $this->stateIdx); + $this->buffer = hash('sha512', $hq, true); + $this->bufferIdx = 0; + $this->stateIdx += 1; + } + + static function getMaxInt(): int + { + return self::MAX_INT; + } + + const INT_BIT_MASK_MAP = [ + 0x1 => 1, + 0x3 => 2, + 0x7 => 3, + 0xf => 4, + 0x1f => 5, + 0x3f => 6, + 0x7f => 7, + 0xff => 8, + 0x1ff => 9, + 0x3ff => 10, + 0x7ff => 11, + 0xfff => 12, + 0x1fff => 13, + 0x3fff => 14, + 0x7fff => 15, + 0xffff => 16, + 0x1ffff => 17, + 0x3ffff => 18, + 0x7ffff => 19, + 0xfffff => 20, + 0x1fffff => 21, + 0x3fffff => 22, + 0x7fffff => 23, + 0xffffff => 24, + 0x1ffffff => 25, + 0x3ffffff => 26, + 0x7ffffff => 27, + 0xfffffff => 28, + 0x1fffffff => 29, + 0x3fffffff => 30, + 0x7fffffff => 31, + 0xffffffff => 32, + 0x1ffffffff => 33, + 0x3ffffffff => 34, + 0x7ffffffff => 35, + 0xfffffffff => 36, + 0x1fffffffff => 37, + 0x3fffffffff => 38, + 0x7fffffffff => 39, + 0xffffffffff => 40, + 0x1ffffffffff => 41, + 0x3ffffffffff => 42, + 0x7ffffffffff => 43, + 0xfffffffffff => 44, + 0x1fffffffffff => 45, + 0x3fffffffffff => 46, + 0x7fffffffffff => 47, + 0xffffffffffff => 48, + 0x1ffffffffffff => 49, + 0x3ffffffffffff => 50, + 0x7ffffffffffff => 51, + 0xfffffffffffff => 52, + 0x1fffffffffffff => 53, + ]; + + private static function calcBitMask($n) + { + $n |= $n >> 1; + $n |= $n >> 2; + $n |= $n >> 4; + $n |= $n >> 8; + $n |= $n >> 16; + $n |= $n >> 32; + + return $n; + } + + + public function nextBytes(int $bytes): string + { + if ($bytes <= 0) { + throw new \InvalidArgumentException("{$bytes} <= 0"); + } + + if ($this->bufferIdx + $bytes <= self::BUFFER_BYTE_SIZE) { + $buffer = substr($this->buffer, $this->bufferIdx, $bytes); + $this->bufferIdx += $bytes; + if ($this->bufferIdx == self::BUFFER_BYTE_SIZE) { + $this->genNextBlock(); + } + return $buffer; + } + + + $result = [substr($this->buffer, $this->bufferIdx)]; + $remain = $bytes - (self::BUFFER_BYTE_SIZE - $this->bufferIdx); + + while ($remain > self::BUFFER_BYTE_SIZE) { + $this->genNextBlock(); + $result[] = $this->buffer; + $remain -= self::BUFFER_BYTE_SIZE; + } + + $this->genNextBlock(); + if ($remain == 0) { + return join("", $result); + } + + $result[] = substr($this->buffer, 0, $remain); + $this->bufferIdx = $remain; + return join("", $result); + } + + public function nextBits(int $bits): string + { + $bytes = ($bits + 7) >> 3; + $headBits = $bits & 0x7; + + $buffer = $this->nextBytes($bytes); + if ($headBits === 0) { + return $buffer; + } + $buffer[$bytes - 1] = chr(ord($buffer[$bytes - 1]) & (0xff >> (8 - $headBits))); + return $buffer; + } + + + static private function parseU64(string $value): int + { + return unpack('P', $value)[1]; + } + + private function _nextInt(int $bits): int + { + $buffer = $this->nextBits($bits) . "\x00\x00\x00\x00\x00\x00\x00"; + + return self::parseU64($buffer); + } + + public function nextInt(?int $max = null): int + { + if ($max === null || $max === self::MAX_INT) { + $buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT) . "\x00"; + return self::parseU64($buffer); + } + + if ($max > self::MAX_INT) { + throw new \InvalidArgumentException('Over Max Int'); + } else if ($max === 0) { + return 0; + } else if ($max < 0) { + return -$this->nextInt(-$max); + } + + $mask = self::calcBitMask($max); + $bits = self::INT_BIT_MASK_MAP[$mask]; + + $n = $this->_nextInt($bits); + while ($n > $max) { + $n = $this->_nextInt($bits); + } + + return $n; + } + + public function nextFloat1(): float + { + $max = 1 << MAX_RNG_SUPPORT_BIT; + while (true) { + $buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT + 1) . "\x00"; + $nInt = self::parseU64($buffer); + if ($nInt <= $max) { + break; + } + } + return $nInt / $max; + } + + static function build(string $seed, int $idx = 0): self + { + return new LiteHashDRBG($seed, $idx); + } +} diff --git a/src/sammo/RNG.php b/src/sammo/RNG.php new file mode 100644 index 00000000..4ee1a66d --- /dev/null +++ b/src/sammo/RNG.php @@ -0,0 +1,28 @@ +rng->nextFloat1(); + } + + public function nextRange(int|float $min, int|float $max): float + { + $range = $max - $min; + return $this->nextFloat1() * $range + $min; + } + + public function nextRangeInt(int $min, int $max): int + { + $range = $max - $min; + if ($range > $this->rng->getMaxInt()) { + throw new \InvalidArgumentException("Invalid random int range"); + } + return $this->rng->nextInt($range) + $min; + } + + public function nextInt(?int $max = null): int{ + return $this->rng->nextInt($max); + } + + public function nextBit(): bool + { + return $this->rng->nextBits(1) !== "\0"; + } + + public function nextBool(int|float $prob = 0.5): bool + { + if ($prob >= 1) { + return true; + } + return $this->nextFloat1() < $prob; + } + + public function shuffle(array $srcArray): array + { + if(!$srcArray){ + return $srcArray; + } + $cnt = count($srcArray); + if ($cnt > $this->rng->getMaxInt()) { + throw new \InvalidArgumentException("Invalid random int range"); + } + $result = []; + foreach ($srcArray as $val) { + $result[] = $val; + } + + //PHP의 range는 max 포함 + foreach (range(0, $cnt - 1) as $srcIdx) { + $destIdx = $this->rng->nextInt($cnt - $srcIdx - 1) + $srcIdx; + if($srcIdx === $destIdx){ + continue; + } + $tmpVal = $result[$srcIdx]; + $result[$srcIdx] = $result[$destIdx]; + $result[$destIdx] = $tmpVal; + } + + return $result; + } + + public function shuffleAssoc(array $srcArray): array + { + if(!$srcArray){ + return $srcArray; + } + $result = []; + foreach ($this->shuffle(array_keys($srcArray)) as $key) { + $result[$key] = $srcArray[$key]; + } + return $result; + } + + public function choice(array $items) + { + $keys = array_keys($items); + $keyIdx = $this->rng->nextInt(count($keys) - 1); + return $items[$keys[$keyIdx]]; + } + + public function choiceUsingWeight(array $items) + { + $sum = 0; + foreach ($items as $value) { + if ($value <= 0) { + continue; + } + $sum += $value; + } + + $rd = $this->nextFloat1() * $sum; + foreach ($items as $item => $value) { + if ($value <= 0) { + $value = 0; + } + if ($rd <= $value) { + return $item; + } + $rd -= $value; + } + + //fallback. 이곳으로 빠지지 않음 + end($items); + return $items[key($items)][0]; + } + + public function choiceUsingWeightPair(array $items) + { + $sum = 0; + foreach ($items as [$item, $value]) { + if ($value <= 0) { + continue; + } + $sum += $value; + } + + $rd = $this->nextFloat1() * $sum; + foreach ($items as [$item, $value]) { + if ($value <= 0) { + $value = 0; + } + if ($rd <= $value) { + return $item; + } + $rd -= $value; + } + + //fallback. 이곳으로 빠지지 않음 + end($items); + return $items[key($items)][0]; + } +} diff --git a/src/sammo/Util.php b/src/sammo/Util.php index 4eb37d46..b70083d7 100644 --- a/src/sammo/Util.php +++ b/src/sammo/Util.php @@ -809,5 +809,4 @@ class Util extends \utilphp\util } } } - }; diff --git a/tests/RNGTest.php b/tests/RNGTest.php new file mode 100644 index 00000000..f75abf11 --- /dev/null +++ b/tests/RNGTest.php @@ -0,0 +1,415 @@ + 0; $remainFill--) { + $tmp[] = $filler; + } + + $result = join("", $tmp); + + $moreLen = $length - strlen($result); + if ($moreLen === 0) { + return $result; + } + + return $result . substr($filler, 0, $moreLen); +} + +class DummyBlockRNG extends LiteHashDRBG +{ + private int $repeatBlockCnt; + /** + * + * @param string[] $repeatBlock + * @param int $stateIdx + * @return void + * @throws RuntimeException + */ + public function __construct(protected array $repeatBlock, protected int $stateIdx = 0) + { + foreach ($repeatBlock as $block) { + if (strlen($block) != BLOCK_SIZE) { + throw new RuntimeException('Invalid repeat block'); + } + } + $this->repeatBlockCnt = count($this->repeatBlock); + $this->genNextBlock(); + } + + protected function genNextBlock(): void + { + $this->buffer = $this->repeatBlock[$this->stateIdx]; + $this->bufferIdx = 0; + $this->stateIdx = ($this->stateIdx + 1) % $this->repeatBlockCnt; + } +} + +/* Python TestVector +import hashlib +import struct + +fixedKey = 'HelloWorld'.encode('utf-8') + +def hash(key, idx): + idxV = struct.pack("setName('DummyRNG-SimpleByte'); + $this->assertEquals("\x00", $rng->nextBytes(1)); + $this->assertEquals("\x11\x22", $rng->nextBytes(2)); + $this->assertEquals("\x33\x44\x55", $rng->nextBytes(3)); + $this->assertEquals("\x66\x77\x88\x99", $rng->nextBytes(4)); + + $this->setName('DummyRNG-OverflowBlock'); + foreach (range(1, 16) as $_i) { + $this->assertEquals( + "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", + $rng->nextBytes(16) + ); + } + + $this->setName('DummyRNG-MultiBlock'); + $this->assertEquals( + fillBlock('', "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", BLOCK_SIZE * 2), + $rng->nextBytes(BLOCK_SIZE * 2) + ); + + $this->setName('DummyRNG-BitTest'); + $this->assertEquals("\x00", $rng->nextBits(1)); //aa + $this->assertEquals("\x01", $rng->nextBits(1)); //bb + $this->assertEquals("\xcc", $rng->nextBits(8)); //cc + $this->assertEquals("\xdd\x02", $rng->nextBits(10)); //ddee + $this->assertEquals("\x7f", $rng->nextBits(7)); //ff + $this->assertEquals("\x00\x11\x22\x33\x44\x55\x06", $rng->nextBits(53)); + + $this->setName('DummyRNG-Int'); + $this->assertEquals(0x77, $rng->nextInt(0xff)); + $this->assertEquals(0x9988, $rng->nextInt((1 << 16) - 1)); + $this->assertEquals(0xddccbbaa, $rng->nextInt((1 << 32) - 1)); + $this->assertEquals(0x0433221100ffee, $rng->nextInt()); + $this->assertEquals(0x05, $rng->nextInt(0x0f)); //55 + $this->assertEquals(0x06, $rng->nextInt(0x12)); //66 + $this->assertEquals(0x08, $rng->nextInt(99)); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8) + $this->assertEquals(0x99, $rng->nextInt(0x99)); //99 + $this->assertEquals(0xaa, $rng->nextInt(0xaa)); //aa (fit Max) + + $floatMax = 1 << 53; + $this->setName('DummyRNG-Float'); //7개씩 + $fa = $rng->nextFloat1(); + $this->assertEquals(0x1100ffeeddccbb / $floatMax, $fa, 'float1-1'); + $this->assertIsFloat($fa); + $this->assertLessThan(0.5313720384, $fa); + $this->assertGreaterThan(0.5313720383, $fa); + + $fb = $rng->nextFloat1(); + $this->assertEquals(0x08776655443322 / $floatMax, $fb, 'float1-2'); + } + + public function testRandUtilDummy_shuffle(){ + $rng = new DummyBlockRNG([ + fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10") + ]); + $randUtil = new RandUtil($rng); + + /** + * 7, [7,1,2,3,4,5,6,0] + * 6, [7,0,2,3,4,5,6,1] + * 5, [7,0,1,3,4,5,6,2] + * 4, [7,0,1,2,4,5,6,3] + * 3, [7,0,1,2,3,5,6,4] + * 2, [7,0,1,2,3,4,6,5] + * 1, [7,0,1,2,3,4,5,6] + */ + $this->assertEquals( + $randUtil->shuffle(range(0, 7)), + [7, 0, 1, 2, 3, 4, 5, 6] + ); + + /** + * 0, [0,1,2,3,4,5,6,7,8,9] + * 7, [0,8,2,3,4,5,6,7,1,9] + * 6, [0,8,1,3,4,5,6,7,2,9] + * 5, [0,8,1,2,4,5,6,7,3,9] + * 4, [0,8,1,2,3,5,6,7,4,9] + * 3, [0,8,1,2,3,4,6,7,5,9] + * 2, [0,8,1,2,3,4,5,7,6,9] + * 1, [0,8,1,2,3,4,5,6,7,9] + * 0, [0,8,1,2,3,4,5,6,7,9] + */ + $this->assertEquals( + $randUtil->shuffle(range(0, 9)), + [0, 8, 1, 2, 3, 4, 5, 6, 7, 9] + ); + + + $rng = new DummyBlockRNG([ + fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10") + ]); + $randUtil = new RandUtil($rng); + //Same as first, but assoc. + $this->assertEquals($randUtil->shuffleAssoc([ + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + 'e' => 4, + 'f' => 5, + 'g' => 6, + 'h' => 7, + ]), [ + 'h' => 7, + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + 'e' => 4, + 'f' => 5, + 'g' => 6, + ]); + } + + public function testRandUtilDummy_choiceSeries(){ + $rng = new DummyBlockRNG([ + fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10") + ]); + $randUtil = new RandUtil($rng); + + //0x17(7), 0x16(6) + $this->assertEquals($randUtil->choice([0, 1, 2, 3, 4, 5]), 5); + + //0x15(5), Set 순서 유지 + $this->assertEquals($randUtil->choice([5, 3, 1, 2, 8, 0]), 8); + + //0x14(4), Js의 Object와 순서 다름! + $this->assertEquals($randUtil->choice([ + 2=>'t', 3=>'q', 4=>'x', + 'c'=> 'c', 'a'=> 'a', 'b'=> 'b' + ]), 'c'); + + //0.6275740099377194 * 38.1 = 23.91 + $this->assertEquals($randUtil->choiceUsingWeight([ + "a"=> 0.1, + "b"=> 10, + "tt"=> 2, + "x"=> -1, + "c"=> 20, + "d"=> 0, + "e"=> 6 + ]), 'c'); + + //0.658946544056166 + $this->assertEquals($randUtil->choiceUsingWeightPair([ + ['xx', 10], + ]), 'xx'); + + //0.6903152783785083 * 27.3 = 18.84560709973328 + $this->assertEquals($randUtil->choiceUsingWeightPair([ + ['e', 10], + ['d', 4], + ['c', 0.1], + ['baba', 0.2], + ['q', 9], + ['xt', 4] + ]), 'q'); + } + + public function testRNGInvalidNextBits0() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBits(0); + } + + public function testRNGInvalidNextBitsMinus1() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBits(-1); + } + + public function testRNGInvalidNextBytes0() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBytes(0); + } + + public function testRNGInvalidNextBytesMinus1() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextBytes(-1); + } + + public function testRNGInvalidNextIntOverflow() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $this->expectException('\InvalidArgumentException'); + $rng->nextInt(1 << 53); + } + + public function testRandUtilEmptyChoice() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choice([]); + } + + public function testRandUtilEmptychoiceUsingWeight() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choiceUsingWeight([]); + } + + public function testRandUtilEmptychoiceUsingWeightPair() + { + $this->expectException('\InvalidArgumentException'); + $rng = new LiteHashDRBG(self::FIXED_KEY); + $randUtil = new RandUtil($rng); + $randUtil->choiceUsingWeightPair([]); + } + + /** + * @doesNotPerformAssertions + */ + public function testRNGAcceptable() + { + $rng = new LiteHashDRBG(self::FIXED_KEY); + $rng->nextInt(0); + $rng->nextInt(1 << 53 - 1); + $rng->nextBytes(65); + $rng->nextBits(512); + + $randUtil = new RandUtil($rng); + $randUtil->choice([0, 0, 0]); + $randUtil->choiceUsingWeight([0 => 0, 1 => -1]); + $randUtil->choiceUsingWeightPair([ + [0, 0], + [1, 0], + [2, -1], + ]); + $randUtil->nextBool(1.1); + $randUtil->nextBool(-0.1); + $randUtil->shuffle([]); + $randUtil->shuffle([1]); + $randUtil->shuffleAssoc([]); + $randUtil->shuffleAssoc([1]); + $randUtil->nextRange(0, 0); + $randUtil->nextRangeInt(0, 0); + $randUtil->nextRange(1, -1); + $randUtil->nextRangeInt(1, -1); + + $longKey = self::FIXED_KEY; + foreach(range(0, 7) as $_){ + $longKey.=$longKey; + } + $rngLong = new LiteHashDRBG($longKey); + foreach(range(0, 9) as $_){ + $rngLong->nextBytes(16); + } + } + + public function testRNGBytes() + { + global $rngTestVector; + $testVector = $rngTestVector; + + $rng = new LiteHashDRBG(static::FIXED_KEY); + + $offset = 0; + $this->assertEquals($rng->nextBytes(10), substr($testVector, $offset, 10)); + $offset += 10; + $this->assertEquals($rng->nextBytes(32), substr($testVector, $offset, 32)); + $offset += 32; + $this->assertEquals($rng->nextBytes(1), substr($testVector, $offset, 1)); + $offset += 1; + $this->assertEquals($rng->nextBytes(64), substr($testVector, $offset, 64)); + $offset += 64; + $this->assertEquals($rng->nextBytes(5), substr($testVector, $offset, 5)); + $offset += 5; + $this->assertEquals($rng->nextBytes(16), substr($testVector, $offset, 16)); + $offset += 16; + } + + public function testRNGBits() + { + global $rngTestVector; + $testVector = $rngTestVector; + + $rng = new LiteHashDRBG(static::FIXED_KEY); + + $offset = 0; + $testBits = [10, 4, 15, 32, 7, 99, 512, 1, 2, 3]; + + foreach($testBits as $bits){ + $bytes = intdiv($bits + 7, 8); + $A = $rng->nextBits($bits); + $B = substr($testVector, $offset, $bytes); + + $offset += $bytes; + + if($bits % 8 != 0){ + $bitMask = 0xff >> (8 - ($bits % 8)); + $B[$bytes - 1] = chr(ord($B[$bytes - 1]) & $bitMask); + } + $this->assertEquals($A, $B); + } + } + + public function testRNGFloat() + { + global $rngTestVector; + $testVector = $rngTestVector; + + $rng = new LiteHashDRBG(static::FIXED_KEY); + $rng2 = new DummyBlockRNG([ + substr($testVector, BLOCK_SIZE * 0, BLOCK_SIZE), + substr($testVector, BLOCK_SIZE * 1, BLOCK_SIZE), + substr($testVector, BLOCK_SIZE * 2, BLOCK_SIZE), + substr($testVector, BLOCK_SIZE * 3, BLOCK_SIZE), + substr($testVector, BLOCK_SIZE * 4, BLOCK_SIZE), + ]); + + foreach(range(0, 17) as $idx){ + $this->assertEquals($rng->nextFloat1(), $rng2->nextFloat1(), "float{$idx}"); + } + } +}