From 11e17c4ae515b82a70b1c20243d28751646f7348 Mon Sep 17 00:00:00 2001 From: hide_d Date: Tue, 1 Mar 2022 05:46:22 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(inprogress):=20=ED=8A=B9=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=EB=93=9C=EA=B0=92=EC=9D=84=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=95=98=EB=8A=94=20=EA=B2=B0=EC=A0=95?= =?UTF-8?q?=EB=A1=A0=EC=A0=81=20=EB=82=9C=EC=88=98=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/ts/util/RNG.ts | 13 +++ hwe/ts/util/RandUtil.ts | 116 ++++++++++++++++++++ src/sammo/LiteHashDRBG.php | 214 +++++++++++++++++++++++++++++++++++++ src/sammo/RNG.php | 28 +++++ src/sammo/RandUtil.php | 133 +++++++++++++++++++++++ 5 files changed, 504 insertions(+) create mode 100644 hwe/ts/util/RNG.ts create mode 100644 hwe/ts/util/RandUtil.ts create mode 100644 src/sammo/LiteHashDRBG.php create mode 100644 src/sammo/RNG.php create mode 100644 src/sammo/RandUtil.php diff --git a/hwe/ts/util/RNG.ts b/hwe/ts/util/RNG.ts new file mode 100644 index 00000000..3641c475 --- /dev/null +++ b/hwe/ts/util/RNG.ts @@ -0,0 +1,13 @@ +export interface RNG { + + /** + * nextInt()가 반환 가능한 최대값 + */ + getMaxInt(): number; + + nextBytes(bytes: number): ArrayBuffer; + nextBits(bits: number): ArrayBuffer; + + 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..573e7570 --- /dev/null +++ b/hwe/ts/util/RandUtil.ts @@ -0,0 +1,116 @@ +import { RNG } from './RNG'; + +export class RandUtils{ + 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 nextBit(): boolean{ + const view = new DataView(this.rng.nextBits(1)); + return view.getUint8(0) != 0; + } + + public nextBool(prob = 0.5): boolean{ + return this.nextFloat1() < prob; + } + + public shuffle(srcArray: T[]): T[]{ + const cnt = srcArray.length; + 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; + [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){ + 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{ + 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{ + 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/src/sammo/LiteHashDRBG.php b/src/sammo/LiteHashDRBG.php new file mode 100644 index 00000000..dacf7f4f --- /dev/null +++ b/src/sammo/LiteHashDRBG.php @@ -0,0 +1,214 @@ +genNextBlock(); + } + + protected function genNextBlock(): void + { + $hq = $this->seed . pack('P', $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) { + $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 $max): int + { + $mask = self::calcBitMask($max); + $bits = self::INT_BIT_MASK_MAP[$mask]; + + $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); + } + + $n = $this->_nextInt($max); + while ($n > $max) { + $n = $this->_nextInt($max); + } + + 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 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 + { + $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; + $tmpVal = $result[$srcIdx]; + $result[$srcIdx] = $result[$destIdx]; + $result[$destIdx] = $tmpVal; + } + + return $result; + } + + public function shuffleAssoc(array $srcArray): array + { + $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]; + } +} -- 2.54.0 From a2498506c3437b224d3f52c190f3307f55fcbb75 Mon Sep 17 00:00:00 2001 From: hide_d Date: Wed, 2 Mar 2022 02:17:33 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=95=EB=A1=A0?= =?UTF-8?q?=EC=A0=81=20=EB=82=9C=EC=88=98=20=EC=83=9D=EC=84=B1=EA=B8=B0=20?= =?UTF-8?q?ts=20=EB=B2=84=EC=A0=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/ts/util/LiteHashDRBG.ts | 234 ++++++++++++++++++++++++++++++++++++ hwe/ts/util/RandUtil.ts | 61 +++++----- src/sammo/LiteHashDRBG.php | 25 ++-- src/sammo/RandUtil.php | 4 + src/sammo/Util.php | 1 - 5 files changed, 285 insertions(+), 40 deletions(-) create mode 100644 hwe/ts/util/LiteHashDRBG.ts diff --git a/hwe/ts/util/LiteHashDRBG.ts b/hwe/ts/util/LiteHashDRBG.ts new file mode 100644 index 00000000..13df8e6a --- /dev/null +++ b/hwe/ts/util/LiteHashDRBG.ts @@ -0,0 +1,234 @@ +import { RNG } from "./RNG"; + +import { sha512 } from 'js-sha512'; + +//Buffer를 써도 된다면 Buffer가 최선이지만.. +type Bytes = ArrayBuffer | DataView | Uint8Array; +type MaybeBytes = Bytes | string; + +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 + +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: MaybeBytes, protected 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 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(); + } + + 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): ArrayBuffer { + 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, bytes); + this.bufferIdx += bytes; + if (this.bufferIdx === bufferByteSize) { + this.genNextBlock(); + } + return result; + } + + const result = new ArrayBuffer(Math.max(bytes, baseBytes)); + const resultU8 = new Uint8Array(result); + resultU8.set(new Uint8Array(this.buffer, this.bufferIdx, bytes)); + this.bufferIdx += bytes; + if( this.bufferIdx === bufferByteSize){ + this.genNextBlock(); + } + return result; + } + + const result = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes); + const resultU8 = new Uint8Array(result); + + resultU8.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); + offset += bufferByteSize; + remain -= bufferByteSize; + } + + this.genNextBlock(); + if (remain === 0) { + return result; + } + + resultU8.set(new Uint8Array(this.buffer, 0, remain), offset); + this.bufferIdx = remain; + return result; + } + + public nextBits(bits: number, baseBytes?: number): ArrayBuffer { + bits |= 0; + const bytes = (bits + 7) >> 3; + const headBits = bits & 0x7; + + const result = this.nextBytes(bytes, baseBytes); + if (headBits === 0) { + return result; + } + + const resultU8 = new Uint8Array(result); + resultU8[bytes - 1] &= 0xff >> (8 - headBits); + return result; + } + + protected _nextInt(bits: number): bigint{ + const buffer = this.nextBits(bits, 8); + const dataView = new DataView(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: MaybeBytes, stateIdx = 0): LiteHashDRBG{ + return new LiteHashDRBG(seed, stateIdx); + } +} \ No newline at end of file diff --git a/hwe/ts/util/RandUtil.ts b/hwe/ts/util/RandUtil.ts index 573e7570..e6ac149e 100644 --- a/hwe/ts/util/RandUtil.ts +++ b/hwe/ts/util/RandUtil.ts @@ -1,41 +1,48 @@ import { RNG } from './RNG'; -export class RandUtils{ - constructor(protected rng: RNG){ +export class RandUtil { + constructor(protected rng: RNG) { } - public nextFloat1(): number{ + public nextFloat1(): number { return this.rng.nextFloat1(); } - public nextRange(min: number, max: number): number{ + public nextRange(min: number, max: number): number { const range = max - min; return this.nextFloat1() * (range) + min; } - public nextRangeInt(min: number, max: number): number{ + public nextRangeInt(min: number, max: number): number { const range = max - min; return this.rng.nextInt(range) + min; } - public nextBit(): boolean{ + 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{ + public nextBool(prob = 0.5): boolean { + if (prob >= 1) { + return true; + } return this.nextFloat1() < prob; } - public shuffle(srcArray: T[]): T[]{ + public shuffle(srcArray: T[]): T[] { const cnt = srcArray.length; - if(cnt > this.rng.getMaxInt()){ + if (cnt > this.rng.getMaxInt()) { throw 'Invalid random int range'; } const result: T[] = Array.from(srcArray); - for(let srcIdx = 0; srcIdx < cnt; srcIdx += 1){ + for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) { const destIdx = this.rng.nextInt(cnt - srcIdx - 1) + srcIdx; [result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]]; } @@ -45,23 +52,23 @@ export class RandUtils{ //Object는 integer key에 예외가 있어 shuffleAssoc은 없음 - public choice(items: T[]|Record|Set): T{ - if(items instanceof Array){ + public choice(items: T[] | Record | Set): T { + if (items instanceof Array) { const idx = this.rng.nextInt(items.length - 1); return items[idx]; } - if(items instanceof Set){ + 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{ + public choiceUsingWeight(items: Record): string | number { let sum = 0; - for(const value of Object.values(items)){ - if(value <= 0){ + for (const value of Object.values(items)) { + if (value <= 0) { continue; } sum += value; @@ -69,15 +76,15 @@ export class RandUtils{ let rd = this.nextFloat1() * sum; - for(const [item, value] of Object.entries(items)){ - if(value <= 0){ - if(rd <= 0){ + for (const [item, value] of Object.entries(items)) { + if (value <= 0) { + if (rd <= 0) { return item; } continue; } - if(rd <= value){ + if (rd <= value) { return item; } rd -= value; @@ -86,10 +93,10 @@ export class RandUtils{ throw new Error('Unreacheable'); } - public choiceUsingWeightPair(items: [T, number][]): T{ + public choiceUsingWeightPair(items: [T, number][]): T { let sum = 0; - for(const [, value] of items){ - if(value <= 0){ + for (const [, value] of items) { + if (value <= 0) { continue; } sum += value; @@ -97,15 +104,15 @@ export class RandUtils{ let rd = this.nextFloat1() * sum; - for(const [item, value] of items){ - if(value <= 0){ - if(rd <= 0){ + for (const [item, value] of items) { + if (value <= 0) { + if (rd <= 0) { return item; } continue; } - if(rd <= value){ + if (rd <= value) { return item; } rd -= value; diff --git a/src/sammo/LiteHashDRBG.php b/src/sammo/LiteHashDRBG.php index dacf7f4f..c46b8f87 100644 --- a/src/sammo/LiteHashDRBG.php +++ b/src/sammo/LiteHashDRBG.php @@ -21,14 +21,14 @@ class LiteHashDRBG implements RNG protected string $buffer; protected int $bufferIdx; - protected function __construct(protected string $seed, protected int $stateIdx = 0) + public function __construct(protected string $seed, protected int $stateIdx = 0) { $this->genNextBlock(); } protected function genNextBlock(): void { - $hq = $this->seed . pack('P', $this->stateIdx); + $hq = $this->seed . pack('V', $this->stateIdx); $this->buffer = hash('sha512', $hq, true); $this->bufferIdx = 0; $this->stateIdx += 1; @@ -149,9 +149,10 @@ class LiteHashDRBG implements RNG $headBits = $bits & 0x7; $buffer = $this->nextBytes($bytes); - if ($headBits != 0) { - $buffer[$bytes - 1] = chr(ord($buffer[$bytes - 1]) & (0xff >> (8 - $headBits))) ; + if ($headBits === 0) { + return $buffer; } + $buffer[$bytes - 1] = chr(ord($buffer[$bytes - 1]) & (0xff >> (8 - $headBits))); return $buffer; } @@ -161,11 +162,8 @@ class LiteHashDRBG implements RNG return unpack('P', $value)[1]; } - private function _nextInt(int $max): int + private function _nextInt(int $bits): int { - $mask = self::calcBitMask($max); - $bits = self::INT_BIT_MASK_MAP[$mask]; - $buffer = $this->nextBits($bits) . "\x00\x00\x00\x00\x00\x00\x00"; return self::parseU64($buffer); @@ -186,9 +184,12 @@ class LiteHashDRBG implements RNG return -$this->nextInt(-$max); } - $n = $this->_nextInt($max); + $mask = self::calcBitMask($max); + $bits = self::INT_BIT_MASK_MAP[$mask]; + + $n = $this->_nextInt($bits); while ($n > $max) { - $n = $this->_nextInt($max); + $n = $this->_nextInt($bits); } return $n; @@ -197,10 +198,10 @@ class LiteHashDRBG implements RNG public function nextFloat1(): float { $max = 1 << MAX_RNG_SUPPORT_BIT; - while(true){ + while (true) { $buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT + 1) . "\x00"; $nInt = self::parseU64($buffer); - if($nInt <= $max){ + if ($nInt <= $max) { break; } } diff --git a/src/sammo/RandUtil.php b/src/sammo/RandUtil.php index 3472816d..ab776e7d 100644 --- a/src/sammo/RandUtil.php +++ b/src/sammo/RandUtil.php @@ -28,6 +28,10 @@ class RandUtil 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"; 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 } } } - }; -- 2.54.0 From 7ef9e67f5dda5367638ac7bfcfb805fb92448e85 Mon Sep 17 00:00:00 2001 From: hide_d Date: Mon, 7 Mar 2022 00:25:36 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20LiteHashDRBG=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20=EC=83=81=EC=84=B8=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/ts/util/LiteHashDRBG.ts | 13 ++++++++++++- src/sammo/LiteHashDRBG.php | 12 +++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/hwe/ts/util/LiteHashDRBG.ts b/hwe/ts/util/LiteHashDRBG.ts index 13df8e6a..251f66e2 100644 --- a/hwe/ts/util/LiteHashDRBG.ts +++ b/hwe/ts/util/LiteHashDRBG.ts @@ -85,7 +85,17 @@ export class LiteHashDRBG implements RNG { protected hq: DataView; protected hqIdxPos: number; - public constructor(protected seed: MaybeBytes, protected stateIdx = 0) { + public constructor(protected seed: MaybeBytes, 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 = (() => { if (seed instanceof Uint8Array) { return seed; @@ -106,6 +116,7 @@ export class LiteHashDRBG implements RNG { this.hqIdxPos = seedU8.byteLength; this.genNextBlock(); + this.bufferIdx = bufferIdx; } protected genNextBlock(): void { diff --git a/src/sammo/LiteHashDRBG.php b/src/sammo/LiteHashDRBG.php index c46b8f87..35aaa3f5 100644 --- a/src/sammo/LiteHashDRBG.php +++ b/src/sammo/LiteHashDRBG.php @@ -21,9 +21,19 @@ class LiteHashDRBG implements RNG protected string $buffer; protected int $bufferIdx; - public function __construct(protected string $seed, protected int $stateIdx = 0) + public function __construct(protected string $seed, protected int $stateIdx = 0, int $bufferIdx = 0) { + if($bufferIdx < 0){ + throw new \InvalidArgumentException("bufferIdx {$bufferIdx} < 0"); + } + if($bufferIdx >= 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 -- 2.54.0 From d0576806e292c372b632f12af89860348a133afd Mon Sep 17 00:00:00 2001 From: hide_d Date: Tue, 8 Mar 2022 22:04:16 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20unit=20test=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/ts/util/RandUtil.ts | 12 +++ src/sammo/RandUtil.php | 6 ++ tests/RNGTest.php | 212 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 tests/RNGTest.php diff --git a/hwe/ts/util/RandUtil.ts b/hwe/ts/util/RandUtil.ts index e6ac149e..5e3bfa89 100644 --- a/hwe/ts/util/RandUtil.ts +++ b/hwe/ts/util/RandUtil.ts @@ -37,6 +37,9 @@ export class RandUtil { public shuffle(srcArray: T[]): T[] { const cnt = srcArray.length; + if(cnt === 0){ + return []; + } if (cnt > this.rng.getMaxInt()) { throw 'Invalid random int range'; } @@ -54,6 +57,9 @@ export class RandUtil { 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]; } @@ -66,6 +72,9 @@ export class RandUtil { } 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) { @@ -94,6 +103,9 @@ export class RandUtil { } 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) { diff --git a/src/sammo/RandUtil.php b/src/sammo/RandUtil.php index ab776e7d..67b11294 100644 --- a/src/sammo/RandUtil.php +++ b/src/sammo/RandUtil.php @@ -47,6 +47,9 @@ class RandUtil 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"); @@ -69,6 +72,9 @@ class RandUtil public function shuffleAssoc(array $srcArray): array { + if(!$srcArray){ + return $srcArray; + } $result = []; foreach ($this->shuffle(array_keys($srcArray)) as $key) { $result[$key] = $srcArray[$key]; diff --git a/tests/RNGTest.php b/tests/RNGTest.php new file mode 100644 index 00000000..66172a43 --- /dev/null +++ b/tests/RNGTest.php @@ -0,0 +1,212 @@ + 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; + } +} + +class RNGTest extends PHPUnit\Framework\TestCase +{ + + const FIXED_KEY = 'HelloWorld'; + public function testDummyRNG() + { + //Block의 값을 고정하고 입력값을 확인 + $rng = new DummyBlockRNG([ + fillBlock('', "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff") + ]); + + $this->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 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); + } +} -- 2.54.0 From 801b846d690589ed86232e44bdec26713cd13fe7 Mon Sep 17 00:00:00 2001 From: hide_d Date: Wed, 9 Mar 2022 11:35:52 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20RNG=20ts=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=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", -- 2.54.0 From d2dcaaf93cff12da1466b8048de810e8c154fe6b Mon Sep 17 00:00:00 2001 From: hide_d Date: Sat, 12 Mar 2022 23:47:49 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20rng=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/test-ts/rng.test.ts | 224 +++++++++++++++++++++++++++++++++++----- hwe/ts/util/RandUtil.ts | 3 + src/sammo/RandUtil.php | 3 + tests/RNGTest.php | 205 +++++++++++++++++++++++++++++++++++- 4 files changed, 410 insertions(+), 25 deletions(-) diff --git a/hwe/test-ts/rng.test.ts b/hwe/test-ts/rng.test.ts index b28bf780..2461a680 100644 --- a/hwe/test-ts/rng.test.ts +++ b/hwe/test-ts/rng.test.ts @@ -1,4 +1,4 @@ -import chai, { assert, expect } from 'chai'; +import chai, { assert } from 'chai'; import chaiBytes from 'chai-bytes'; import { bufferByteSize, LiteHashDRBG } from '../ts/util/LiteHashDRBG'; import { RandUtil } from '../ts/util/RandUtil'; @@ -36,6 +36,7 @@ function fillBlock(body: MaybeBytes, filler: MaybeBytes = '\0', length = bufferB return buffer; } + class DummyBlockRNG extends LiteHashDRBG { private repeatBlockCnt: number; private repeatBlock: ArrayBuffer[]; @@ -133,37 +134,117 @@ describe('RNGtestDummy', () => { }); }); -describe('RNGexpectedError', ()=>{ - const rng = new LiteHashDRBG(fixedKey); - it('nextBits0', ()=>{ - assert.throw(()=>rng.nextBits(0)); - }); - it('nextBits-1', ()=>{ - assert.throw(()=>rng.nextBits(-1)); - }); +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] + ); - it('nextBytes0', ()=>{ - assert.throw(()=>rng.nextBytes(0)); - }); - it('nextBytes-1', ()=>{ - assert.throw(()=>rng.nextBytes(-1)); + /** + * 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('utilEmptyChoice', ()=>{ - assert.throw(()=>randUtil.choice([])); + 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('utilEmptyChoiceUsingWeight', ()=>{ - assert.throw(()=>randUtil.choiceUsingWeight({})); - }); - it('utilEmptyChoiceUsingWeightPair', ()=>{ - assert.throw(()=>randUtil.choiceUsingWeightPair([])); + + 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('RNGAcceptable', ()=>{ +describe('RNGexpectedError', () => { const rng = new LiteHashDRBG(fixedKey); - it('RNG', ()=>{ + 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); @@ -171,7 +252,7 @@ describe('RNGAcceptable', ()=>{ }); const randUtil = new RandUtil(rng); - it('RandUtil', ()=>{ + it('RandUtil', () => { randUtil.choice([0, 0, 0]); randUtil.choiceUsingWeight({ 0: 0, @@ -191,4 +272,99 @@ describe('RNGAcceptable', ()=>{ 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/RandUtil.ts b/hwe/ts/util/RandUtil.ts index 5e3bfa89..5c9c17b1 100644 --- a/hwe/ts/util/RandUtil.ts +++ b/hwe/ts/util/RandUtil.ts @@ -47,6 +47,9 @@ export class RandUtil { 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]]; } diff --git a/src/sammo/RandUtil.php b/src/sammo/RandUtil.php index 67b11294..64726a0f 100644 --- a/src/sammo/RandUtil.php +++ b/src/sammo/RandUtil.php @@ -62,6 +62,9 @@ class RandUtil //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; diff --git a/tests/RNGTest.php b/tests/RNGTest.php index 66172a43..f75abf11 100644 --- a/tests/RNGTest.php +++ b/tests/RNGTest.php @@ -28,7 +28,6 @@ function fillBlock(string $src, string $filler = '\0', int $length = BLOCK_SIZE) return $result . substr($filler, 0, $moreLen); } - class DummyBlockRNG extends LiteHashDRBG { private int $repeatBlockCnt; @@ -58,10 +57,31 @@ class DummyBlockRNG extends LiteHashDRBG } } +/* Python TestVector +import hashlib +import struct + +fixedKey = 'HelloWorld'.encode('utf-8') + +def hash(key, idx): + idxV = struct.pack("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); @@ -208,5 +336,80 @@ class RNGTest extends PHPUnit\Framework\TestCase $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}"); + } } } -- 2.54.0