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 } } } - };