forked from devsam/core
feat: 해시 기반 간이 DRBG, 그에 기반한 RandUtil (#208)
기존의 Util 내의 함수는 mt_rand에 기반하고 있어서 일반적으론 충분하지만, 게임 내부에서 랜덤 값을 정교하게 제어하고 싶은 경우에는 적절하지 않았음. 따라서 직접 seed를 제어할 수 있는 난수생성기를 추가. SHA512(seed || blockIdx) 의 형태로 동작하는 DRBG이며, FIPS 표준을 따르는 RNG는 아니지만 암호학적으로 안전한 형태로 구현함. PHP, TS 두가지 형태로 구현. Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/208 Co-authored-by: hide_d <hided62@gmail.com> Co-committed-by: hide_d <hided62@gmail.com>
This commit is contained in:
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -28,5 +28,5 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"josa",
|
"josa",
|
||||||
"sammo"
|
"sammo"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
import chai, { assert } from 'chai';
|
||||||
|
import chaiBytes from 'chai-bytes';
|
||||||
|
import { bufferByteSize, LiteHashDRBG } from '../ts/util/LiteHashDRBG';
|
||||||
|
import { RandUtil } from '../ts/util/RandUtil';
|
||||||
|
import { convertBytesLikeToArrayBuffer } from '../ts/util/convertBytesLikeToArrayBuffer';
|
||||||
|
import { convertBytesLikeToUint8Array as toBytes } from '../ts/util/convertBytesLikeToUint8Array';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
chai.use(chaiBytes);
|
||||||
|
|
||||||
|
type Bytes = ArrayBuffer | DataView | Uint8Array;
|
||||||
|
type MaybeBytes = Bytes | string;
|
||||||
|
|
||||||
|
function fillBlock(body: MaybeBytes, filler: MaybeBytes = '\0', length = bufferByteSize): Uint8Array {
|
||||||
|
const u8Body = toBytes(body);
|
||||||
|
const u8Filler = toBytes(filler, false);
|
||||||
|
|
||||||
|
if (u8Filler.byteLength < 1) {
|
||||||
|
throw new Error('filler must have length');
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new Uint8Array(length);
|
||||||
|
buffer.set(u8Body, 0);
|
||||||
|
let bufferIdx = u8Body.byteLength;
|
||||||
|
|
||||||
|
while (bufferIdx + u8Filler.byteLength < length) {
|
||||||
|
buffer.set(u8Filler, bufferIdx);
|
||||||
|
bufferIdx += u8Filler.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferIdx < length) {
|
||||||
|
const slice = new Uint8Array(u8Filler.buffer, u8Filler.byteOffset, length - bufferIdx);
|
||||||
|
buffer.set(slice, bufferIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DummyBlockRNG extends LiteHashDRBG {
|
||||||
|
private repeatBlockCnt: number;
|
||||||
|
private repeatBlock: ArrayBuffer[];
|
||||||
|
|
||||||
|
public constructor(repeatBlock: MaybeBytes[], stateIdx = 0) {
|
||||||
|
super('x');
|
||||||
|
|
||||||
|
this.repeatBlock = [];
|
||||||
|
for (const rawBlock of repeatBlock) {
|
||||||
|
const block = convertBytesLikeToArrayBuffer(rawBlock);
|
||||||
|
if (block.byteLength !== bufferByteSize) {
|
||||||
|
throw new Error;
|
||||||
|
}
|
||||||
|
this.repeatBlock.push(block);
|
||||||
|
}
|
||||||
|
this.repeatBlockCnt = this.repeatBlock.length;
|
||||||
|
this.stateIdx = stateIdx;
|
||||||
|
this.bufferIdx = 0;
|
||||||
|
this.genNextBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected genNextBlock() {
|
||||||
|
if (!this.repeatBlock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.buffer = this.repeatBlock[this.stateIdx];
|
||||||
|
this.bufferIdx = 0;
|
||||||
|
this.stateIdx = (this.stateIdx + 1) % this.repeatBlockCnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixedKey = 'HelloWorld';
|
||||||
|
|
||||||
|
|
||||||
|
describe('RNGtestDummy', () => {
|
||||||
|
const rng = new DummyBlockRNG([
|
||||||
|
fillBlock('', "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff")
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('BasicConvert', () => {
|
||||||
|
assert.equal(toBytes("\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", false).length, 16);
|
||||||
|
});
|
||||||
|
it('SimpleByte', () => {
|
||||||
|
assert.equalBytes(toBytes("\x00", false), rng.nextBytes(1), 'b1');
|
||||||
|
assert.equalBytes(toBytes("\x11\x22", false), rng.nextBytes(2), 'b2');
|
||||||
|
assert.equalBytes(toBytes("\x33\x44\x55", false), rng.nextBytes(3), 'b3');
|
||||||
|
assert.equalBytes(toBytes("\x66\x77\x88\x99", false), rng.nextBytes(4), 'b4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('OverflowBlock', () => {
|
||||||
|
for (const idx in _.range(16)) {
|
||||||
|
assert.equalBytes(
|
||||||
|
toBytes("\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", false),
|
||||||
|
rng.nextBytes(16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MultiBlock', () => {
|
||||||
|
assert.equalBytes(
|
||||||
|
fillBlock('', "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", bufferByteSize * 2),
|
||||||
|
rng.nextBytes(bufferByteSize * 2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bitTest', () => {
|
||||||
|
assert.equalBytes(toBytes("\x00", false), rng.nextBits(1)); //aa
|
||||||
|
assert.equalBytes(toBytes("\x01", false), rng.nextBits(1)); //bb
|
||||||
|
assert.equalBytes(toBytes("\xcc", false), rng.nextBits(8)); //cc
|
||||||
|
assert.equalBytes(toBytes("\xdd\x02", false), rng.nextBits(10)); //ddee
|
||||||
|
assert.equalBytes(toBytes("\x7f", false), rng.nextBits(7)); //ff
|
||||||
|
assert.equalBytes(toBytes("\x00\x11\x22\x33\x44\x55\x06", false), rng.nextBits(53));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('int', () => {
|
||||||
|
assert.equal(0x77, rng.nextInt(0xff));
|
||||||
|
assert.equal(0x9988, rng.nextInt((1 << 16) - 1));
|
||||||
|
assert.equal(0xddccbbaa, rng.nextInt(0xffffffff));
|
||||||
|
assert.equal(0x0433221100ffee, rng.nextInt());
|
||||||
|
assert.equal(0x05, rng.nextInt(0x0f)); //55
|
||||||
|
assert.equal(0x06, rng.nextInt(0x12)); //66
|
||||||
|
assert.equal(0x08, rng.nextInt(99)); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8)
|
||||||
|
assert.equal(0x99, rng.nextInt(0x99)); //99
|
||||||
|
assert.equal(0xaa, rng.nextInt(0xaa)); //aa (fit Max)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('float', () => {
|
||||||
|
const floatMax = 2 ** 53;
|
||||||
|
const fa = rng.nextFloat1();
|
||||||
|
assert.equal(0x1100ffeeddccbb / floatMax, fa);
|
||||||
|
assert.isTrue(0.5313720384 > fa);
|
||||||
|
assert.isTrue(0.5313720383 < fa);
|
||||||
|
const fb = rng.nextFloat1();
|
||||||
|
assert.equal(0x08776655443322 / floatMax, fb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RandUtilDummy', () => {
|
||||||
|
it('shuffle', () => {
|
||||||
|
const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]);
|
||||||
|
const randUtil = new RandUtil(rng);
|
||||||
|
/**
|
||||||
|
* 7, [7,1,2,3,4,5,6,0]
|
||||||
|
* 6, [7,0,2,3,4,5,6,1]
|
||||||
|
* 5, [7,0,1,3,4,5,6,2]
|
||||||
|
* 4, [7,0,1,2,4,5,6,3]
|
||||||
|
* 3, [7,0,1,2,3,5,6,4]
|
||||||
|
* 2, [7,0,1,2,3,4,6,5]
|
||||||
|
* 1, [7,0,1,2,3,4,5,6]
|
||||||
|
*/
|
||||||
|
assert.deepEqual(
|
||||||
|
randUtil.shuffle(Array.from(_.range(8))),
|
||||||
|
[7, 0, 1, 2, 3, 4, 5, 6]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0, [0,1,2,3,4,5,6,7,8,9]
|
||||||
|
* 7, [0,8,2,3,4,5,6,7,1,9]
|
||||||
|
* 6, [0,8,1,3,4,5,6,7,2,9]
|
||||||
|
* 5, [0,8,1,2,4,5,6,7,3,9]
|
||||||
|
* 4, [0,8,1,2,3,5,6,7,4,9]
|
||||||
|
* 3, [0,8,1,2,3,4,6,7,5,9]
|
||||||
|
* 2, [0,8,1,2,3,4,5,7,6,9]
|
||||||
|
* 1, [0,8,1,2,3,4,5,6,7,9]
|
||||||
|
* 0, [0,8,1,2,3,4,5,6,7,9]
|
||||||
|
*/
|
||||||
|
assert.deepEqual(
|
||||||
|
randUtil.shuffle(Array.from(_.range(10))),
|
||||||
|
[0, 8, 1, 2, 3, 4, 5, 6, 7, 9]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]);
|
||||||
|
const randUtil = new RandUtil(rng);
|
||||||
|
it('choice', () => {
|
||||||
|
|
||||||
|
|
||||||
|
//0x17(7), 0x16(6)
|
||||||
|
assert.equal(randUtil.choice([0, 1, 2, 3, 4, 5]), 5);
|
||||||
|
|
||||||
|
//0x15(5), Set 순서 유지
|
||||||
|
assert.equal(randUtil.choice(new Set([5, 3, 1, 2, 8, 0])), 8);
|
||||||
|
|
||||||
|
//0x14(4), 정렬 순서상 숫자(소-대) > 문자열(삽입순) > 심볼 순서
|
||||||
|
assert.equal(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', '3': 'q' }), 'c');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('choiceUsingWeight', () => {
|
||||||
|
//0.6275740099377194 * 38.1 = 23.91
|
||||||
|
assert.equal(randUtil.choiceUsingWeight({
|
||||||
|
a: 0.1,
|
||||||
|
b: 10,
|
||||||
|
tt: 2,
|
||||||
|
x: -1,
|
||||||
|
c: 20,
|
||||||
|
d: 0,
|
||||||
|
e: 6
|
||||||
|
}), 'c');
|
||||||
|
|
||||||
|
//0.658946544056166
|
||||||
|
assert.equal(randUtil.choiceUsingWeightPair([
|
||||||
|
['xx', 10],
|
||||||
|
]), 'xx');
|
||||||
|
|
||||||
|
//0.6903152783785083 * 27.3 = 18.84560709973328
|
||||||
|
assert.equal(randUtil.choiceUsingWeightPair([
|
||||||
|
['e', 10],
|
||||||
|
['d', 4],
|
||||||
|
['c', 0.1],
|
||||||
|
['baba', 0.2],
|
||||||
|
['q', 9],
|
||||||
|
['xt', 4]
|
||||||
|
]), 'q');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RNGexpectedError', () => {
|
||||||
|
const rng = new LiteHashDRBG(fixedKey);
|
||||||
|
it('nextBits0', () => {
|
||||||
|
assert.throw(() => rng.nextBits(0));
|
||||||
|
});
|
||||||
|
it('nextBits-1', () => {
|
||||||
|
assert.throw(() => rng.nextBits(-1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nextBytes0', () => {
|
||||||
|
assert.throw(() => rng.nextBytes(0));
|
||||||
|
});
|
||||||
|
it('nextBytes-1', () => {
|
||||||
|
assert.throw(() => rng.nextBytes(-1));
|
||||||
|
});
|
||||||
|
|
||||||
|
const randUtil = new RandUtil(rng);
|
||||||
|
it('utilEmptyChoice', () => {
|
||||||
|
assert.throw(() => randUtil.choice([]));
|
||||||
|
});
|
||||||
|
it('utilEmptyChoiceUsingWeight', () => {
|
||||||
|
assert.throw(() => randUtil.choiceUsingWeight({}));
|
||||||
|
});
|
||||||
|
it('utilEmptyChoiceUsingWeightPair', () => {
|
||||||
|
assert.throw(() => randUtil.choiceUsingWeightPair([]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RNGAcceptable', () => {
|
||||||
|
const rng = new LiteHashDRBG(fixedKey);
|
||||||
|
it('RNG', () => {
|
||||||
|
rng.nextInt(0);
|
||||||
|
rng.nextInt(2 ** 53 - 1);
|
||||||
|
rng.nextBytes(65);
|
||||||
|
rng.nextBits(512);
|
||||||
|
});
|
||||||
|
|
||||||
|
const randUtil = new RandUtil(rng);
|
||||||
|
it('RandUtil', () => {
|
||||||
|
randUtil.choice([0, 0, 0]);
|
||||||
|
randUtil.choiceUsingWeight({
|
||||||
|
0: 0,
|
||||||
|
1: -1
|
||||||
|
});
|
||||||
|
randUtil.choiceUsingWeightPair([
|
||||||
|
[0, 0],
|
||||||
|
[1, 0],
|
||||||
|
[2, -2]
|
||||||
|
]);
|
||||||
|
randUtil.nextBool(1.1);
|
||||||
|
randUtil.nextBool(-0.1);
|
||||||
|
randUtil.shuffle([]);
|
||||||
|
randUtil.shuffle([1]);
|
||||||
|
randUtil.nextRange(0, 0);
|
||||||
|
randUtil.nextRangeInt(0, 0);
|
||||||
|
randUtil.nextRange(1, -1);
|
||||||
|
randUtil.nextRangeInt(1, -1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RNGLong', () => {
|
||||||
|
const longKey = fixedKey;
|
||||||
|
for(const _a of _.range(8)){
|
||||||
|
longKey.concat(longKey);
|
||||||
|
}
|
||||||
|
const rngLong = new LiteHashDRBG(longKey);
|
||||||
|
for(const _a of _.range(10)){
|
||||||
|
rngLong.nextBytes(16);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Python TestVector
|
||||||
|
import hashlib
|
||||||
|
import struct
|
||||||
|
|
||||||
|
fixedKey = 'HelloWorld'.encode('utf-8')
|
||||||
|
|
||||||
|
def hash(key, idx):
|
||||||
|
idxV = struct.pack("<I", idx)
|
||||||
|
return hashlib.sha512(key + idxV).digest()
|
||||||
|
|
||||||
|
for idx in range(5):
|
||||||
|
print(hash(fixedKey, idx).hex())
|
||||||
|
*/
|
||||||
|
describe('RNG', () => {
|
||||||
|
|
||||||
|
//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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type Bytes = ArrayBuffer | DataView | Uint8Array;
|
||||||
|
export type BytesLike = Bytes | string;
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { RNG } from "./RNG";
|
||||||
|
|
||||||
|
import { sha512 } from 'js-sha512';
|
||||||
|
|
||||||
|
import { convertBytesLikeToUint8Array } from "./convertBytesLikeToUint8Array";
|
||||||
|
import { BytesLike } from "./BytesLike";
|
||||||
|
|
||||||
|
const maxRngSupportBit = 53;
|
||||||
|
const maxInt = 0x1f_ffff_ffff_ffff; // NOTE: b 0, 10000110011, 11...11
|
||||||
|
const maxIntMore1 = 0x20_0000_0000_0000n; //NOTE: b 0, 10000110100, 00...00
|
||||||
|
const maxIntMore1f = Number(maxIntMore1);
|
||||||
|
export const bufferByteSize = 512 / 8; //SHA512
|
||||||
|
|
||||||
|
const intBitMapMask = new Map([
|
||||||
|
[0x1n, 1],
|
||||||
|
[0x3n, 2],
|
||||||
|
[0x7n, 3],
|
||||||
|
[0xfn, 4],
|
||||||
|
[0x1fn, 5],
|
||||||
|
[0x3fn, 6],
|
||||||
|
[0x7fn, 7],
|
||||||
|
[0xffn, 8],
|
||||||
|
[0x1ffn, 9],
|
||||||
|
[0x3ffn, 10],
|
||||||
|
[0x7ffn, 11],
|
||||||
|
[0xfffn, 12],
|
||||||
|
[0x1fffn, 13],
|
||||||
|
[0x3fffn, 14],
|
||||||
|
[0x7fffn, 15],
|
||||||
|
[0xffffn, 16],
|
||||||
|
[0x1ffffn, 17],
|
||||||
|
[0x3ffffn, 18],
|
||||||
|
[0x7ffffn, 19],
|
||||||
|
[0xfffffn, 20],
|
||||||
|
[0x1fffffn, 21],
|
||||||
|
[0x3fffffn, 22],
|
||||||
|
[0x7fffffn, 23],
|
||||||
|
[0xffffffn, 24],
|
||||||
|
[0x1ffffffn, 25],
|
||||||
|
[0x3ffffffn, 26],
|
||||||
|
[0x7ffffffn, 27],
|
||||||
|
[0xfffffffn, 28],
|
||||||
|
[0x1fffffffn, 29],
|
||||||
|
[0x3fffffffn, 30],
|
||||||
|
[0x7fffffffn, 31],
|
||||||
|
[0xffffffffn, 32],
|
||||||
|
[0x1ffffffffn, 33],
|
||||||
|
[0x3ffffffffn, 34],
|
||||||
|
[0x7ffffffffn, 35],
|
||||||
|
[0xfffffffffn, 36],
|
||||||
|
[0x1fffffffffn, 37],
|
||||||
|
[0x3fffffffffn, 38],
|
||||||
|
[0x7fffffffffn, 39],
|
||||||
|
[0xffffffffffn, 40],
|
||||||
|
[0x1ffffffffffn, 41],
|
||||||
|
[0x3ffffffffffn, 42],
|
||||||
|
[0x7ffffffffffn, 43],
|
||||||
|
[0xfffffffffffn, 44],
|
||||||
|
[0x1fffffffffffn, 45],
|
||||||
|
[0x3fffffffffffn, 46],
|
||||||
|
[0x7fffffffffffn, 47],
|
||||||
|
[0xffffffffffffn, 48],
|
||||||
|
[0x1ffffffffffffn, 49],
|
||||||
|
[0x3ffffffffffffn, 50],
|
||||||
|
[0x7ffffffffffffn, 51],
|
||||||
|
[0xfffffffffffffn, 52],
|
||||||
|
[0x1fffffffffffffn, 53],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function calcBitMask(n: bigint): bigint {
|
||||||
|
n |= n >> 1n;
|
||||||
|
n |= n >> 2n;
|
||||||
|
n |= n >> 4n;
|
||||||
|
n |= n >> 8n;
|
||||||
|
n |= n >> 16n;
|
||||||
|
n |= n >> 32n;
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
export class LiteHashDRBG implements RNG {
|
||||||
|
|
||||||
|
protected buffer!: ArrayBuffer;
|
||||||
|
protected bufferIdx!: number;
|
||||||
|
protected hq: DataView;
|
||||||
|
protected hqIdxPos: number;
|
||||||
|
|
||||||
|
public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) {
|
||||||
|
if(bufferIdx < 0){
|
||||||
|
throw new Error(`bufferIdx ${bufferIdx} < 0`);
|
||||||
|
}
|
||||||
|
if(bufferIdx >= bufferByteSize){
|
||||||
|
throw new Error(`bufferidx ${bufferIdx} >= ${bufferByteSize}`);
|
||||||
|
}
|
||||||
|
if(stateIdx < 0){
|
||||||
|
throw new Error(`stateIdx ${stateIdx} < 0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedU8 = convertBytesLikeToUint8Array(seed);
|
||||||
|
const hqBuffer = new ArrayBuffer(seedU8.byteLength + 4);
|
||||||
|
const hqU8 = new Uint8Array(hqBuffer);
|
||||||
|
|
||||||
|
hqU8.set(seedU8, 0);
|
||||||
|
this.hq = new DataView(hqBuffer);
|
||||||
|
this.hqIdxPos = seedU8.byteLength;
|
||||||
|
|
||||||
|
this.genNextBlock();
|
||||||
|
this.bufferIdx = bufferIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected genNextBlock(): void {
|
||||||
|
this.hq.setUint32(this.hqIdxPos, this.stateIdx, true);
|
||||||
|
const digest = sha512.arrayBuffer(this.hq.buffer);
|
||||||
|
this.buffer = digest;
|
||||||
|
this.bufferIdx = 0;
|
||||||
|
this.stateIdx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxInt(): number {
|
||||||
|
return maxInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBytes(bytes: number, baseBytes?: number): Uint8Array {
|
||||||
|
bytes |= 0;
|
||||||
|
if (bytes <= 0) {
|
||||||
|
throw new Error(`${bytes} <= 0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bufferIdx + bytes <= bufferByteSize) {
|
||||||
|
if(baseBytes === undefined || bytes >= baseBytes){
|
||||||
|
const result = this.buffer.slice(this.bufferIdx, this.bufferIdx + bytes);
|
||||||
|
this.bufferIdx += bytes;
|
||||||
|
if (this.bufferIdx === bufferByteSize) {
|
||||||
|
this.genNextBlock();
|
||||||
|
}
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBuffer = new ArrayBuffer(Math.max(bytes, baseBytes));
|
||||||
|
const result = new Uint8Array(resultBuffer);
|
||||||
|
result.set(new Uint8Array(this.buffer, this.bufferIdx, bytes));
|
||||||
|
this.bufferIdx += bytes;
|
||||||
|
if( this.bufferIdx === bufferByteSize){
|
||||||
|
this.genNextBlock();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBuffer = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes);
|
||||||
|
const result = new Uint8Array(resultBuffer);
|
||||||
|
|
||||||
|
result.set(new Uint8Array(this.buffer, this.bufferIdx));
|
||||||
|
let offset = bufferByteSize - this.bufferIdx;
|
||||||
|
let remain = bytes - offset;
|
||||||
|
|
||||||
|
while (remain > bufferByteSize) {
|
||||||
|
this.genNextBlock();
|
||||||
|
result.set(new Uint8Array(this.buffer), offset);
|
||||||
|
offset += bufferByteSize;
|
||||||
|
remain -= bufferByteSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.genNextBlock();
|
||||||
|
if (remain === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.set(new Uint8Array(this.buffer, 0, remain), offset);
|
||||||
|
this.bufferIdx = remain;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBits(bits: number, baseBytes?: number): Uint8Array {
|
||||||
|
bits |= 0;
|
||||||
|
const bytes = (bits + 7) >> 3;
|
||||||
|
const headBits = bits & 0x7;
|
||||||
|
|
||||||
|
const result = this.nextBytes(bytes, baseBytes);
|
||||||
|
if (headBits === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[bytes - 1] &= 0xff >> (8 - headBits);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _nextInt(bits: number): bigint{
|
||||||
|
const buffer = this.nextBits(bits, 8);
|
||||||
|
const dataView = new DataView(buffer.buffer);
|
||||||
|
return dataView.getBigUint64(0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
if (max === undefined || max === maxInt) {
|
||||||
|
return Number(this._nextInt(maxRngSupportBit));
|
||||||
|
}
|
||||||
|
if (max > maxInt) {
|
||||||
|
throw new Error('Over max int');
|
||||||
|
}
|
||||||
|
if (max === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (max < 0) {
|
||||||
|
return -this.nextInt(-max);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mask = calcBitMask(BigInt(max));
|
||||||
|
const bits = intBitMapMask.get(mask) as number;
|
||||||
|
|
||||||
|
let n = Number(this._nextInt(bits));
|
||||||
|
while (n > max){
|
||||||
|
n = Number(this._nextInt(bits));
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while(true){
|
||||||
|
const nInt = this._nextInt(maxRngSupportBit + 1);
|
||||||
|
if(nInt < maxIntMore1){
|
||||||
|
return Number(nInt) / maxIntMore1f;
|
||||||
|
}
|
||||||
|
if(nInt === maxIntMore1){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static build(seed: BytesLike, stateIdx = 0): LiteHashDRBG{
|
||||||
|
return new LiteHashDRBG(seed, stateIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface RNG {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* nextInt()가 반환 가능한 최대값
|
||||||
|
*/
|
||||||
|
getMaxInt(): number;
|
||||||
|
|
||||||
|
nextBytes(bytes: number): Uint8Array;
|
||||||
|
nextBits(bits: number): Uint8Array;
|
||||||
|
|
||||||
|
nextInt(max?: number): number;
|
||||||
|
nextFloat1(): number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { RNG } from './RNG';
|
||||||
|
|
||||||
|
export class RandUtil {
|
||||||
|
constructor(protected rng: RNG) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
return this.rng.nextFloat1();
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextRange(min: number, max: number): number {
|
||||||
|
const range = max - min;
|
||||||
|
return this.nextFloat1() * (range) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextRangeInt(min: number, max: number): number {
|
||||||
|
const range = max - min;
|
||||||
|
return this.rng.nextInt(range) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
return this.rng.nextInt(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBit(): boolean {
|
||||||
|
const view = new DataView(this.rng.nextBits(1));
|
||||||
|
return view.getUint8(0) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBool(prob = 0.5): boolean {
|
||||||
|
if (prob >= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.nextFloat1() < prob;
|
||||||
|
}
|
||||||
|
|
||||||
|
public shuffle<T>(srcArray: T[]): T[] {
|
||||||
|
const cnt = srcArray.length;
|
||||||
|
if(cnt === 0){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (cnt > this.rng.getMaxInt()) {
|
||||||
|
throw 'Invalid random int range';
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: T[] = Array.from(srcArray);
|
||||||
|
for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) {
|
||||||
|
const destIdx = this.rng.nextInt(cnt - srcIdx - 1) + srcIdx;
|
||||||
|
if(srcIdx === destIdx){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Object는 integer key에 예외가 있어 shuffleAssoc은 없음
|
||||||
|
|
||||||
|
public choice<T>(items: T[] | Record<string | number, T> | Set<T>): T {
|
||||||
|
if (items instanceof Array) {
|
||||||
|
if(items.length === 0){
|
||||||
|
throw new Error('Empty items');
|
||||||
|
}
|
||||||
|
const idx = this.rng.nextInt(items.length - 1);
|
||||||
|
return items[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items instanceof Set) {
|
||||||
|
return this.choice(Array.from(items.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items[this.choice(Array.from(Object.keys(items)))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public choiceUsingWeight(items: Record<string | number, number>): string | number {
|
||||||
|
if(Object.keys(items).length === 0){
|
||||||
|
throw new Error('Empty items');
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
for (const value of Object.values(items)) {
|
||||||
|
if (value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sum += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rd = this.nextFloat1() * sum;
|
||||||
|
|
||||||
|
for (const [item, value] of Object.entries(items)) {
|
||||||
|
if (value <= 0) {
|
||||||
|
if (rd <= 0) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rd <= value) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
rd -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unreacheable');
|
||||||
|
}
|
||||||
|
|
||||||
|
public choiceUsingWeightPair<T>(items: [T, number][]): T {
|
||||||
|
if(items.length === 0){
|
||||||
|
throw new Error('Empty items');
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
for (const [, value] of items) {
|
||||||
|
if (value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sum += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rd = this.nextFloat1() * sum;
|
||||||
|
|
||||||
|
for (const [item, value] of items) {
|
||||||
|
if (value <= 0) {
|
||||||
|
if (rd <= 0) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rd <= value) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
rd -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unreacheable');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+3
-1
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm-run-all test-php-gateway test-ts",
|
"test": "npm-run-all test-php-gateway test-ts",
|
||||||
"test-php-gateway": "vendor/bin/phpunit --bootstrap vendor/autoload.php tests",
|
"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",
|
"build": "webpack",
|
||||||
"buildDev": "webpack --mode=development",
|
"buildDev": "webpack --mode=development",
|
||||||
"watch": "webpack watch --mode=development",
|
"watch": "webpack watch --mode=development",
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
"babel-preset-modern-browsers": "^15.0.2",
|
"babel-preset-modern-browsers": "^15.0.2",
|
||||||
"bootswatch": "^5.1.3",
|
"bootswatch": "^5.1.3",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
"chai-bytes": "^0.1.2",
|
||||||
"clean-terminal-webpack-plugin": "^3.0.0",
|
"clean-terminal-webpack-plugin": "^3.0.0",
|
||||||
"css-loader": "^6.6.0",
|
"css-loader": "^6.6.0",
|
||||||
"cssnano": "^5.0.17",
|
"cssnano": "^5.0.17",
|
||||||
@@ -95,6 +96,7 @@
|
|||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"ts-node": "^10.6.0",
|
"ts-node": "^10.6.0",
|
||||||
|
"tsconfig-paths": "^3.13.0",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-eslint-parser": "^8.2.0",
|
"vue-eslint-parser": "^8.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
//NOTE: JavaScript 버전과 일치
|
||||||
|
const MAX_RNG_SUPPORT_BIT = 53;
|
||||||
|
if (PHP_INT_SIZE * 8 < MAX_RNG_SUPPORT_BIT) {
|
||||||
|
throw new \RangeException("PHP not support {$MAX_RNG_SUPPORT_BIT} bit integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reseed를 하지 않는 단순한 형태의 sha512 drbg
|
||||||
|
* float: bit를 강제로 채움
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
class LiteHashDRBG implements RNG
|
||||||
|
{
|
||||||
|
const MAX_INT = (1 << MAX_RNG_SUPPORT_BIT) - 1;
|
||||||
|
const BUFFER_BYTE_SIZE = 512 / 8; //SHA512
|
||||||
|
|
||||||
|
protected string $buffer;
|
||||||
|
protected int $bufferIdx;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$hq = $this->seed . pack('V', $this->stateIdx);
|
||||||
|
$this->buffer = hash('sha512', $hq, true);
|
||||||
|
$this->bufferIdx = 0;
|
||||||
|
$this->stateIdx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function getMaxInt(): int
|
||||||
|
{
|
||||||
|
return self::MAX_INT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INT_BIT_MASK_MAP = [
|
||||||
|
0x1 => 1,
|
||||||
|
0x3 => 2,
|
||||||
|
0x7 => 3,
|
||||||
|
0xf => 4,
|
||||||
|
0x1f => 5,
|
||||||
|
0x3f => 6,
|
||||||
|
0x7f => 7,
|
||||||
|
0xff => 8,
|
||||||
|
0x1ff => 9,
|
||||||
|
0x3ff => 10,
|
||||||
|
0x7ff => 11,
|
||||||
|
0xfff => 12,
|
||||||
|
0x1fff => 13,
|
||||||
|
0x3fff => 14,
|
||||||
|
0x7fff => 15,
|
||||||
|
0xffff => 16,
|
||||||
|
0x1ffff => 17,
|
||||||
|
0x3ffff => 18,
|
||||||
|
0x7ffff => 19,
|
||||||
|
0xfffff => 20,
|
||||||
|
0x1fffff => 21,
|
||||||
|
0x3fffff => 22,
|
||||||
|
0x7fffff => 23,
|
||||||
|
0xffffff => 24,
|
||||||
|
0x1ffffff => 25,
|
||||||
|
0x3ffffff => 26,
|
||||||
|
0x7ffffff => 27,
|
||||||
|
0xfffffff => 28,
|
||||||
|
0x1fffffff => 29,
|
||||||
|
0x3fffffff => 30,
|
||||||
|
0x7fffffff => 31,
|
||||||
|
0xffffffff => 32,
|
||||||
|
0x1ffffffff => 33,
|
||||||
|
0x3ffffffff => 34,
|
||||||
|
0x7ffffffff => 35,
|
||||||
|
0xfffffffff => 36,
|
||||||
|
0x1fffffffff => 37,
|
||||||
|
0x3fffffffff => 38,
|
||||||
|
0x7fffffffff => 39,
|
||||||
|
0xffffffffff => 40,
|
||||||
|
0x1ffffffffff => 41,
|
||||||
|
0x3ffffffffff => 42,
|
||||||
|
0x7ffffffffff => 43,
|
||||||
|
0xfffffffffff => 44,
|
||||||
|
0x1fffffffffff => 45,
|
||||||
|
0x3fffffffffff => 46,
|
||||||
|
0x7fffffffffff => 47,
|
||||||
|
0xffffffffffff => 48,
|
||||||
|
0x1ffffffffffff => 49,
|
||||||
|
0x3ffffffffffff => 50,
|
||||||
|
0x7ffffffffffff => 51,
|
||||||
|
0xfffffffffffff => 52,
|
||||||
|
0x1fffffffffffff => 53,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static function calcBitMask($n)
|
||||||
|
{
|
||||||
|
$n |= $n >> 1;
|
||||||
|
$n |= $n >> 2;
|
||||||
|
$n |= $n >> 4;
|
||||||
|
$n |= $n >> 8;
|
||||||
|
$n |= $n >> 16;
|
||||||
|
$n |= $n >> 32;
|
||||||
|
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function nextBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
if ($bytes <= 0) {
|
||||||
|
throw new \InvalidArgumentException("{$bytes} <= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->bufferIdx + $bytes <= self::BUFFER_BYTE_SIZE) {
|
||||||
|
$buffer = substr($this->buffer, $this->bufferIdx, $bytes);
|
||||||
|
$this->bufferIdx += $bytes;
|
||||||
|
if ($this->bufferIdx == self::BUFFER_BYTE_SIZE) {
|
||||||
|
$this->genNextBlock();
|
||||||
|
}
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$result = [substr($this->buffer, $this->bufferIdx)];
|
||||||
|
$remain = $bytes - (self::BUFFER_BYTE_SIZE - $this->bufferIdx);
|
||||||
|
|
||||||
|
while ($remain > self::BUFFER_BYTE_SIZE) {
|
||||||
|
$this->genNextBlock();
|
||||||
|
$result[] = $this->buffer;
|
||||||
|
$remain -= self::BUFFER_BYTE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->genNextBlock();
|
||||||
|
if ($remain == 0) {
|
||||||
|
return join("", $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = substr($this->buffer, 0, $remain);
|
||||||
|
$this->bufferIdx = $remain;
|
||||||
|
return join("", $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBits(int $bits): string
|
||||||
|
{
|
||||||
|
$bytes = ($bits + 7) >> 3;
|
||||||
|
$headBits = $bits & 0x7;
|
||||||
|
|
||||||
|
$buffer = $this->nextBytes($bytes);
|
||||||
|
if ($headBits === 0) {
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
$buffer[$bytes - 1] = chr(ord($buffer[$bytes - 1]) & (0xff >> (8 - $headBits)));
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static private function parseU64(string $value): int
|
||||||
|
{
|
||||||
|
return unpack('P', $value)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _nextInt(int $bits): int
|
||||||
|
{
|
||||||
|
$buffer = $this->nextBits($bits) . "\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
|
||||||
|
return self::parseU64($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextInt(?int $max = null): int
|
||||||
|
{
|
||||||
|
if ($max === null || $max === self::MAX_INT) {
|
||||||
|
$buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT) . "\x00";
|
||||||
|
return self::parseU64($buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max > self::MAX_INT) {
|
||||||
|
throw new \InvalidArgumentException('Over Max Int');
|
||||||
|
} else if ($max === 0) {
|
||||||
|
return 0;
|
||||||
|
} else if ($max < 0) {
|
||||||
|
return -$this->nextInt(-$max);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask = self::calcBitMask($max);
|
||||||
|
$bits = self::INT_BIT_MASK_MAP[$mask];
|
||||||
|
|
||||||
|
$n = $this->_nextInt($bits);
|
||||||
|
while ($n > $max) {
|
||||||
|
$n = $this->_nextInt($bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextFloat1(): float
|
||||||
|
{
|
||||||
|
$max = 1 << MAX_RNG_SUPPORT_BIT;
|
||||||
|
while (true) {
|
||||||
|
$buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT + 1) . "\x00";
|
||||||
|
$nInt = self::parseU64($buffer);
|
||||||
|
if ($nInt <= $max) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $nInt / $max;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function build(string $seed, int $idx = 0): self
|
||||||
|
{
|
||||||
|
return new LiteHashDRBG($seed, $idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
interface RNG
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int nextInt()가 반환 가능한 최대값
|
||||||
|
*/
|
||||||
|
public static function getMaxInt(): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param int $bytes
|
||||||
|
* @return string Little Endian 형태로 채워진 binary 값
|
||||||
|
*/
|
||||||
|
public function nextBytes(int $bytes): string;
|
||||||
|
public function nextBits(int $bits): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ?int $max 최대치(해당 값 포함)
|
||||||
|
* @return int 0과 최대치 사이의 임의의 정수
|
||||||
|
*/
|
||||||
|
public function nextInt(?int $max = null): int;
|
||||||
|
|
||||||
|
public function nextFloat1(): float;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
class RandUtil
|
||||||
|
{
|
||||||
|
public function __construct(protected RNG $rng)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextFloat1(): float
|
||||||
|
{
|
||||||
|
return $this->rng->nextFloat1();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextRange(int|float $min, int|float $max): float
|
||||||
|
{
|
||||||
|
$range = $max - $min;
|
||||||
|
return $this->nextFloat1() * $range + $min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextRangeInt(int $min, int $max): int
|
||||||
|
{
|
||||||
|
$range = $max - $min;
|
||||||
|
if ($range > $this->rng->getMaxInt()) {
|
||||||
|
throw new \InvalidArgumentException("Invalid random int range");
|
||||||
|
}
|
||||||
|
return $this->rng->nextInt($range) + $min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextInt(?int $max = null): int{
|
||||||
|
return $this->rng->nextInt($max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBit(): bool
|
||||||
|
{
|
||||||
|
return $this->rng->nextBits(1) !== "\0";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextBool(int|float $prob = 0.5): bool
|
||||||
|
{
|
||||||
|
if ($prob >= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $this->nextFloat1() < $prob;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shuffle(array $srcArray): array
|
||||||
|
{
|
||||||
|
if(!$srcArray){
|
||||||
|
return $srcArray;
|
||||||
|
}
|
||||||
|
$cnt = count($srcArray);
|
||||||
|
if ($cnt > $this->rng->getMaxInt()) {
|
||||||
|
throw new \InvalidArgumentException("Invalid random int range");
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($srcArray as $val) {
|
||||||
|
$result[] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
//PHP의 range는 max 포함
|
||||||
|
foreach (range(0, $cnt - 1) as $srcIdx) {
|
||||||
|
$destIdx = $this->rng->nextInt($cnt - $srcIdx - 1) + $srcIdx;
|
||||||
|
if($srcIdx === $destIdx){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tmpVal = $result[$srcIdx];
|
||||||
|
$result[$srcIdx] = $result[$destIdx];
|
||||||
|
$result[$destIdx] = $tmpVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shuffleAssoc(array $srcArray): array
|
||||||
|
{
|
||||||
|
if(!$srcArray){
|
||||||
|
return $srcArray;
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($this->shuffle(array_keys($srcArray)) as $key) {
|
||||||
|
$result[$key] = $srcArray[$key];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function choice(array $items)
|
||||||
|
{
|
||||||
|
$keys = array_keys($items);
|
||||||
|
$keyIdx = $this->rng->nextInt(count($keys) - 1);
|
||||||
|
return $items[$keys[$keyIdx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function choiceUsingWeight(array $items)
|
||||||
|
{
|
||||||
|
$sum = 0;
|
||||||
|
foreach ($items as $value) {
|
||||||
|
if ($value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sum += $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rd = $this->nextFloat1() * $sum;
|
||||||
|
foreach ($items as $item => $value) {
|
||||||
|
if ($value <= 0) {
|
||||||
|
$value = 0;
|
||||||
|
}
|
||||||
|
if ($rd <= $value) {
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
$rd -= $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//fallback. 이곳으로 빠지지 않음
|
||||||
|
end($items);
|
||||||
|
return $items[key($items)][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function choiceUsingWeightPair(array $items)
|
||||||
|
{
|
||||||
|
$sum = 0;
|
||||||
|
foreach ($items as [$item, $value]) {
|
||||||
|
if ($value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sum += $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rd = $this->nextFloat1() * $sum;
|
||||||
|
foreach ($items as [$item, $value]) {
|
||||||
|
if ($value <= 0) {
|
||||||
|
$value = 0;
|
||||||
|
}
|
||||||
|
if ($rd <= $value) {
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
$rd -= $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//fallback. 이곳으로 빠지지 않음
|
||||||
|
end($items);
|
||||||
|
return $items[key($items)][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -809,5 +809,4 @@ class Util extends \utilphp\util
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use sammo\LiteHashDRBG;
|
||||||
|
use sammo\RandUtil;
|
||||||
|
|
||||||
|
const BLOCK_SIZE = LiteHashDRBG::BUFFER_BYTE_SIZE;
|
||||||
|
|
||||||
|
function fillBlock(string $src, string $filler = '\0', int $length = BLOCK_SIZE)
|
||||||
|
{
|
||||||
|
$tmp = [$src];
|
||||||
|
$fillerLen = strlen($filler);
|
||||||
|
if ($fillerLen < 1) {
|
||||||
|
throw new \InvalidArgumentException('filler must have length');
|
||||||
|
}
|
||||||
|
|
||||||
|
$remainFill = intdiv($length - strlen($src), $fillerLen);
|
||||||
|
for (; $remainFill > 0; $remainFill--) {
|
||||||
|
$tmp[] = $filler;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = join("", $tmp);
|
||||||
|
|
||||||
|
$moreLen = $length - strlen($result);
|
||||||
|
if ($moreLen === 0) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result . substr($filler, 0, $moreLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyBlockRNG extends LiteHashDRBG
|
||||||
|
{
|
||||||
|
private int $repeatBlockCnt;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param string[] $repeatBlock
|
||||||
|
* @param int $stateIdx
|
||||||
|
* @return void
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function __construct(protected array $repeatBlock, protected int $stateIdx = 0)
|
||||||
|
{
|
||||||
|
foreach ($repeatBlock as $block) {
|
||||||
|
if (strlen($block) != BLOCK_SIZE) {
|
||||||
|
throw new RuntimeException('Invalid repeat block');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->repeatBlockCnt = count($this->repeatBlock);
|
||||||
|
$this->genNextBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function genNextBlock(): void
|
||||||
|
{
|
||||||
|
$this->buffer = $this->repeatBlock[$this->stateIdx];
|
||||||
|
$this->bufferIdx = 0;
|
||||||
|
$this->stateIdx = ($this->stateIdx + 1) % $this->repeatBlockCnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Python TestVector
|
||||||
|
import hashlib
|
||||||
|
import struct
|
||||||
|
|
||||||
|
fixedKey = 'HelloWorld'.encode('utf-8')
|
||||||
|
|
||||||
|
def hash(key, idx):
|
||||||
|
idxV = struct.pack("<I", idx)
|
||||||
|
return hashlib.sha512(key + idxV).digest()
|
||||||
|
|
||||||
|
for idx in range(5):
|
||||||
|
print(hash(fixedKey, idx).hex())
|
||||||
|
*/
|
||||||
|
$rngTestVector = hex2bin(join('', [
|
||||||
|
'24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4',
|
||||||
|
'2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165',
|
||||||
|
'8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a',
|
||||||
|
'398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47',
|
||||||
|
'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb',
|
||||||
|
]));
|
||||||
|
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 testRandUtilDummy_shuffle(){
|
||||||
|
$rng = new DummyBlockRNG([
|
||||||
|
fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10")
|
||||||
|
]);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 7, [7,1,2,3,4,5,6,0]
|
||||||
|
* 6, [7,0,2,3,4,5,6,1]
|
||||||
|
* 5, [7,0,1,3,4,5,6,2]
|
||||||
|
* 4, [7,0,1,2,4,5,6,3]
|
||||||
|
* 3, [7,0,1,2,3,5,6,4]
|
||||||
|
* 2, [7,0,1,2,3,4,6,5]
|
||||||
|
* 1, [7,0,1,2,3,4,5,6]
|
||||||
|
*/
|
||||||
|
$this->assertEquals(
|
||||||
|
$randUtil->shuffle(range(0, 7)),
|
||||||
|
[7, 0, 1, 2, 3, 4, 5, 6]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0, [0,1,2,3,4,5,6,7,8,9]
|
||||||
|
* 7, [0,8,2,3,4,5,6,7,1,9]
|
||||||
|
* 6, [0,8,1,3,4,5,6,7,2,9]
|
||||||
|
* 5, [0,8,1,2,4,5,6,7,3,9]
|
||||||
|
* 4, [0,8,1,2,3,5,6,7,4,9]
|
||||||
|
* 3, [0,8,1,2,3,4,6,7,5,9]
|
||||||
|
* 2, [0,8,1,2,3,4,5,7,6,9]
|
||||||
|
* 1, [0,8,1,2,3,4,5,6,7,9]
|
||||||
|
* 0, [0,8,1,2,3,4,5,6,7,9]
|
||||||
|
*/
|
||||||
|
$this->assertEquals(
|
||||||
|
$randUtil->shuffle(range(0, 9)),
|
||||||
|
[0, 8, 1, 2, 3, 4, 5, 6, 7, 9]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
$rng = new DummyBlockRNG([
|
||||||
|
fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10")
|
||||||
|
]);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
//Same as first, but assoc.
|
||||||
|
$this->assertEquals($randUtil->shuffleAssoc([
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
'h' => 7,
|
||||||
|
]), [
|
||||||
|
'h' => 7,
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRandUtilDummy_choiceSeries(){
|
||||||
|
$rng = new DummyBlockRNG([
|
||||||
|
fillBlock('', "\x17\x16\x15\x14\x13\x12\x11\x10")
|
||||||
|
]);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
|
||||||
|
//0x17(7), 0x16(6)
|
||||||
|
$this->assertEquals($randUtil->choice([0, 1, 2, 3, 4, 5]), 5);
|
||||||
|
|
||||||
|
//0x15(5), Set 순서 유지
|
||||||
|
$this->assertEquals($randUtil->choice([5, 3, 1, 2, 8, 0]), 8);
|
||||||
|
|
||||||
|
//0x14(4), Js의 Object와 순서 다름!
|
||||||
|
$this->assertEquals($randUtil->choice([
|
||||||
|
2=>'t', 3=>'q', 4=>'x',
|
||||||
|
'c'=> 'c', 'a'=> 'a', 'b'=> 'b'
|
||||||
|
]), 'c');
|
||||||
|
|
||||||
|
//0.6275740099377194 * 38.1 = 23.91
|
||||||
|
$this->assertEquals($randUtil->choiceUsingWeight([
|
||||||
|
"a"=> 0.1,
|
||||||
|
"b"=> 10,
|
||||||
|
"tt"=> 2,
|
||||||
|
"x"=> -1,
|
||||||
|
"c"=> 20,
|
||||||
|
"d"=> 0,
|
||||||
|
"e"=> 6
|
||||||
|
]), 'c');
|
||||||
|
|
||||||
|
//0.658946544056166
|
||||||
|
$this->assertEquals($randUtil->choiceUsingWeightPair([
|
||||||
|
['xx', 10],
|
||||||
|
]), 'xx');
|
||||||
|
|
||||||
|
//0.6903152783785083 * 27.3 = 18.84560709973328
|
||||||
|
$this->assertEquals($randUtil->choiceUsingWeightPair([
|
||||||
|
['e', 10],
|
||||||
|
['d', 4],
|
||||||
|
['c', 0.1],
|
||||||
|
['baba', 0.2],
|
||||||
|
['q', 9],
|
||||||
|
['xt', 4]
|
||||||
|
]), 'q');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGInvalidNextBits0()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng->nextBits(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGInvalidNextBitsMinus1()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng->nextBits(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGInvalidNextBytes0()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng->nextBytes(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGInvalidNextBytesMinus1()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng->nextBytes(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGInvalidNextIntOverflow()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng->nextInt(1 << 53);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRandUtilEmptyChoice()
|
||||||
|
{
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
$randUtil->choice([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRandUtilEmptychoiceUsingWeight()
|
||||||
|
{
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
$randUtil->choiceUsingWeight([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRandUtilEmptychoiceUsingWeightPair()
|
||||||
|
{
|
||||||
|
$this->expectException('\InvalidArgumentException');
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
$randUtil->choiceUsingWeightPair([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @doesNotPerformAssertions
|
||||||
|
*/
|
||||||
|
public function testRNGAcceptable()
|
||||||
|
{
|
||||||
|
$rng = new LiteHashDRBG(self::FIXED_KEY);
|
||||||
|
$rng->nextInt(0);
|
||||||
|
$rng->nextInt(1 << 53 - 1);
|
||||||
|
$rng->nextBytes(65);
|
||||||
|
$rng->nextBits(512);
|
||||||
|
|
||||||
|
$randUtil = new RandUtil($rng);
|
||||||
|
$randUtil->choice([0, 0, 0]);
|
||||||
|
$randUtil->choiceUsingWeight([0 => 0, 1 => -1]);
|
||||||
|
$randUtil->choiceUsingWeightPair([
|
||||||
|
[0, 0],
|
||||||
|
[1, 0],
|
||||||
|
[2, -1],
|
||||||
|
]);
|
||||||
|
$randUtil->nextBool(1.1);
|
||||||
|
$randUtil->nextBool(-0.1);
|
||||||
|
$randUtil->shuffle([]);
|
||||||
|
$randUtil->shuffle([1]);
|
||||||
|
$randUtil->shuffleAssoc([]);
|
||||||
|
$randUtil->shuffleAssoc([1]);
|
||||||
|
$randUtil->nextRange(0, 0);
|
||||||
|
$randUtil->nextRangeInt(0, 0);
|
||||||
|
$randUtil->nextRange(1, -1);
|
||||||
|
$randUtil->nextRangeInt(1, -1);
|
||||||
|
|
||||||
|
$longKey = self::FIXED_KEY;
|
||||||
|
foreach(range(0, 7) as $_){
|
||||||
|
$longKey.=$longKey;
|
||||||
|
}
|
||||||
|
$rngLong = new LiteHashDRBG($longKey);
|
||||||
|
foreach(range(0, 9) as $_){
|
||||||
|
$rngLong->nextBytes(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGBytes()
|
||||||
|
{
|
||||||
|
global $rngTestVector;
|
||||||
|
$testVector = $rngTestVector;
|
||||||
|
|
||||||
|
$rng = new LiteHashDRBG(static::FIXED_KEY);
|
||||||
|
|
||||||
|
$offset = 0;
|
||||||
|
$this->assertEquals($rng->nextBytes(10), substr($testVector, $offset, 10));
|
||||||
|
$offset += 10;
|
||||||
|
$this->assertEquals($rng->nextBytes(32), substr($testVector, $offset, 32));
|
||||||
|
$offset += 32;
|
||||||
|
$this->assertEquals($rng->nextBytes(1), substr($testVector, $offset, 1));
|
||||||
|
$offset += 1;
|
||||||
|
$this->assertEquals($rng->nextBytes(64), substr($testVector, $offset, 64));
|
||||||
|
$offset += 64;
|
||||||
|
$this->assertEquals($rng->nextBytes(5), substr($testVector, $offset, 5));
|
||||||
|
$offset += 5;
|
||||||
|
$this->assertEquals($rng->nextBytes(16), substr($testVector, $offset, 16));
|
||||||
|
$offset += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGBits()
|
||||||
|
{
|
||||||
|
global $rngTestVector;
|
||||||
|
$testVector = $rngTestVector;
|
||||||
|
|
||||||
|
$rng = new LiteHashDRBG(static::FIXED_KEY);
|
||||||
|
|
||||||
|
$offset = 0;
|
||||||
|
$testBits = [10, 4, 15, 32, 7, 99, 512, 1, 2, 3];
|
||||||
|
|
||||||
|
foreach($testBits as $bits){
|
||||||
|
$bytes = intdiv($bits + 7, 8);
|
||||||
|
$A = $rng->nextBits($bits);
|
||||||
|
$B = substr($testVector, $offset, $bytes);
|
||||||
|
|
||||||
|
$offset += $bytes;
|
||||||
|
|
||||||
|
if($bits % 8 != 0){
|
||||||
|
$bitMask = 0xff >> (8 - ($bits % 8));
|
||||||
|
$B[$bytes - 1] = chr(ord($B[$bytes - 1]) & $bitMask);
|
||||||
|
}
|
||||||
|
$this->assertEquals($A, $B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRNGFloat()
|
||||||
|
{
|
||||||
|
global $rngTestVector;
|
||||||
|
$testVector = $rngTestVector;
|
||||||
|
|
||||||
|
$rng = new LiteHashDRBG(static::FIXED_KEY);
|
||||||
|
$rng2 = new DummyBlockRNG([
|
||||||
|
substr($testVector, BLOCK_SIZE * 0, BLOCK_SIZE),
|
||||||
|
substr($testVector, BLOCK_SIZE * 1, BLOCK_SIZE),
|
||||||
|
substr($testVector, BLOCK_SIZE * 2, BLOCK_SIZE),
|
||||||
|
substr($testVector, BLOCK_SIZE * 3, BLOCK_SIZE),
|
||||||
|
substr($testVector, BLOCK_SIZE * 4, BLOCK_SIZE),
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach(range(0, 17) as $idx){
|
||||||
|
$this->assertEquals($rng->nextFloat1(), $rng2->nextFloat1(), "float{$idx}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user