<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace ZendTest\Crypt\Key\Derivation;

use PHPUnit\Framework\TestCase;
use ReflectionClass;
use Zend\Crypt\Key\Derivation\Exception;
use Zend\Crypt\Key\Derivation\Scrypt;

/**
 * @group      Zend_Crypt
 */
class ScryptTest extends TestCase
{
    protected static function getMethod($name)
    {
        $class = new ReflectionClass(Scrypt::class);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method;
    }

    /**
     * Test vector of Salsa 20/8 core
     *
     * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-7
     */
    public function testVectorSalsa208Core()
    {
        $hexInput  = '7e 87 9a 21 4f 3e c9 86 7c a9 40 e6 41 71 8f 26
                      ba ee 55 5b 8c 61 c1 b5 0d f8 46 11 6d cd 3b 1d
                      ee 24 f3 19 df 9b 3d 85 14 12 1e 4b 5a c5 aa 32
                      76 02 1d 29 09 c7 48 29 ed eb c6 8d b8 b8 c2 5e';

        $hexOutput = 'a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
                      04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
                      b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
                      e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81';

        $salsaAlg = 'salsa208Core32';
        if (PHP_INT_SIZE === 8) {
            $salsaAlg = 'salsa208Core64';
        }
        $salsa20 = self::getMethod($salsaAlg);
        $obj     = $this->getMockForAbstractClass(Scrypt::class);
        $input   = self::hex2bin(str_replace([' ', "\n"], '', $hexInput));
        $result  = $salsa20->invokeArgs($obj, [$input]);

        $this->assertEquals(64, strlen($input), 'Input must be a string of 64 bytes');
        $this->assertEquals(64, strlen($result), 'Output must be a string of 64 bytes');
        $this->assertEquals(str_replace([' ', "\n"], '', $hexOutput), bin2hex($result));
    }

    /**
     * Test vector of Scrypt BlockMix
     *
     * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-8
     */
    public function testVectorScryptBlockMix()
    {
        $hexInput  = 'f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
                      77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
                      89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
                      09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7

                      89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
                      cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
                      67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
                      7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89';

        $hexOutput = 'a4 1f 85 9c 66 08 cc 99 3b 81 ca cb 02 0c ef 05
                      04 4b 21 81 a2 fd 33 7d fd 7b 1c 63 96 68 2f 29
                      b4 39 31 68 e3 c9 e6 bc fe 6b c5 b7 a0 6d 96 ba
                      e4 24 cc 10 2c 91 74 5c 24 ad 67 3d c7 61 8f 81

                      20 ed c9 75 32 38 81 a8 05 40 f6 4c 16 2d cd 3c
                      21 07 7c fe 5f 8d 5f e2 b1 a4 16 8f 95 36 78 b7
                      7d 3b 3d 80 3b 60 e4 ab 92 09 96 e5 9b 4d 53 b6
                      5d 2a 22 58 77 d5 ed f5 84 2c b9 f1 4e ef e4 25';

        $blockMix = self::getMethod('scryptBlockMix');
        $obj      = $this->getMockForAbstractClass(Scrypt::class);
        $input    = self::hex2bin(str_replace([' ', "\n"], '', $hexInput));
        $result   = $blockMix->invokeArgs($obj, [$input, 1]);

        $this->assertEquals(str_replace([' ', "\n"], '', $hexOutput), bin2hex($result));
    }

    /**
     * Test vector of Scrypt ROMix
     *
     * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-9
     */
    public function testVectorScryptROMix()
    {
        $hexInput  = 'f7 ce 0b 65 3d 2d 72 a4 10 8c f5 ab e9 12 ff dd
                      77 76 16 db bb 27 a7 0e 82 04 f3 ae 2d 0f 6f ad
                      89 f6 8f 48 11 d1 e8 7b cc 3b d7 40 0a 9f fd 29
                      09 4f 01 84 63 95 74 f3 9a e5 a1 31 52 17 bc d7
                      89 49 91 44 72 13 bb 22 6c 25 b5 4d a8 63 70 fb
                      cd 98 43 80 37 46 66 bb 8f fc b5 bf 40 c2 54 b0
                      67 d2 7c 51 ce 4a d5 fe d8 29 c9 0b 50 5a 57 1b
                      7f 4d 1c ad 6a 52 3c da 77 0e 67 bc ea af 7e 89';

        $hexOutput = '79 cc c1 93 62 9d eb ca 04 7f 0b 70 60 4b f6 b6
                      2c e3 dd 4a 96 26 e3 55 fa fc 61 98 e6 ea 2b 46
                      d5 84 13 67 3b 99 b0 29 d6 65 c3 57 60 1f b4 26
                      a0 b2 f4 bb a2 00 ee 9f 0a 43 d1 9b 57 1a 9c 71
                      ef 11 42 e6 5d 5a 26 6f dd ca 83 2c e5 9f aa 7c
                      ac 0b 9c f1 be 2b ff ca 30 0d 01 ee 38 76 19 c4
                      ae 12 fd 44 38 f2 03 a0 e4 e1 c4 7e c3 14 86 1f
                      4e 90 87 cb 33 39 6a 68 73 e8 f9 d2 53 9a 4b 8e';

        $roMix  = self::getMethod('scryptROMix');
        $obj    = $this->getMockForAbstractClass(Scrypt::class);
        $input  = self::hex2bin(str_replace([' ', "\n"], '', $hexInput));
        $result = $roMix->invokeArgs($obj, [$input, 16, 1]);

        $this->assertEquals(str_replace([' ', "\n"], '', $hexOutput), bin2hex($result));
    }

    /**
     * Test Vector Scrypt
     *
     * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-11
     */
    public function testVectorScrypt()
    {
        $hexOutput = 'd3 3c 6e c1 81 8d aa f7 28 f5 5a fa df ea a5 58
                      b3 8e fa 81 30 5b 35 21 a7 f1 2f 4b e0 97 e8 4d
                      18 40 92 d2 a2 e9 3b f7 1f d1 ef e0 52 71 0f 66
                      b9 56 ce 45 da 43 aa 90 99 de 74 06 d3 a0 5e 2a';

        $result = Scrypt::calc('password', '', 16, 1, 1, 64);
        $this->assertEquals(64, strlen($result));
        $this->assertEquals(str_replace([' ', "\n"], '', $hexOutput), bin2hex($result));
    }

    public function testScryptWrongN1()
    {
        $this->expectException(Exception\InvalidArgumentException::class);
        Scrypt::calc('test', 'salt', 17, 1, 1, 64);
    }

    public function testScryptWronN2()
    {
        $this->expectException(Exception\InvalidArgumentException::class);
        Scrypt::calc('test', 'salt', PHP_INT_MAX, 1, 1, 64);
    }

    public function testScryptWrongR()
    {
        $this->expectException(Exception\InvalidArgumentException::class);
        Scrypt::calc('test', 'salt', PHP_INT_MAX / 128, 4, 1, 64);
    }

    /**
     * Test scrypt correct size output
     */
    public function testScryptSize()
    {
        for ($size = 0; $size < 64; $size++) {
            if (extension_loaded('Scrypt') && ($size < 16)) {
                $this->expectException(Exception\InvalidArgumentException::class);
            }
            $result = Scrypt::calc('test', 'salt', 16, 1, 1, $size) ?: '';
            $this->assertEquals($size, strlen($result));
        }
    }

    /**
     * Convert a string with hex values in binary string
     *
     * @param  string $hex
     * @return string
     */
    protected static function hex2bin($hex)
    {
        $len    = strlen($hex);
        $result = '';
        for ($i = 0; $i < $len; $i += 2) {
            $result .= chr(hexdec($hex[$i] . $hex[$i + 1]));
        }
        return $result;
    }
}
