PHP Classes

File: src/SeedSpring.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   PHP Seed Spring   src/SeedSpring.php   Download  
File: src/SeedSpring.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP Seed Spring
Generate 128 bit pseudo-random numbers
Author: By
Last change:
Date: 3 years ago
Size: 8,992 bytes
 

Contents

Class file image Download
<?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);
    }
}