Completed
Branch master (537795)
by
unknown
33:10
created

CryptHKDF::HKDFExpand()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 3
nop 5
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
/**
3
 * Extract-and-Expand Key Derivation Function (HKDF). A cryptographicly
4
 * secure key expansion function based on RFC 5869.
5
 *
6
 * This relies on the secrecy of $wgSecretKey (by default), or $wgHKDFSecret.
7
 * By default, sha256 is used as the underlying hashing algorithm, but any other
8
 * algorithm can be used. Finding the secret key from the output would require
9
 * an attacker to discover the input key (the PRK) to the hmac that generated
10
 * the output, and discover the particular data, hmac'ed with an evolving key
11
 * (salt), to produce the PRK. Even with md5, no publicly known attacks make
12
 * this currently feasible.
13
 *
14
 * This program is free software; you can redistribute it and/or modify
15
 * it under the terms of the GNU General Public License as published by
16
 * the Free Software Foundation; either version 2 of the License, or
17
 * (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU General Public License along
25
 * with this program; if not, write to the Free Software Foundation, Inc.,
26
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27
 * http://www.gnu.org/copyleft/gpl.html
28
 *
29
 * @author Chris Steipp
30
 * @file
31
 */
32
33
class CryptHKDF {
34
35
	/**
36
	 * @var BagOStuff The persistent cache
37
	 */
38
	protected $cache = null;
39
40
	/**
41
	 * @var string Cache key we'll use for our salt
42
	 */
43
	protected $cacheKey = null;
44
45
	/**
46
	 * @var string The hash algorithm being used
47
	 */
48
	protected $algorithm = null;
49
50
	/**
51
	 * @var string binary string, the salt for the HKDF
52
	 * @see getSaltUsingCache
53
	 */
54
	protected $salt = '';
55
56
	/**
57
	 * @var string The pseudorandom key
58
	 */
59
	private $prk = '';
60
61
	/**
62
	 * The secret key material. This must be kept secret to preserve
63
	 * the security properties of this RNG.
64
	 *
65
	 * @var string
66
	 */
67
	private $skm;
68
69
	/**
70
	 * @var string The last block (K(i)) of the most recent expanded key
71
	 */
72
	protected $lastK;
73
74
	/**
75
	 * a "context information" string CTXinfo (which may be null)
76
	 * See http://eprint.iacr.org/2010/264.pdf Section 4.1
77
	 *
78
	 * @var array
79
	 */
80
	protected $context = [];
81
82
	/**
83
	 * Round count is computed based on the hash'es output length,
84
	 * which neither php nor openssl seem to provide easily.
85
	 *
86
	 * @var int[]
87
	 */
88
	public static $hashLength = [
89
		'md5' => 16,
90
		'sha1' => 20,
91
		'sha224' => 28,
92
		'sha256' => 32,
93
		'sha384' => 48,
94
		'sha512' => 64,
95
		'ripemd128' => 16,
96
		'ripemd160' => 20,
97
		'ripemd256' => 32,
98
		'ripemd320' => 40,
99
		'whirlpool' => 64,
100
	];
101
102
	/**
103
	 * @var CryptRand
104
	 */
105
	private $cryptRand;
106
107
	/**
108
	 * @param string $secretKeyMaterial
109
	 * @param string $algorithm Name of hashing algorithm
110
	 * @param BagOStuff $cache
111
	 * @param string|array $context Context to mix into HKDF context
112
	 * @param CryptRand $cryptRand
113
	 * @throws InvalidArgumentException if secret key material is too short
114
	 */
115
	public function __construct( $secretKeyMaterial, $algorithm, BagOStuff $cache, $context,
116
		CryptRand $cryptRand
117
	) {
118
		if ( strlen( $secretKeyMaterial ) < 16 ) {
119
			throw new InvalidArgumentException( "secret was too short." );
120
		}
121
		$this->skm = $secretKeyMaterial;
122
		$this->algorithm = $algorithm;
123
		$this->cache = $cache;
124
		$this->context = is_array( $context ) ? $context : [ $context ];
125
		$this->cryptRand = $cryptRand;
126
127
		// To prevent every call from hitting the same memcache server, pick
128
		// from a set of keys to use. mt_rand is only use to pick a random
129
		// server, and does not affect the security of the process.
130
		$this->cacheKey = $cache->makeKey( 'HKDF', mt_rand( 0, 16 ) );
131
	}
132
133
	/**
134
	 * Save the last block generated, so the next user will compute a different PRK
135
	 * from the same SKM. This should keep things unpredictable even if an attacker
136
	 * is able to influence CTXinfo.
137
	 */
138
	function __destruct() {
139
		if ( $this->lastK ) {
140
			$this->cache->set( $this->cacheKey, $this->lastK );
141
		}
142
	}
143
144
	/**
145
	 * MW specific salt, cached from last run
146
	 * @return string Binary string
147
	 */
148
	protected function getSaltUsingCache() {
149
		if ( $this->salt == '' ) {
150
			$lastSalt = $this->cache->get( $this->cacheKey );
151
			if ( $lastSalt === false ) {
152
				// If we don't have a previous value to use as our salt, we use
153
				// 16 bytes from CryptRand, which will use a small amount of
154
				// entropy from our pool. Note, "XTR may be deterministic or keyed
155
				// via an optional “salt value”  (i.e., a non-secret random
156
				// value)..." - http://eprint.iacr.org/2010/264.pdf. However, we
157
				// use a strongly random value since we can.
158
				$lastSalt = $this->cryptRand->generate( 16 );
159
			}
160
			// Get a binary string that is hashLen long
161
			$this->salt = hash( $this->algorithm, $lastSalt, true );
162
		}
163
		return $this->salt;
164
	}
165
166
	/**
167
	 * Produce $bytes of secure random data. As a side-effect,
168
	 * $this->lastK is set to the last hashLen block of key material.
169
	 *
170
	 * @param int $bytes Number of bytes of data
171
	 * @param string $context Context to mix into CTXinfo
172
	 * @return string Binary string of length $bytes
173
	 */
174
	public function generate( $bytes, $context = '' ) {
175
		if ( $this->prk === '' ) {
176
			$salt = $this->getSaltUsingCache();
177
			$this->prk = self::HKDFExtract(
178
				$this->algorithm,
179
				$salt,
180
				$this->skm
181
			);
182
		}
183
184
		$CTXinfo = implode( ':', array_merge( $this->context, [ $context ] ) );
185
186
		return self::HKDFExpand(
187
			$this->algorithm,
188
			$this->prk,
189
			$CTXinfo,
190
			$bytes,
191
			$this->lastK
192
		);
193
	}
194
195
	/**
196
	 * RFC5869 defines HKDF in 2 steps, extraction and expansion.
197
	 * From http://eprint.iacr.org/2010/264.pdf:
198
	 *
199
	 * The scheme HKDF is specifed as:
200
	 * 	HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t)
201
	 * where the values K(i) are defined as follows:
202
	 * 	PRK = HMAC(XTS, SKM)
203
	 * 	K(1) = HMAC(PRK, CTXinfo || 0);
204
	 * 	K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t;
205
	 * where t = [L/k] and the value K(t) is truncated to its first d = L mod k bits;
206
	 * the counter i is non-wrapping and of a given fixed size, e.g., a single byte.
207
	 * Note that the length of the HMAC output is the same as its key length and therefore
208
	 * the scheme is well defined.
209
	 *
210
	 * XTS is the "extractor salt"
211
	 * SKM is the "secret keying material"
212
	 *
213
	 * N.B. http://eprint.iacr.org/2010/264.pdf seems to differ from RFC 5869 in that the test
214
	 * vectors from RFC 5869 only work if K(0) = '' and K(1) = HMAC(PRK, K(0) || CTXinfo || 1)
215
	 *
216
	 * @param string $hash The hashing function to use (e.g., sha256)
217
	 * @param string $ikm The input keying material
218
	 * @param string $salt The salt to add to the ikm, to get the prk
219
	 * @param string $info Optional context (change the output without affecting
220
	 *	the randomness properties of the output)
221
	 * @param int $L Number of bytes to return
222
	 * @return string Cryptographically secure pseudorandom binary string
223
	 */
224
	public static function HKDF( $hash, $ikm, $salt, $info, $L ) {
225
		$prk = self::HKDFExtract( $hash, $salt, $ikm );
226
		$okm = self::HKDFExpand( $hash, $prk, $info, $L );
227
		return $okm;
228
	}
229
230
	/**
231
	 * Extract the PRK, PRK = HMAC(XTS, SKM)
232
	 * Note that the hmac is keyed with XTS (the salt),
233
	 * and the SKM (source key material) is the "data".
234
	 *
235
	 * @param string $hash The hashing function to use (e.g., sha256)
236
	 * @param string $salt The salt to add to the ikm, to get the prk
237
	 * @param string $ikm The input keying material
238
	 * @return string Binary string (pseudorandm key) used as input to HKDFExpand
239
	 */
240
	private static function HKDFExtract( $hash, $salt, $ikm ) {
241
		return hash_hmac( $hash, $ikm, $salt, true );
242
	}
243
244
	/**
245
	 * Expand the key with the given context
246
	 *
247
	 * @param string $hash Hashing Algorithm
248
	 * @param string $prk A pseudorandom key of at least HashLen octets
249
	 *    (usually, the output from the extract step)
250
	 * @param string $info Optional context and application specific information
251
	 *    (can be a zero-length string)
252
	 * @param int $bytes Length of output keying material in bytes
253
	 *    (<= 255*HashLen)
254
	 * @param string &$lastK Set by this function to the last block of the expansion.
255
	 *    In MediaWiki, this is used to seed future Extractions.
256
	 * @return string Cryptographically secure random string $bytes long
257
	 * @throws InvalidArgumentException
258
	 */
259
	private static function HKDFExpand( $hash, $prk, $info, $bytes, &$lastK = '' ) {
260
		$hashLen = self::$hashLength[$hash];
261
		$rounds = ceil( $bytes / $hashLen );
262
		$output = '';
263
264
		if ( $bytes > 255 * $hashLen ) {
265
			throw new InvalidArgumentException( 'Too many bytes requested from HDKFExpand' );
266
		}
267
268
		// K(1) = HMAC(PRK, CTXinfo || 1);
269
		// K(i) = HMAC(PRK, K(i-1) || CTXinfo || i); 1 < i <= t;
270
		for ( $counter = 1; $counter <= $rounds; ++$counter ) {
271
			$lastK = hash_hmac(
272
				$hash,
273
				$lastK . $info . chr( $counter ),
274
				$prk,
275
				true
276
			);
277
			$output .= $lastK;
278
		}
279
280
		return substr( $output, 0, $bytes );
281
	}
282
}
283