Completed
Pull Request — master (#3237)
by Emanuele
14:50
created

Token_Hash::_state_seed()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.2017

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 0
dl 0
loc 21
rs 9.2728
c 0
b 0
f 0
ccs 7
cts 11
cp 0.6364
crap 6.2017
1
<?php
2
3
/**
4
 * Used to generate random hash codes for use in forms or anywhere else
5
 * that a secure random hash value is needed
6
 *
7
 * @name      ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
10
 *
11
 * @version 1.1
12
 *
13
 */
14
15
/**
16
 * Class Token_Hash
17
 *
18
 * Used to generate a high entropy random hash for use in one time use forms
19
 *
20
 * - Can return up to a 32 character alphanumeric hash A-Z a-z 0-9
21
 */
22
class Token_Hash
23
{
24
	/**
25
	 * Available characters for private salt
26
	 * @var string
27
	 */
28
	private $itoa64;
29
30
	/**
31
	 * Random digits for initial seeding
32
	 * @var string
33
	 */
34
	private $random_state = '';
35
36
	/**
37
	 * Random salt to feed crypt
38
	 * @var string
39
	 */
40
	private $_salt = '';
41
42
	/**
43
	 * Basic constructor, sets characters and random state
44
	 */
45 10
	public function __construct()
46
	{
47
		// Valid salt characters for crypt
48 10
		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
49
50
		// Set a random state to initialize
51 10
		$this->_state_seed();
52 10
	}
53
54
	/**
55
	 * Set a random value for our initial state
56
	 */
57 10
	private function _state_seed()
58
	{
59
		// PHP >= 7
60 10
		if (is_callable('random_bytes'))
61 10
		{
62
			$this->random_state = bin2hex(random_bytes(8));
63
		}
64
		// If openssl is installed, let it create a character code
65 10
		elseif (function_exists('openssl_random_pseudo_bytes') && (substr(PHP_OS, 0, 3) !== 'WIN' || version_compare(PHP_VERSION, '5.3.4', '>=')))
66
		{
67 10
			$this->random_state = bin2hex(openssl_random_pseudo_bytes(8));
68 10
		}
69
		else
70
		{
71
			// Just use uniqid as a seed
72
			$this->random_state = uniqid('', true);
73
74
			// Use the trailing 16 characters
75
			$this->random_state = substr(str_replace(array(' ', '.'), '', $this->random_state), -16);
76
		}
77 10
	}
78
79
	/**
80
	 * Generates a random hash
81
	 *
82
	 * What it does:
83
	 *  - Uses crypt to generate a hash value
84
	 *  - Returns other than salt on failure
85
	 *  - Returns a A-Z a-z 0-9 string of length characters on success
86
	 *  - If supplying a salt it must be min 16 characters.  It will be wrapped to indicate
87
	 * its a sha512 salt and cleaned to only contain word characters
88
	 *
89
	 * @param int $length the number of characters to return
90
	 * @param string $salt use a custom salt, leave empty to let the system generate a secure one
91
	 *
92
	 * @return string the random hash
93
	 */
94 9
	public function generate_hash($length = 10, $salt = '')
95
	{
96
		// Generate a random salt
97 9
		$this->_salt = $salt;
98 9
		$this->_gen_salt();
99
100
		// A random character password to hash
101 9
		$this->_state_seed();
102 9
		$password = bin2hex($this->get_random_bytes($length));
103
104
		// Hash away
105 9
		$hash = crypt($password, $this->_salt);
106
107
		// For our purposes lets stay with alphanumeric values only
108 9
		$hash = preg_replace('~\W~', '', $hash);
109
110 9
		return substr($hash, mt_rand(0, 45), $length);
111
	}
112
113
	/**
114
	 * Generates a salt that tells crypt to use sha512
115
	 *
116
	 * - Wraps a random or supplied salt with $6$ ... $
117
	 * - If supplied a salt, validates it is good to use
118
	 */
119 9
	private function _gen_salt()
120
	{
121
		// Not supplied one, then generate a random one, this is preferred
122 9
		if (empty($this->_salt) || strlen($this->_salt) < 16)
123 9
		{
124 9
			$this->_salt = '$6$' . $this->_private_salt($this->get_random_bytes(16)) . '$';
125 9
		}
126
		// Supplied a salt, make sure its valid
127
		else
128
		{
129
			// Prep it for crypt / etc
130
			$this->_salt = substr(preg_replace('~\W~', '', $this->_salt), 0, 16);
131
			$this->_salt = str_pad($this->_salt, 16, $this->_private_salt($this->get_random_bytes(16)), STR_PAD_RIGHT);
132
			$this->_salt = '$6$' . $this->_salt . '$';
133
		}
134 9
	}
135
136
	/**
137
	 * Generates a random salt with a character set that is suitable for crypt
138
	 *
139
	 * @param string $input a binary string as supplied from get_random_bytes
140
	 *
141
	 * @return string
142
	 */
143 9
	private function _private_salt($input)
144
	{
145 9
		$i = 0;
146 9
		$output = '';
147
148
		do
149
		{
150 9
			$c1 = ord($input[$i++]);
151 9
			$output .= $this->itoa64[$c1 >> 2];
152 9
			$c1 = ($c1 & 0x03) << 4;
153
154
			// Finished?
155 9
			if ($i >= 16)
156 9
			{
157 9
				$output .= $this->itoa64[$c1];
158 9
				break;
159
			}
160
161 9
			$c2 = ord($input[$i++]);
162 9
			$c1 |= $c2 >> 4;
163 9
			$output .= $this->itoa64[$c1];
164 9
			$c1 = ($c2 & 0x0f) << 2;
165
166 9
			$c2 = ord($input[$i++]);
167 9
			$c1 |= $c2 >> 6;
168 9
			$output .= $this->itoa64[$c1];
169 9
			$output .= $this->itoa64[$c2 & 0x3f];
170 9
		} while (1);
171
172 9
		return $output;
173
	}
174
175
	/**
176
	 * Generates a random string of binary characters
177
	 *
178
	 * - Can use as is or pass though bintohex or via ord to get characters.
179
	 *
180
	 * @param int $count The number of bytes to produce
181
	 *
182
	 * @return string
183
	 */
184 9
	public function get_random_bytes($count)
185
	{
186 9
		$output = '';
187
188
		// Loop every 16 characters
189 9
		for ($i = 0; $i < $count; $i += 16)
190
		{
191 9
			$this->random_state = hash('sha1', microtime() . $this->random_state);
192 9
			$output .= hash('sha1', $this->random_state, true);
193 9
		}
194
195 9
		return substr($output, 0, $count);
196
	}
197
}
198