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}"); + } } }