<?php
namespace ParagonIE\SeedSpring;
use ParagonIE\ConstantTime\Binary;
/**
* Class SeedSpring
* @package ParagonIE\SeedSpring
*/
final class SeedSpring
{
const SEEK_SET = 0;
// Increase the nonce counter
const SEEK_INCREASE = 1;
const SEEK_INC = 1;
/**
* @var int
*/
protected $counter;
/**
* @var bool
*/
protected $usePolyfill;
/**
* SeedSpring constructor.
*
* @param string $seed
* @param int $counter
*/
public function __construct($seed = '', $counter = 0)
{
if (Binary::safeStrlen($seed) !== 16) {
throw new \InvalidArgumentException('Seed must be 16 bytes');
}
$this->seed('set', $seed);
$this->counter = 0;
$this->usePolyfill = !\in_array(
'aes-256-ctr',
\openssl_get_cipher_methods(),
true
);
}
/**
* Set/get a seed (purpose: hide it from crash dumps)
*
* @param string $action (get|set)
* @param string $data (for setting)
* @return string
* @throws \Error
*/
private function seed($action = 'get', $data = '')
{
static $seed = [];
$hash = \spl_object_hash($this);
if ($action === 'set') {
$seed[$hash] = $data;
return '';
} elseif ($action === 'get') {
/**
* @var array<string, string> $seed
* @var string $return
*/
return (string) $seed[$hash];
}
throw new \Error('Unknown action');
}
/**
* Seek to a given position
*
* @param int $position
* @param int $seektype Set to self:SEEK_SET or self::SEEK_INCREASE
* @return self
*/
public function seek($position, $seektype = self::SEEK_SET)
{
switch ($seektype) {
case self::SEEK_SET:
$this->counter = $position;
break;
case self::SEEK_INCREASE:
$this->counter += $position;
break;
}
return $this;
}
/**
* Deterministic random byte generator
*
* @param int $numBytes How many bytes do we want?
* @return string
*/
public function getBytes($numBytes)
{
if ($this->usePolyfill) {
return self::aes256ctr(
\str_repeat("\0", $numBytes),
$this->seed('get'),
$this->getNonce($numBytes)
);
}
return (string) \openssl_encrypt(
\str_repeat("\0", $numBytes),
'aes-128-ctr',
$this->seed('get'),
OPENSSL_RAW_DATA,
$this->getNonce($numBytes)
);
}
/**
* Generate a deterministic random integer
*
* Stolen from paragonie/random_compat
*
* @param int $min
* @param int $max
* @return int
* @throws \Error
* @throws \Exception
*/
public function getInt($min, $max)
{
/**
* Now that we've verified our weak typing system has given us an integer,
* let's validate the logic then we can move forward with generating random
* integers along a given range.
*/
if ($min > $max) {
throw new \Error(
'Minimum value must be less than or equal to the maximum value'
);
}
if ($max === $min) {
return $min;
}
/**
* Initialize variables to 0
*
* We want to store:
* $bytes => the number of random bytes we need
* $mask => an integer bitmask (for use with the &) operator
* so we can minimize the number of discards
*
* @var int $valueShift
* @var int $mask
* @var int $bytes
* @var int $bits
* @var int $attempts
*/
$attempts = $bits = $bytes = $mask = $valueShift = 0;
/**
* At this point, $range is a positive number greater than 0. It might
* overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to
* a float and we will lose some precision.
* @var int|float $range
*/
$range = $max - $min;
/**
* Test for integer overflow:
*/
if (!\is_int($range)) {
/**
* Still safely calculate wider ranges.
* Provided by @CodesInChaos, @oittaa
*
* @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435
*
* We use ~0 as a mask in this case because it generates all 1s
*
* @ref https://eval.in/400356 (32-bit)
* @ref http://3v4l.org/XX9r5 (64-bit)
*/
$bytes = PHP_INT_SIZE;
/** @var int $mask */
$mask = ~0;
} else {
/**
* $bits is effectively ceil(log($range, 2)) without dealing with
* type juggling
*/
while ($range > 0) {
if ($bits % 8 === 0) {
++$bytes;
}
++$bits;
$range >>= 1;
$mask = $mask << 1 | 1;
}
$valueShift = $min;
}
/**
* Now that we have our parameters set up, let's begin generating
* random integers until one falls between $min and $max
*/
do {
/**
* The rejection probability is at most 0.5, so this corresponds
* to a failure probability of 2^-128 for a working RNG
*/
if ($attempts > 128) {
throw new \Exception(
'RNG is broken - too many rejections'
);
}
/**
* Let's grab the necessary number of random bytes
*/
$randomByteString = $this->getBytes($bytes);
/**
* Let's turn $randomByteString into an integer
*
* This uses bitwise operators (<< and |) to build an integer
* out of the values extracted from ord()
*
* Example: [9F] | [6D] | [32] | [0C] =>
* 159 + 27904 + 3276800 + 201326592 =>
* 204631455
*/
$val = 0;
for ($i = 0; $i < $bytes; ++$i) {
$val |= \ord($randomByteString[$i]) << ($i * 8);
}
/**
* Apply mask
*/
$val &= $mask;
$val += $valueShift;
++$attempts;
/**
* If $val overflows to a floating point number,
* ... or is larger than $max,
* ... or smaller than $min,
* then try again.
*/
} while (!\is_int($val) || $val > $max || $val < $min);
return (int) $val;
}
/**
* Get (and increment) the nonce for AES-CTR
*
* @param int $increment
* @return string
*/
protected function getNonce($increment = 0)
{
$nonce = '';
$ctr = $this->counter;
$incr = (int) \ceil(($increment + ($increment % 16)) / 16);
$this->counter += $incr;
while ($ctr > 0) {
$nonce = \pack('C', $ctr & 0xFF) . $nonce;
$ctr >>= 8;
}
return \str_pad($nonce, 16, "\0", STR_PAD_LEFT);
}
/**
* Userland polyfill for AES-256-CTR, using AES-256-ECB
*
* @param string $plaintext
* @param string $key
* @param string $nonce
* @return string
*/
public static function aes256ctr($plaintext, $key, $nonce)
{
if (empty($plaintext)) {
return '';
}
$length = Binary::safeStrlen($plaintext);
/** @var int $numBlocks */
$numBlocks = (($length - 1) >> 4) + 1;
$stream = '';
for ($i = 0; $i < $numBlocks; ++$i) {
$stream .= $nonce;
$nonce = self::ctrNonceIncrease($nonce);
}
/** @var string $xor */
$xor = \openssl_encrypt(
$stream,
'aes-256-ecb',
$key,
OPENSSL_RAW_DATA
);
return (string) (
$plaintext ^ Binary::safeSubstr($xor, 0, $length)
);
}
/**
* Increase a counter nonce, starting with the LSB (big-endian)
*
* @param string $nonce
* @return string
*/
public static function ctrNonceIncrease($nonce)
{
/** @var array<int, int> $pieces */
$pieces = \unpack('C*', $nonce);
$c = 0;
++$pieces[16];
for ($i = 16; $i > 0; --$i) {
$pieces[$i] += $c;
$c = $pieces[$i] >> 8;
$pieces[$i] &= 0xff;
}
\array_unshift($pieces, \str_repeat('C', 16));
return (string) \call_user_func_array('pack', $pieces);
}
}
|