CryptHKDF.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. class CryptHKDF {
  33. /**
  34. * @var BagOStuff The persistent cache
  35. */
  36. protected $cache = null;
  37. /**
  38. * @var string Cache key we'll use for our salt
  39. */
  40. protected $cacheKey = null;
  41. /**
  42. * @var string The hash algorithm being used
  43. */
  44. protected $algorithm = null;
  45. /**
  46. * @var string binary string, the salt for the HKDF
  47. * @see getSaltUsingCache
  48. */
  49. protected $salt = '';
  50. /**
  51. * @var string The pseudorandom key
  52. */
  53. private $prk = '';
  54. /**
  55. * The secret key material. This must be kept secret to preserve
  56. * the security properties of this RNG.
  57. *
  58. * @var string
  59. */
  60. private $skm;
  61. /**
  62. * @var string The last block (K(i)) of the most recent expanded key
  63. */
  64. protected $lastK;
  65. /**
  66. * a "context information" string CTXinfo (which may be null)
  67. * See http://eprint.iacr.org/2010/264.pdf Section 4.1
  68. *
  69. * @var array
  70. */
  71. protected $context = [];
  72. /**
  73. * Round count is computed based on the hash'es output length,
  74. * which neither php nor openssl seem to provide easily.
  75. *
  76. * @var int[]
  77. */
  78. public static $hashLength = [
  79. 'md5' => 16,
  80. 'sha1' => 20,
  81. 'sha224' => 28,
  82. 'sha256' => 32,
  83. 'sha384' => 48,
  84. 'sha512' => 64,
  85. 'ripemd128' => 16,
  86. 'ripemd160' => 20,
  87. 'ripemd256' => 32,
  88. 'ripemd320' => 40,
  89. 'whirlpool' => 64,
  90. ];
  91. /**
  92. * @param string $secretKeyMaterial
  93. * @param string $algorithm Name of hashing algorithm
  94. * @param BagOStuff $cache
  95. * @param string|array $context Context to mix into HKDF context
  96. * @throws InvalidArgumentException if secret key material is too short
  97. */
  98. public function __construct( $secretKeyMaterial, $algorithm, BagOStuff $cache, $context ) {
  99. if ( strlen( $secretKeyMaterial ) < 16 ) {
  100. throw new InvalidArgumentException( "secret was too short." );
  101. }
  102. $this->skm = $secretKeyMaterial;
  103. $this->algorithm = $algorithm;
  104. $this->cache = $cache;
  105. $this->context = is_array( $context ) ? $context : [ $context ];
  106. // To prevent every call from hitting the same memcache server, pick
  107. // from a set of keys to use. mt_rand is only use to pick a random
  108. // server, and does not affect the security of the process.
  109. $this->cacheKey = $cache->makeKey( 'HKDF', mt_rand( 0, 16 ) );
  110. }
  111. /**
  112. * Save the last block generated, so the next user will compute a different PRK
  113. * from the same SKM. This should keep things unpredictable even if an attacker
  114. * is able to influence CTXinfo.
  115. */
  116. function __destruct() {
  117. if ( $this->lastK ) {
  118. $this->cache->set( $this->cacheKey, $this->lastK );
  119. }
  120. }
  121. /**
  122. * MW specific salt, cached from last run
  123. * @return string Binary string
  124. */
  125. protected function getSaltUsingCache() {
  126. if ( $this->salt == '' ) {
  127. $lastSalt = $this->cache->get( $this->cacheKey );
  128. if ( $lastSalt === false ) {
  129. // If we don't have a previous value to use as our salt, we use
  130. // 16 bytes from random_bytes(), which will use a small amount of
  131. // entropy from our pool. Note, "XTR may be deterministic or keyed
  132. // via an optional “salt value” (i.e., a non-secret random
  133. // value)..." - http://eprint.iacr.org/2010/264.pdf. However, we
  134. // use a strongly random value since we can.
  135. $lastSalt = random_bytes( 16 );
  136. }
  137. // Get a binary string that is hashLen long
  138. $this->salt = hash( $this->algorithm, $lastSalt, true );
  139. }
  140. return $this->salt;
  141. }
  142. /**
  143. * Produce $bytes of secure random data. As a side-effect,
  144. * $this->lastK is set to the last hashLen block of key material.
  145. *
  146. * @param int $bytes Number of bytes of data
  147. * @param string $context Context to mix into CTXinfo
  148. * @return string Binary string of length $bytes
  149. */
  150. public function generate( $bytes, $context = '' ) {
  151. if ( $this->prk === '' ) {
  152. $salt = $this->getSaltUsingCache();
  153. $this->prk = self::HKDFExtract(
  154. $this->algorithm,
  155. $salt,
  156. $this->skm
  157. );
  158. }
  159. $CTXinfo = implode( ':', array_merge( $this->context, [ $context ] ) );
  160. return self::HKDFExpand(
  161. $this->algorithm,
  162. $this->prk,
  163. $CTXinfo,
  164. $bytes,
  165. $this->lastK
  166. );
  167. }
  168. /**
  169. * RFC5869 defines HKDF in 2 steps, extraction and expansion.
  170. * From http://eprint.iacr.org/2010/264.pdf:
  171. *
  172. * The scheme HKDF is specifed as:
  173. * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t)
  174. * where the values K(i) are defined as follows:
  175. * PRK = HMAC(XTS, SKM)
  176. * K(1) = HMAC(PRK, CTXinfo || 0);
  177. * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t;
  178. * where t = [L/k] and the value K(t) is truncated to its first d = L mod k bits;
  179. * the counter i is non-wrapping and of a given fixed size, e.g., a single byte.
  180. * Note that the length of the HMAC output is the same as its key length and therefore
  181. * the scheme is well defined.
  182. *
  183. * XTS is the "extractor salt"
  184. * SKM is the "secret keying material"
  185. *
  186. * N.B. http://eprint.iacr.org/2010/264.pdf seems to differ from RFC 5869 in that the test
  187. * vectors from RFC 5869 only work if K(0) = '' and K(1) = HMAC(PRK, K(0) || CTXinfo || 1)
  188. *
  189. * @param string $hash The hashing function to use (e.g., sha256)
  190. * @param string $ikm The input keying material
  191. * @param string $salt The salt to add to the ikm, to get the prk
  192. * @param string $info Optional context (change the output without affecting
  193. * the randomness properties of the output)
  194. * @param int $L Number of bytes to return
  195. * @return string Cryptographically secure pseudorandom binary string
  196. */
  197. public static function HKDF( $hash, $ikm, $salt, $info, $L ) {
  198. $prk = self::HKDFExtract( $hash, $salt, $ikm );
  199. $okm = self::HKDFExpand( $hash, $prk, $info, $L );
  200. return $okm;
  201. }
  202. /**
  203. * Extract the PRK, PRK = HMAC(XTS, SKM)
  204. * Note that the hmac is keyed with XTS (the salt),
  205. * and the SKM (source key material) is the "data".
  206. *
  207. * @param string $hash The hashing function to use (e.g., sha256)
  208. * @param string $salt The salt to add to the ikm, to get the prk
  209. * @param string $ikm The input keying material
  210. * @return string Binary string (pseudorandm key) used as input to HKDFExpand
  211. */
  212. private static function HKDFExtract( $hash, $salt, $ikm ) {
  213. return hash_hmac( $hash, $ikm, $salt, true );
  214. }
  215. /**
  216. * Expand the key with the given context
  217. *
  218. * @param string $hash Hashing Algorithm
  219. * @param string $prk A pseudorandom key of at least HashLen octets
  220. * (usually, the output from the extract step)
  221. * @param string $info Optional context and application specific information
  222. * (can be a zero-length string)
  223. * @param int $bytes Length of output keying material in bytes
  224. * (<= 255*HashLen)
  225. * @param string &$lastK Set by this function to the last block of the expansion.
  226. * In MediaWiki, this is used to seed future Extractions.
  227. * @return string Cryptographically secure random string $bytes long
  228. * @throws InvalidArgumentException
  229. */
  230. private static function HKDFExpand( $hash, $prk, $info, $bytes, &$lastK = '' ) {
  231. $hashLen = self::$hashLength[$hash];
  232. $rounds = ceil( $bytes / $hashLen );
  233. $output = '';
  234. if ( $bytes > 255 * $hashLen ) {
  235. throw new InvalidArgumentException( 'Too many bytes requested from HDKFExpand' );
  236. }
  237. // K(1) = HMAC(PRK, CTXinfo || 1);
  238. // K(i) = HMAC(PRK, K(i-1) || CTXinfo || i); 1 < i <= t;
  239. for ( $counter = 1; $counter <= $rounds; ++$counter ) {
  240. $lastK = hash_hmac(
  241. $hash,
  242. $lastK . $info . chr( $counter ),
  243. $prk,
  244. true
  245. );
  246. $output .= $lastK;
  247. }
  248. return substr( $output, 0, $bytes );
  249. }
  250. }