1<?php
2/**
3 * Copyright 2015-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file LICENSE for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @author   Michael Slusarz <slusarz@horde.org>
9 * @category Horde
10 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package  Crypt_Blowfish
12 */
13
14/**
15 * PBKDF2 (Password-Based Key Derivation Function 2) implementation (RFC
16 * 2898; PKCS #5 v2.0).
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2015-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
22 * @package   Crypt_Blowfish
23 * @link      https://defuse.ca/php-pbkdf2.htm pbkdf2 code released to the
24 *            public domain.
25 */
26class Horde_Crypt_Blowfish_Pbkdf2
27{
28    /**
29     * Hash algorithm used to create key.
30     *
31     * @var string
32     */
33    public $hashAlgo;
34
35    /**
36     * Number of iterations to use.
37     *
38     * @var integer
39     */
40    public $iterations;
41
42    /**
43     * Salt.
44     *
45     * @var string
46     */
47    public $salt;
48
49    /**
50     * The derived key.
51     *
52     * @var string
53     */
54    protected $_key;
55
56    /**
57     * Constructor.
58     *
59     * @param string $pass        The password.
60     * @param string $key_length  Length of the derived key (in bytes).
61     * @param array $opts         Additional options:
62     *   - algo: (string) Hash algorithm.
63     *   - i_count: (integer) Iteration count.
64     *   - salt: (string) The salt to use.
65     */
66    public function __construct($pass, $key_length, array $opts = array())
67    {
68        $this->iterations = isset($opts['i_count'])
69            ? $opts['i_count']
70            : 16384;
71
72        if (($key_length <= 0) || ($this->iterations <= 0)) {
73            throw new InvalidArgumentException('Invalid arguments');
74        }
75
76        $this->hashAlgo = isset($opts['algo'])
77            ? $opts['algo']
78            : 'SHA256';
79
80        /* Nice to have, but salt does not need to be cryptographically
81         * secure random value. */
82        $this->salt = isset($opts['salt'])
83            ? $opts['salt']
84            : (function_exists('openssl_random_pseudo_bytes')
85                  ? openssl_random_pseudo_bytes($key_length)
86                  : substr(hash('sha512', new Horde_Support_Randomid(), true), 0, $key_length));
87
88        if (function_exists('hash_pbkdf2')) {
89            $this->_key = hash_pbkdf2(
90                $this->hashAlgo,
91                $pass,
92                $this->salt,
93                $this->iterations,
94                $key_length,
95                true
96            );
97            return;
98        }
99
100        $hash_length = strlen(hash($this->hashAlgo, '', true));
101        $block_count = ceil($key_length / $hash_length);
102
103        $hash = '';
104        for ($i = 1; $i <= $block_count; ++$i) {
105            // $i encoded as 4 bytes, big endian.
106            $last = $this->salt . pack('N', $i);
107            for ($j = 0; $j < $this->iterations; $j++) {
108                $last = hash_hmac($this->hashAlgo, $last, $pass, true);
109                if ($j) {
110                    $xorsum ^= $last;
111                } else {
112                    $xorsum = $last;
113                }
114            }
115            $hash .= $xorsum;
116        }
117
118        $this->_key = substr($hash, 0, $key_length);
119    }
120
121    /**
122     */
123    public function __toString()
124    {
125        return $this->_key;
126    }
127
128}
129