Commit a new cryptographic random generator class for use in MediaWiki.
[lhc/web/wiklou.git] / includes / CryptRand.php
1 <?php
2 /**
3 * A static cryptographic random generator class used for generating secret keys
4 *
5 * This is based in part on Drupal code as well as what we used in our own code
6 * prior to introduction of this class.
7 *
8 * @file
9 */
10
11 final class MWCryptRand {
12
13 /**
14 * Initialize an initial random state based off of whatever we can find
15 */
16 private static function initialRandomState() {
17 // $_SERVER contains a variety of unstable user and system specific information
18 // It'll vary a little with each page, and vary even more with separate users
19 // It'll also vary slightly across different machines
20 $state = serialize( $_SERVER );
21
22 // To try and vary the system information of the state a bit more
23 // by including the system's hostname into the state
24 $state .= wfHostname();
25
26 // Try and make this a little more unstable by including the varying process
27 // id of the php process we are running inside of if we are able to access it
28 if ( function_exists( 'getmypid' ) ) {
29 $state .= getmypid();
30 }
31
32 // If available try to increase the instability of the data by throwing in
33 // the precise amount of memory that we happen to be using at the moment.
34 if ( function_exists( 'memory_get_usage' ) ) {
35 $state .= memory_get_usage( true );
36 }
37
38 // It's mostly worthless but throw the wiki's id into the data for a little more variance
39 $state .= wfWikiID();
40
41 // If we have a secret key or proxy key set then throw it into the state as well
42 global $wgSecretKey, $wgProxyKey;
43 if ( $wgSecretKey ) {
44 $state .= $wgSecretKey;
45 } elseif ( $wgProxyKey ) {
46 $state .= $wgProxyKey;
47 }
48
49 return $state;
50 }
51
52 /**
53 * Return a rolling random state initially build using data from unstable sources
54 * @return A new weak random state
55 */
56 public static function randomState() {
57 static $state = null;
58 if ( is_null( $state ) ) {
59 // Initialize the state with whatever unstable data we can find
60 // It's important that this data is hashed right afterwards to prevent
61 // it from being leaked into the output stream
62 $state = self::initialRandomState();
63 }
64 // Generate a new random state based on the initial random state or previous
65 // random state by combining it with both the current time and a random value
66 // Simple append/prepend based methods of combining data and a salt have
67 // weaknesses in them, take advantage of the availability of hmac to abuse
68 // it's method of combining data and a key into a hash which is free of
69 // the typical weakness of simple concatenation
70 // Note that in hmac large keys are reduced in size and the key is then
71 // xor-ed to create two separate keys. For this reason we use the smaller
72 // time+rand as the key and the larger state as the data.
73 // We also don't bother passing numbers to mt_rand since you can't make
74 // it generate with any more entropy than it's default max value.
75 $state = self::hmac( $state, microtime() . mt_rand() );
76 return $state;
77 }
78
79 /**
80 * Decide on the best acceptable hash algorithm we have available for hash()
81 * @return String A hash algorithm
82 */
83 private static function hashAlgo() {
84 static $algo = null;
85 if ( !is_null( $algo ) ) {
86 return $algo;
87 }
88
89 $algos = hash_algos();
90 $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' );
91
92 foreach ( $preference as $algorithm ) {
93 if ( in_array( $algorithm, $algos ) ) {
94 $algo = $algorithm; # assign to static
95 return $algo;
96 }
97 }
98
99 // We only reach here if no acceptable hash is found in the list, this should
100 // be a technical impossibility since most of php's hash list is fixed and
101 // some of the ones we list are available as their own native functions
102 // But since we already require at least 5.2 and hash() was default in
103 // 5.1.2 we don't bother falling back to methods like sha1 and md5.
104 throw new MWException( "Could not find an acceptable hashing function in hash_algos()" );
105 }
106
107 /**
108 * Generate an acceptably unstable one-way-hash of some text
109 * making use of the best hash algorithm that we have available.
110 *
111 * @return String A raw hash of the data
112 */
113 private static function hash( $data ) {
114 return hash( self::hashAlgo(), $data, true );
115 }
116
117 /**
118 * Generate an acceptably unstable one-way-hmac of some text
119 * making use of the best hash algorithm that we have available.
120 *
121 * @return String A raw hash of the data
122 */
123 private static function hmac( $data, $key ) {
124 return hash_hmac( self::hashAlgo(), $data, $key, true );
125 }
126
127
128
129 private static $strong = null;
130
131 /**
132 * Return a boolean indicating whether or not the source used for cryptographic
133 * random bytes generation in the previously run generate* call
134 * was cryptographically strong.
135 *
136 * @return bool Returns true if the source was strong, false if not.
137 */
138 public static function wasStrong() {
139 if ( is_null( self::$strong ) ) {
140 throw new MWException( __METHOD__ . ' called before generation of random data' );
141 }
142 return self::$strong;
143 }
144
145 /**
146 * Generate a run of (ideally) cryptographically random data and return
147 * it in raw binary form.
148 * You can use MWCryptRand::wasStrong() if you wish to know if the source used
149 * was cryptographically strong.
150 *
151 * @param $bytes int the number of bytes of random data to generate
152 * @return String Raw binary random data
153 */
154 public static function generate( $bytes ) {
155 $bytes = floor( $bytes );
156 static $buffer = '';
157 if ( is_null( self::$strong ) ) {
158 // Set strength to false initially until we know what source data is coming from
159 self::$strong = true;
160 }
161
162 if ( strlen( $buffer ) < $bytes ) {
163 // /dev/urandom is generally considered the best possible commonly
164 // available random source, and is available on most *nix systems.
165 wfSuppressWarnings();
166 $urandom = fopen( "/dev/urandom", "rb" );
167 wfRestoreWarnings();
168
169 // Attempt to read all our random data from urandom
170 // php's fread always does buffered reads based on the stream's chunk_size
171 // so in reality it will usually read more than the amount of data we're
172 // asked for and it doesn't cost anything extra to store that.
173 // We don't have access to the stream's chunk_size, fread maxes out at 8k
174 // so we'll go along with Drupal's decision to read at least 4k
175 if ( $urandom ) {
176 $buffer .= fread( $urandom, max( 1024 * 4, $bytes ) );
177 fclose( $urandom );
178 if ( strlen( $buffer ) >= $bytes ) {
179 // urandom is always strong, set to true if all our data was generated using it
180 self::$strong = true;
181 }
182 }
183 }
184
185 if ( strlen( $buffer ) < $bytes ) {
186 // If available and we failed to read enough data out of urandom make use
187 // of openssl's random_pesudo_bytes method to attempt to generate randomness.
188 // However don't do this on Windows with PHP < 5.3.4 due to a bug:
189 // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
190 if ( ( $bytes - strlen( $buffer ) > 0 )
191 && function_exists( 'openssl_random_pseudo_bytes' )
192 && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
193 ) {
194 $buffer .= openssl_random_pseudo_bytes( $bytes - strlen( $buffer ), $openssl_strong );
195 if ( strlen( $buffer ) >= $bytes ) {
196 // openssl tells us if the random source was strong, if some of our data was generated
197 // using it use it's say on whether the randomness is strong
198 self::$strong = !!$openssl_strong;
199 }
200 }
201 }
202
203
204 // If we cannot use or generate enough data from /dev/urandom or openssl
205 // use this loop to generate a good set of pesudo random data.
206 // This works by initializing a random state using a pile of unstable data
207 // and continually shoving it through a hash along with a variable salt.
208 // We hash the random state with more salt to avoid the state from leaking
209 // out and being used to predict the /randomness/ that follows.
210 while ( strlen( $buffer ) < $bytes ) {
211 $buffer .= self::hmac( self::randomState(), mt_rand() );
212 // This code is never really cryptographically strong, if we use it
213 // at all, then set strong to false.
214 self::$strong = false;
215 }
216
217 // Once the buffer has been filled up with enough random data to fulfill
218 // the request shift off enough data to handle the request and leave the
219 // unused portion left inside the buffer for the next request for random data
220 $generated = substr( $buffer, 0, $bytes );
221 $buffer = substr( $buffer, $bytes );
222
223 return $generated;
224 }
225
226 /**
227 * Generate a run of (ideally) cryptographically random data and return
228 * it in hexadecimal string format.
229 * You can use MWCryptRand::wasStrong() if you wish to know if the source used
230 * was cryptographically strong.
231 *
232 * @param $chars int the number of hex chars of random data to generate
233 * @return String Hexadecimal random data
234 */
235 public static function generateHex( $chars ) {
236 // hex strings are 2x the length of raw binary so we divide the length in half
237 // odd numbers will result in a .5 that leads the generate() being 1 character
238 // short, so we use ceil() to ensure that we always have enough bytes
239 $bytes = ceil( $chars / 2 );
240 // Generate the data and then convert it to a hex string
241 $hex = bin2hex( self::generate( $bytes ) );
242 // A bit of paranoia here, the caller asked for a specific length of string
243 // here, and it's possible (eg when given an odd number) that we may actually
244 // have at least 1 char more than they asked for. Just in case they made this
245 // call intending to insert it into a database that does truncation we don't
246 // want to give them too much and end up with their database and their live
247 // code having two different values because part of what we gave them is truncated
248 // hence, we strip out any run of characters longer than what we were asked for.
249 return substr( $hex, 0, $chars );
250 }
251
252 }