root/fCryptography.php

Revision 883, 21.6 kB (checked in by wbond, 3 weeks ago)

Added the $types and $regex parameters to fCore::startErrorCapture() and the $regex parameter to fCore::stopErrorCapture(). Updated various classes to use fCore::startErrorCapture()/fCore::stopErrorCapture() instead of error_reporting().

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