226 lines
5.7 KiB
PHP
226 lines
5.7 KiB
PHP
<?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);
|
|
}
|
|
}
|