root/fCryptography.php

Revision 763, 20.6 kB (checked in by wbond, 3 days ago)

Fixed ticket #399 - changed fCryptography::seedRandom() to pass a directory instead of a filename to disk_free_space()

LineHide Line Numbers
1 <?php
2 /**
3  * Provides cryptography functionality, including hashing, symmetric-key encryption and public-key encryption
4  *
5  * @copyright  Copyright (c) 2007-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fCryptography
11  *
12  * @version    1.0.0b8
13  * @changes    1.0.0b8  Fixed ::seedRandom() to pass a directory instead of a file to [http://php.net/disk_free_space `disk_free_space()`] [wb, 2010-03-09]
14  * @changes    1.0.0b7  SECURITY FIX: fixed issue with ::random() and ::randomString() not producing random output on OSX, made ::seedRandom() more robust [wb, 2009-10-06]
15  * @changes    1.0.0b6  Changed ::symmetricKeyEncrypt() to throw an fValidationException when the $secret_key is less than 8 characters [wb, 2009-09-30]
16  * @changes    1.0.0b5  Fixed a bug where some windows machines would throw an exception when generating random strings or numbers [wb, 2009-06-09]
17  * @changes    1.0.0b4  Updated for new fCore API [wb, 2009-02-16]
18  * @changes    1.0.0b3  Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
19  * @changes    1.0.0b2  Backwards compatibility break - changed ::symmetricKeyEncrypt() to not encrypt the IV since we are using HMAC on it [wb, 2009-01-26]
20  * @changes    1.0.0b   The initial implementation [wb, 2007-11-27]
21  */
22 class fCryptography
23 {
24     // The following constants allow for nice looking callbacks to static methods
25     const checkPasswordHash   = 'fCryptography::checkPasswordHash';
26     const hashPassword        = 'fCryptography::hashPassword';
27     const publicKeyDecrypt    = 'fCryptography::publicKeyDecrypt';
28     const publicKeyEncrypt    = 'fCryptography::publicKeyEncrypt';
29     const publicKeySign       = 'fCryptography::publicKeySign';
30     const publicKeyVerify     = 'fCryptography::publicKeyVerify';
31     const random              = 'fCryptography::random';
32     const randomString        = 'fCryptography::randomString';
33     const symmetricKeyDecrypt = 'fCryptography::symmetricKeyDecrypt';
34     const symmetricKeyEncrypt = 'fCryptography::symmetricKeyEncrypt';
35    
36    
37     /**
38     * Checks a password against a hash created with ::hashPassword()
39     *
40     * @param  string $password  The password to check
41     * @param  string $hash      The hash to check against
42     * @return boolean  If the password matches the hash
43     */
44     static public function checkPasswordHash($password, $hash)
45     {
46         $salt = substr($hash, 29, 10);
47        
48         if (self::hashWithSalt($password, $salt) == $hash) {
49             return TRUE;
50         }
51        
52         return FALSE;
53     }
54    
55    
56     /**
57     * Create a private key resource based on a filename and password
58     *
59     * @throws fValidationException  When the private key is invalid
60     *
61     * @param  string $private_key_file  The path to a PEM-encoded private key
62     * @param  string $password          The password for the private key
63     * @return resource  The private key resource
64     */
65     static private function createPrivateKeyResource($private_key_file, $password)
66     {
67         if (!file_exists($private_key_file)) {
68             throw new fProgrammerException(
69                 'The path to the PEM-encoded private key specified, %s, is not valid',
70                 $private_key_file
71             );
72         }
73         if (!is_readable($private_key_file)) {
74             throw new fEnvironmentException(
75                 'The PEM-encoded private key specified, %s, is not readable',
76                 $private_key_file
77             );
78         }
79        
80         $private_key          = file_get_contents($private_key_file);
81         $private_key_resource = openssl_pkey_get_private($private_key, $password);
82        
83         if ($private_key_resource === FALSE) {
84             throw new fValidationException(
85                 'The private key file specified, %s, does not appear to be a valid private key or the password provided is incorrect',
86                 $private_key_file
87             );
88         }
89        
90         return $private_key_resource;
91     }
92    
93    
94     /**
95     * Create a public key resource based on a filename
96     *
97     * @param  string $public_key_file  The path to an X.509 public key certificate
98     * @return resource  The public key resource
99     */
100     static private function createPublicKeyResource($public_key_file)
101     {
102         if (!file_exists($public_key_file)) {
103             throw new fProgrammerException(
104                 'The path to the X.509 certificate specified, %s, is not valid',
105                 $public_key_file
106             );
107         }
108         if (!is_readable($public_key_file)) {
109             throw new fEnvironmentException(
110                 'The X.509 certificate specified, %s, can not be read',
111                 $public_key_file
112             );
113         }
114        
115         $public_key = file_get_contents($public_key_file);
116         $public_key_resource = openssl_pkey_get_public($public_key);
117        
118         if ($public_key_resource === FALSE) {
119             throw new fProgrammerException(
120                 'The public key certificate specified, %s, does not appear to be a valid certificate',
121                 $public_key_file
122             );
123         }
124        
125         return $public_key_resource;
126     }
127    
128    
129     /**
130     * Hashes a password using a loop of sha1 hashes and a salt, making rainbow table attacks infeasible
131     *
132     * @param  string $password  The password to hash
133     * @return string  An 80 character string of the Flourish fingerprint, salt and hashed password
134     */
135     static public function hashPassword($password)
136     {
137         $salt = self::randomString(10);
138        
139         return self::hashWithSalt($password, $salt);
140     }
141    
142    
143     /**
144     * Performs a large iteration of hashing a string with a salt
145     *
146     * @param  string $source  The string to hash
147     * @param  string $salt    The salt for the hash
148     * @return string  An 80 character string of the Flourish fingerprint, salt and hashed password
149     */
150     static private function hashWithSalt($source, $salt)
151     {
152         $sha1 = sha1($salt . $source);
153         for ($i = 0; $i < 1000; $i++) {
154             $sha1 = sha1($sha1 . (($i % 2 == 0) ? $source : $salt));
155         }
156        
157         return 'fCryptography::password_hash#' . $salt . '#' . $sha1;
158     }
159        
160    
161     /**
162     * Decrypts ciphertext encrypted using public-key encryption via ::publicKeyEncrypt()
163     *
164     * A public key (X.509 certificate) is required for encryption and a
165     * private key (PEM) is required for decryption.
166     *
167     * @throws fValidationException  When the ciphertext appears to be corrupted
168     *
169     * @param  string $ciphertext        The content to be decrypted
170     * @param  string $private_key_file  The path to a PEM-encoded private key
171     * @param  string $password          The password for the private key
172     * @return string  The decrypted plaintext
173     */
174     static public function publicKeyDecrypt($ciphertext, $private_key_file, $password)
175     {
176         self::verifyPublicKeyEnvironment();
177        
178         $private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
179        
180         $elements = explode('#', $ciphertext);
181        
182         // We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
183         if (sizeof($elements) != 4 || $elements[0] != 'fCryptography::public') {
184             throw new fProgrammerException(
185                 'The ciphertext provided does not appear to have been encrypted using %s',
186                 __CLASS__ . '::publicKeyEncrypt()'
187             );
188         }
189        
190         $encrypted_key = base64_decode($elements[1]);
191         $ciphertext    = base64_decode($elements[2]);
192         $provided_hmac = $elements[3];
193        
194         $plaintext = '';
195         $result = openssl_open($ciphertext, $plaintext, $encrypted_key, $private_key_resource);
196         openssl_free_key($private_key_resource);
197        
198         if ($result === FALSE) {
199             throw new fEnvironmentException(
200                 'There was an unknown error decrypting the ciphertext provided'
201             );
202         }
203        
204         $hmac = hash_hmac('sha1', $encrypted_key . $ciphertext, $plaintext);
205        
206         // By verifying the HMAC we ensure the integrity of the data
207         if ($hmac != $provided_hmac) {
208             throw new fValidationException(
209                 'The ciphertext provided appears to have been tampered with or corrupted'
210             );
211         }
212        
213         return $plaintext;
214     }
215    
216    
217     /**
218     * Encrypts the passed data using public key encryption via OpenSSL
219     *
220     * A public key (X.509 certificate) is required for encryption and a
221     * private key (PEM) is required for decryption.
222     *
223     * @param  string $plaintext        The content to be encrypted
224     * @param  string $public_key_file  The path to an X.509 public key certificate
225     * @return string  A base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::publicKeyDecrypt()
226     */
227     static public function publicKeyEncrypt($plaintext, $public_key_file)
228     {
229         self::verifyPublicKeyEnvironment();
230        
231         $public_key_resource = self::createPublicKeyResource($public_key_file);
232        
233         $ciphertext     = '';
234         $encrypted_keys = array();
235         $result = openssl_seal($plaintext, $ciphertext, $encrypted_keys, array($public_key_resource));
236         openssl_free_key($public_key_resource);
237        
238         if ($result === FALSE) {
239             throw new fEnvironmentException(
240                 'There was an unknown error encrypting the plaintext provided'
241             );
242         }
243        
244         $hmac = hash_hmac('sha1', $encrypted_keys[0] . $ciphertext, $plaintext);
245        
246         return 'fCryptography::public#' . base64_encode($encrypted_keys[0]) . '#' . base64_encode($ciphertext) . '#' . $hmac;
247     }
248    
249    
250     /**
251     * Creates a signature for plaintext to allow verification of the creator
252     *
253     * A private key (PEM) is required for signing and a public key
254     * (X.509 certificate) is required for verification.
255     *
256     * @throws fValidationException  When the private key is invalid
257     *
258     * @param  string $plaintext         The content to be signed
259     * @param  string $private_key_file  The path to a PEM-encoded private key
260     * @param  string $password          The password for the private key
261     * @return string  The base64-encoded signature suitable for verification using ::publicKeyVerify()
262     */
263     static public function publicKeySign($plaintext, $private_key_file, $password)
264     {
265         self::verifyPublicKeyEnvironment();
266        
267         $private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
268        
269         $result = openssl_sign($plaintext, $signature, $private_key_resource);
270         openssl_free_key($private_key_resource);
271        
272         if (!$result) {
273             throw new fEnvironmentException(
274                 'There was an unknown error signing the plaintext'
275             );
276         }
277        
278         return base64_encode($signature);
279     }
280    
281    
282     /**
283     * Checks a signature for plaintext to verify the creator - works with ::publicKeySign()
284     *
285     * A private key (PEM) is required for signing and a public key
286     * (X.509 certificate) is required for verification.
287     *
288     * @param  string $plaintext         The content to check
289     * @param  string $signature         The base64-encoded signature for the plaintext
290     * @param  string $public_key_file   The path to an X.509 public key certificate
291     * @return boolean  If the public key file is the public key of the user who signed the plaintext
292     */
293     static public function publicKeyVerify($plaintext, $signature, $public_key_file)
294     {
295         self::verifyPublicKeyEnvironment();
296        
297         $public_key_resource = self::createPublicKeyResource($public_key_file);
298        
299         $result = openssl_verify($plaintext, base64_decode($signature), $public_key_resource);
300         openssl_free_key($public_key_resource);
301        
302         if ($result === -1) {
303             throw new fEnvironmentException(
304                 'There was an unknown error verifying the plaintext and signature against the public key specified'
305             );
306         }
307        
308         return ($result === 1) ? TRUE : FALSE;
309     }
310    
311    
312     /**
313     * Generates a random number using [http://php.net/mt_rand mt_rand()] after ensuring a good PRNG seed
314     *
315     * @param  integer $min  The minimum number to return
316     * @param  integer $max  The maximum number to return
317     * @return integer  The psuedo-random number
318     */
319     static public function random($min=NULL, $max=NULL)
320     {
321         self::seedRandom();
322         if ($min !== NULL || $max !== NULL) {
323             return mt_rand($min, $max);
324         }
325         return mt_rand();
326     }
327    
328    
329     /**
330     * Returns a random string of the type and length specified
331     *
332     * @param  integer $length  The length of string to return
333     * @param  string  $type    The type of string to return: `'alphanumeric'`, `'alpha'`, `'numeric'`, or `'hexadecimal'`
334     * @return string  A random string of the type and length specified
335     */
336     static public function randomString($length, $type='alphanumeric')
337     {
338         if ($length < 1) {
339             throw new fProgrammerException(
340                 'The length specified, %1$s, is less than the minimum of %2$s',
341                 $length,
342                 1
343             );
344         }
345        
346         switch ($type) {
347             case 'alphanumeric':
348                 $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
349                 break;
350                
351             case 'alpha':
352                 $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
353                 break;
354                
355             case 'numeric':
356                 $alphabet = '0123456789';
357                 break;
358                
359             case 'hexadecimal':
360                 $alphabet = 'abcdef0123456789';
361                 break;
362                
363             default:
364                 throw new fProgrammerException(
365                     'The type specified, %1$s, is invalid. Must be one of: %2$s.',
366                     $type
367                 );
368         }
369        
370         $alphabet_length = strlen($alphabet);
371         $output = '';
372        
373         for ($i = 0; $i < $length; $i++) {
374             $output .= $alphabet[self::random(0, $alphabet_length-1)];
375         }
376        
377         return $output;
378     }
379    
380    
381     /**
382     * Makes sure that the PRNG has been seeded with a fairly secure value
383     *
384     * @return void
385     */
386     static private function seedRandom()
387     {
388         static $seeded = FALSE;
389        
390         if ($seeded) {
391             return;
392         }
393        
394         $old_level = error_reporting(error_reporting() & ~E_WARNING);
395        
396         $bytes = NULL;
397        
398         // On linux/unix/solaris we should be able to use /dev/urandom
399         if (!fCore::checkOS('windows') && $handle = fopen('/dev/urandom', 'rb')) {
400             $bytes = fread($handle, 4);
401             fclose($handle);
402                
403         // On windows we should be able to use the Cryptographic Application Programming Interface COM object
404         } elseif (fCore::checkOS('windows') && class_exists('COM', FALSE)) {
405             try {
406                 // This COM object no longer seems to work on PHP 5.2.9+, no response on the bug report yet
407                 $capi  = new COM('CAPICOM.Utilities.1');
408                 $bytes = base64_decode($capi->getrandom(4, 0));
409                 unset($capi);
410             } catch (Exception $e) { }
411         }
412        
413         // If we could not use the OS random number generators we get some of the most unique info we can       
414         if (!$bytes) {
415             $string = microtime(TRUE) . uniqid('', TRUE) . join('', stat(__FILE__)) . disk_free_space(dirname(__FILE__));
416             $bytes  = substr(pack('H*', md5($string)), 0, 4);
417         }
418        
419         error_reporting($old_level);
420        
421         $seed = (int) (base_convert(bin2hex($bytes), 16, 10) - 2147483647);
422        
423         mt_srand($seed);
424        
425         $seeded = TRUE;
426     }
427    
428    
429     /**
430     * Decrypts ciphertext encrypted using symmetric-key encryption via ::symmetricKeyEncrypt()
431     *
432     * Since this is symmetric-key cryptography, the same key is used for
433     * encryption and decryption.
434     *
435     * @throws fValidationException  When the ciphertext appears to be corrupted
436     *
437     * @param  string $ciphertext  The content to be decrypted
438     * @param  string $secret_key  The secret key to use for decryption
439     * @return string  The decrypted plaintext
440     */
441     static public function symmetricKeyDecrypt($ciphertext, $secret_key)
442     {
443         self::verifySymmetricKeyEnvironment();
444        
445         $elements = explode('#', $ciphertext);
446        
447         // We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
448         if (sizeof($elements) != 4 || $elements[0] != 'fCryptography::symmetric') {
449             throw new fProgrammerException(
450                 'The ciphertext provided does not appear to have been encrypted using %s',
451                 __CLASS__ . '::symmetricKeyEncrypt()'
452             );
453         }
454        
455         $iv            = base64_decode($elements[1]);
456         $ciphertext    = base64_decode($elements[2]);
457         $provided_hmac = $elements[3];
458        
459         $hmac = hash_hmac('sha1', $iv . '#' . $ciphertext, $secret_key);
460        
461         // By verifying the HMAC we ensure the integrity of the data
462         if ($hmac != $provided_hmac) {
463             throw new fValidationException(
464                 'The ciphertext provided appears to have been tampered with or corrupted'
465             );
466         }
467        
468         // Set up the main encryption, we are gonna use AES-256 (also know as rijndael-256) in cipher feedback mode
469         $module   = mcrypt_module_open('rijndael-192', '', 'cfb', '');
470         $key      = substr(sha1($secret_key), 0, mcrypt_enc_get_key_size($module));
471         mcrypt_generic_init($module, $key, $iv);
472        
473         $old_level = error_reporting(error_reporting() & ~E_WARNING);
474         $plaintext = mdecrypt_generic($module, $ciphertext);
475         error_reporting($old_level);
476        
477         mcrypt_generic_deinit($module);
478         mcrypt_module_close($module);
479        
480         return $plaintext;
481     }
482    
483    
484     /**
485     * Encrypts the passed data using symmetric-key encryption
486     *
487     * Since this is symmetric-key cryptography, the same key is used for
488     * encryption and decryption.
489     *
490     * @throws fValidationException  When the $secret_key is less than 8 characters long
491    
492     * @param  string $plaintext   The content to be encrypted
493     * @param  string $secret_key  The secret key to use for encryption - must be at least 8 characters
494     * @return string  An encrypted and base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::symmetricKeyDecrypt()
495     */
496     static public function symmetricKeyEncrypt($plaintext, $secret_key)
497     {
498         if (strlen($secret_key) < 8) {
499             throw new fValidationException(
500                 'The secret key specified does not meet the minimum requirement of being at least %s characters long',
501                 8
502             );
503         }
504        
505         self::verifySymmetricKeyEnvironment();
506        
507         // Set up the main encryption, we are gonna use AES-192 (also know as rijndael-192)
508         // in cipher feedback mode. Cipher feedback mode is chosen because no extra padding
509         // is added, ensuring we always get the exact same plaintext out of the decrypt method
510         $module   = mcrypt_module_open('rijndael-192', '', 'cfb', '');
511         $key      = substr(sha1($secret_key), 0, mcrypt_enc_get_key_size($module));
512         srand();
513         $iv       = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
514        
515         // Finish the main encryption
516         mcrypt_generic_init($module, $key, $iv);
517        
518         $old_level = error_reporting(error_reporting() & ~E_WARNING);
519         $ciphertext = mcrypt_generic($module, $plaintext);
520         error_reporting($old_level);
521        
522         // Clean up the main encryption
523         mcrypt_generic_deinit($module);
524         mcrypt_module_close($module);
525        
526         // Here we are generating the HMAC for the encrypted data to ensure data integrity
527         $hmac = hash_hmac('sha1', $iv . '#' . $ciphertext, $secret_key);
528        
529         // All of the data is then encoded using base64 to prevent issues with character sets
530         $encoded_iv         = base64_encode($iv);
531         $encoded_ciphertext = base64_encode($ciphertext);
532        
533         // Indicate in the resulting encrypted data what the encryption tool was
534         return 'fCryptography::symmetric#' . $encoded_iv . '#' . $encoded_ciphertext . '#' . $hmac;
535     }
536    
537    
538     /**
539     * Makes sure the required PHP extensions and library versions are all correct
540     *
541     * @return void
542     */
543     static private function verifyPublicKeyEnvironment()
544     {
545         if (!extension_loaded('openssl')) {
546             throw new fEnvironmentException(
547                 'The PHP %s extension is required, however is does not appear to be loaded',
548                 'openssl'
549             );
550         }
551     }
552    
553    
554     /**
555     * Makes sure the required PHP extensions and library versions are all correct
556     *
557     * @return void
558     */
559     static private function verifySymmetricKeyEnvironment()
560     {
561         if (!extension_loaded('mcrypt')) {
562             throw new fEnvironmentException(
563                 'The PHP %s extension is required, however is does not appear to be loaded',
564                 'mcrypt'
565             );
566         }
567         if (!extension_loaded('hash')) {
568             throw new fEnvironmentException(
569                 'The PHP %s extension is required, however is does not appear to be loaded',
570                 'hash'
571             );
572         }
573         if (!function_exists('mcrypt_module_open')) {
574             throw new fEnvironmentException(
575                 'The cipher used, %1$s (also known as %2$s), requires libmcrypt version 2.4.x or newer. The version installed does not appear to meet this requirement.',
576                 'AES-192',
577                 'rijndael-192'
578             );
579         }
580         if (!in_array('rijndael-192', mcrypt_list_algorithms())) {
581             throw new fEnvironmentException(
582                 'The cipher used, %1$s (also known as %2$s), does not appear to be supported by the installed version of libmcrypt',
583                 'AES-192',
584                 'rijndael-192'
585             );
586         }
587     }
588    
589    
590     /**
591     * Forces use as a static class
592     *
593     * @return fSecurity
594     */
595     private function __construct() { }
596 }
597  
598  
599  
600 /**
601  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>
602  *
603  * Permission is hereby granted, free of charge, to any person obtaining a copy
604  * of this software and associated documentation files (the "Software"), to deal
605  * in the Software without restriction, including without limitation the rights
606  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
607  * copies of the Software, and to permit persons to whom the Software is
608  * furnished to do so, subject to the following conditions:
609  *
610  * The above copyright notice and this permission notice shall be included in
611  * all copies or substantial portions of the Software.
612  *
613  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
614  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
615  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
616  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
617  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
618  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
619  * THE SOFTWARE.
620  */