]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/libs/CryptHKDF.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.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 }