From 801b846d690589ed86232e44bdec26713cd13fe7 Mon Sep 17 00:00:00 2001 From: hide_d Date: Wed, 9 Mar 2022 11:35:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20RNG=20ts=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mocharc.json | 12 ++ .vscode/settings.json | 4 +- hwe/test-ts/rng.test.ts | 194 +++++++++++++++++++ hwe/ts/util/BytesLike.ts | 2 + hwe/ts/util/LiteHashDRBG.ts | 53 ++--- hwe/ts/util/RNG.ts | 4 +- hwe/ts/util/convertBytesLikeToArrayBuffer.ts | 17 ++ hwe/ts/util/convertBytesLikeToUint8Array.ts | 17 ++ package.json | 4 +- 9 files changed, 269 insertions(+), 38 deletions(-) create mode 100644 .mocharc.json create mode 100644 hwe/test-ts/rng.test.ts create mode 100644 hwe/ts/util/BytesLike.ts create mode 100644 hwe/ts/util/convertBytesLikeToArrayBuffer.ts create mode 100644 hwe/ts/util/convertBytesLikeToUint8Array.ts 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..b28bf780 --- /dev/null +++ b/hwe/test-ts/rng.test.ts @@ -0,0 +1,194 @@ +import chai, { assert, expect } 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('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); + }) +}); \ 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 index 251f66e2..1eae489a 100644 --- a/hwe/ts/util/LiteHashDRBG.ts +++ b/hwe/ts/util/LiteHashDRBG.ts @@ -2,15 +2,14 @@ import { RNG } from "./RNG"; import { sha512 } from 'js-sha512'; -//Buffer를 써도 된다면 Buffer가 최선이지만.. -type Bytes = ArrayBuffer | DataView | Uint8Array; -type MaybeBytes = Bytes | string; +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); -const bufferByteSize = 512 / 8; //SHA512 +export const bufferByteSize = 512 / 8; //SHA512 const intBitMapMask = new Map([ [0x1n, 1], @@ -85,7 +84,7 @@ export class LiteHashDRBG implements RNG { protected hq: DataView; protected hqIdxPos: number; - public constructor(protected seed: MaybeBytes, protected stateIdx = 0, bufferIdx = 0) { + public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) { if(bufferIdx < 0){ throw new Error(`bufferIdx ${bufferIdx} < 0`); } @@ -96,18 +95,7 @@ export class LiteHashDRBG implements RNG { throw new Error(`stateIdx ${stateIdx} < 0`); } - const seedU8 = (() => { - if (seed instanceof Uint8Array) { - return seed; - } - if (seed instanceof ArrayBuffer) { - return new Uint8Array(seed); - } - if (typeof(seed) === 'string'){ - return (new TextEncoder()).encode(seed); - } - return new Uint8Array(seed.buffer); - })(); + const seedU8 = convertBytesLikeToUint8Array(seed); const hqBuffer = new ArrayBuffer(seedU8.byteLength + 4); const hqU8 = new Uint8Array(hqBuffer); @@ -131,7 +119,7 @@ export class LiteHashDRBG implements RNG { return maxInt; } - public nextBytes(bytes: number, baseBytes?: number): ArrayBuffer { + public nextBytes(bytes: number, baseBytes?: number): Uint8Array { bytes |= 0; if (bytes <= 0) { throw new Error(`${bytes} <= 0`); @@ -139,17 +127,17 @@ export class LiteHashDRBG implements RNG { if (this.bufferIdx + bytes <= bufferByteSize) { if(baseBytes === undefined || bytes >= baseBytes){ - const result = this.buffer.slice(this.bufferIdx, bytes); + const result = this.buffer.slice(this.bufferIdx, this.bufferIdx + bytes); this.bufferIdx += bytes; if (this.bufferIdx === bufferByteSize) { this.genNextBlock(); } - return result; + return new Uint8Array(result); } - const result = new ArrayBuffer(Math.max(bytes, baseBytes)); - const resultU8 = new Uint8Array(result); - resultU8.set(new Uint8Array(this.buffer, this.bufferIdx, bytes)); + 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(); @@ -157,16 +145,16 @@ export class LiteHashDRBG implements RNG { return result; } - const result = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes); - const resultU8 = new Uint8Array(result); + const resultBuffer = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes); + const result = new Uint8Array(resultBuffer); - resultU8.set(new Uint8Array(this.buffer, this.bufferIdx)); + result.set(new Uint8Array(this.buffer, this.bufferIdx)); let offset = bufferByteSize - this.bufferIdx; let remain = bytes - offset; while (remain > bufferByteSize) { this.genNextBlock(); - resultU8.set(new Uint8Array(this.buffer), offset); + result.set(new Uint8Array(this.buffer), offset); offset += bufferByteSize; remain -= bufferByteSize; } @@ -176,12 +164,12 @@ export class LiteHashDRBG implements RNG { return result; } - resultU8.set(new Uint8Array(this.buffer, 0, remain), offset); + result.set(new Uint8Array(this.buffer, 0, remain), offset); this.bufferIdx = remain; return result; } - public nextBits(bits: number, baseBytes?: number): ArrayBuffer { + public nextBits(bits: number, baseBytes?: number): Uint8Array { bits |= 0; const bytes = (bits + 7) >> 3; const headBits = bits & 0x7; @@ -191,14 +179,13 @@ export class LiteHashDRBG implements RNG { return result; } - const resultU8 = new Uint8Array(result); - resultU8[bytes - 1] &= 0xff >> (8 - headBits); + result[bytes - 1] &= 0xff >> (8 - headBits); return result; } protected _nextInt(bits: number): bigint{ const buffer = this.nextBits(bits, 8); - const dataView = new DataView(buffer); + const dataView = new DataView(buffer.buffer); return dataView.getBigUint64(0, true); } @@ -239,7 +226,7 @@ export class LiteHashDRBG implements RNG { } } - public static build(seed: MaybeBytes, stateIdx = 0): LiteHashDRBG{ + 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 index 3641c475..d097f1e7 100644 --- a/hwe/ts/util/RNG.ts +++ b/hwe/ts/util/RNG.ts @@ -5,8 +5,8 @@ export interface RNG { */ getMaxInt(): number; - nextBytes(bytes: number): ArrayBuffer; - nextBits(bits: number): ArrayBuffer; + nextBytes(bytes: number): Uint8Array; + nextBits(bits: number): Uint8Array; nextInt(max?: number): number; nextFloat1(): number; 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",