Merge "Added a separate error message for mkdir failures"
[lhc/web/wiklou.git] / includes / libs / CryptHKDF.php
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 }