Crypto class 0.3

A bit of an overhaul to my old cryptography class, I thought I’d post it before it goes into my side forum project. This includes functions for password hashing (Bcrypt and PBKDF2) as well as your garden variety encryption helpers for sessions, cookies and such. Questions, comments, corrections (especially corrections) feel free to comment.

Side note: I’ve taken to using the encode/decode functions here in lieu of standard base64_encode/decode in PHP. It’s only in ECB mode so it’s not secure, but plain base64 was leaving things just a little too easy for Peeping Toms.

As always, this comes with no warranties, liabilities, no guarantees, thar be dragons etc…

I’ll post more of the actual forum code when I get a chance.

<?php

/**
 * Encryption/Decryption, Encoding/Decoding and password related functions.
 *
 * The license below was inherited from the project this originally went into.
 * If you need a different license for some reason, just drop me a line.
 *
 * @author Eksith Rodrigo <reksith at gmail.com>
 * @license http://opensource.org/licenses/ISC ISC License
 * @version 0.3
 */

class Crypto {
	
	
	/**#@+
	 * Instance getter, constructor and destructor
	 */
	
	private static $_instance;
	public static function getInstance() {
		if ( !self::$_instance instanceof self ) {
			self::$_instance = new self();
		}
		
		return self::$_instance;
	}
	
	private function __construct() {}
	
	/**#@-*/
	
	
		
	/**#@+
	 * Password related functions
	 */
	
	
	/**
	 * Original PBKDF2 code courtesy of havoc : https://defuse.ca/php-pbkdf2.htm
	 * Adapted here for customized functionality
	 */
	public function create_pbkdf2( $pass, $algo, $rounds, $salt, $klen = 30 ) {
		if ( empty( $salt ) ) {
			$salt = base64_encode( $this->IV( $klen ) );
		}
		
		$hash = $this->pbkdf2( $algo, $pass, $salt, $rounds, $klen );
		
		return "$algo:$rounds:$salt:$hash"; 
			
	}
	
	public function validate_pbkdf2( $pass, $stored, $fast = false ) {
		$args = explode( ':', $stored );
		if ( count( $args ) < 4 ) { return false; }
		
		$p = $this->pbkdf2( $ars[0], $pass, $args[2], ( int ) $args[1], strlen( $args[3] ) );
		
		// Don't use fast for passwords!
		if ( $fast ) {
			return strcmp( $h, $p );
		}
		
		return $this->slow_cmp( $h, $p );
	}
	
	public function slow_cmp( $a, $b ) {
		$d = strlen( $a ) ^ strlen( $b );
		for( $i = 0; $i < strlen( $a ) && $i < strlen( $b ); $i++ ) {
			$d |= ord( $a[$i] ) ^ ord( $b[$i] );
		}
		
		return 0 === $d;
	}
	
	
	/**
	 * PBKDF2
	 *
	 * @param string $algo Hash alorithm ( one from hash_algos() )
	 * @param string $pass Unhashed original password
	 * @param string $salt Random semi-unique salt
	 * @param int $round Number of rounds to hash
	 * @param int $kl Hashed key length 
	 * 		Note: this will be smaller than the final pass size when used 
			with the above two related functions depending on salt length
	 * @param bool $raw Hash return mode
	 */
	public function pbkdf2( $algo, $pass, $salt, $rounds, $kl, $raw = false ) {
		if ( !in_array( $algo, hash_algos(), true ) ) {
			return null;
		}
		
		if ( $rounds <= 0 || $kl <= 0 ) {
			return null;
		}
		
		$hl = strlen( hash( $algo, '', true ) );
		$bl = ceil( $kl / $hl );
		
		$out = '';
		
		for ( $i = 1; $i <= $bl; $i++ ) {
			$last = $salt . pack( "N", $i );
			$last = $xor = hash_hmac( $algo, $last, $pass, true );
			for ( $j = 1; $j < $rounds; $j++ ) {
				$xor ^= ( $last = hash_hmac( $algo, $last, $pass, true ) );
			}
			$out .= $xor;
		}
		
		if ( $raw ) {
			return substr( $out, 0, $kl );
		}
		
		return bin2hex( substr( $out, 0 , $kl ) );
	}
	
	/**
	 * Hash password with blowfish(14 rounds) + salt
	 * 
	 * @param string $user Username
	 * @param string $pass Raw password
	 * @param string $salt Stored salt after being retreived or freshly generated
	 */
	public function password( $user, $pass, $salt ) {
		// This will reject NULL, 0, FALSE etc... but these are terrible passwords anyway
		if ( empty( $user ) || empty( $pass ) || empty( $salt ) ) {
			return false;
		}
		
		return crypt( $pass, '$2y$14$' . $salt . $user . '$' );
	}
	
	
	/**
	 * Verify password hash against the stored value
	 * 
	 * @param string $user Username
	 * @param string $pass Raw password supplied by the user
	 * @param string $salt Password salt as stored in the database
	 * @param string $storedPass Hashed password to match against
	 */
	public function verifyPassword( $user, $pass, $storedPass, $salt ) {
		if ( empty( $user ) || empty( $pass ) || empty( $salt ) || 
			empty( $storedPass ) ) {
			return false;
		}
		
		$pass = $this->password( $user, $pass, $salt );

		if ( $storedPass === $pass ) {
			return true;
		}
		return false;
	}


	/**
	 * Hashes the passed password and creates a matching salt (also encoded)
	 * This function does NOT create a new password.
	 *
	 * @param string $user Username
	 * @param string $pass User provided password
	 * @param string $salt Generated salt
	 */
	public function pairPasswordSalt( $user, &$pass, &$salt ) {
		if ( empty( $pass ) ) {
			return false;
		}

		$salt = bin2hex( $this->IV( 30, true ) );
		$pass = $this->password( $user, $pass, $salt );

		return true;
	}
	
	/**#@-*/
	
	
	
	/**#@+
	 * Random number/string generators
	 */
	
	/**
	 * Generates a random string/password of a given length
	 * (or a random length between 8 and 12 if no length is given)
	 *
	 * @param int $len Random string length. If empty, will revert to a number between 8 - 12
	 * @return string Alphanumeric string
	 */
	public function randomStr( $len = 0, $pass = false ) {
		if ( empty( $len ) ) {
			$len = $this->_rnd( 8, 12 );
		}

		$r = base64_encode( $this->IV( 30, true ) );
		$r = str_replace( array( '+', '/', '-', '_', '=' ), '', $r );
		
		if ( $pass ) {
			// If this is a password, remove confusing chars
			$r = str_replace( array( '1', 'i', 'l', '0', 'o', '5', 's' ), '', $r );
		}
		
		if ( strlen( $r ) > $len ) {
			return substr( $r, 0, $len );
		}
		return $r;
	}
	
	
	/**
	 * Generates a random number
	 *
	 * @param int $min Minimum number size. Defaults to 1
	 * @param int $max Maximum number size. Defaults to maximum allowed in 32 bit PHP
	 */
	public function randomInt( $min = 1, $max = 0x7FFFFFFF ) {
		$d = $max - $min;
		if ( $d < 0 || $d > 0x7FFFFFFF ) {
			$d = 1;
		}

		$r = $this->IV( 4, true );
		$p = unpack( "Nint", $r );
		$f = ( float ) ( $p['int'] & 0x7FFFFFFF ) / 2147483647.0;

		return  round( $f * $d ) + $min;
	}
	
	
	/**
	 * Generates a random IV for encryption and password/string generation
	 *
	 * @returns mixed Binary IV
	 */
	public function IV( $size, $ssl = false ) {
		if ( $ssl && function_exists( 'openssl_random_pseudo_bytes' ) ) {
			return openssl_random_pseudo_bytes( $size, true );
		}
		return mcrypt_create_iv( $size, MCRYPT_DEV_URANDOM );
	}
	
	/**#@-*/
	
	
	
	/**#@+
	 * Encryption
	 */
	
	
	/**
	 * Encrypt and serialize data with a given key
	 *
	 * @param mixed $data Data to encrypt (assumes strings are UTF-8)
	 * @param string $key Encryption key
	 * @return string Serialized and encrypted
	 */
	public function cryptSerialize( $data, $key ) {
		if ( empty( $data ) ) {
			return '';
		} else {
			$data = json_encode( $data );
			return $this->encryption( $data, $key, 'encrypt' );
		}
	}
	
	
	/**
	 * Deserialize and decrypt data
	 * This is safer than PHP's 'deserialize' especially with cookie data
	 *
	 * @param string $data Data to decrypt (this should be completely unaltered)
	 * @param string $key Decryption key
	 * @return mixed Orignally serialized object
	 */
	public function cryptDeserialize( $data, $key ) {
		if ( empty( $data ) ) {
			return '';
		} else {
			$data = $this->encryption( $data, $key, 'decrypt' );
			if ( empty( $data ) ) {
				return '';
			} else {
				try {
					return json_decode( $data );
				} catch ( Exception $e ) {
					return '';
				}
			}
		}
	}
	
	
	/**
	 * Encode ( so called because this is only ECB ) in Rijndael 256 with Base64 encoding
	 *
	 * @param string $data The content to be encoded
	 * @param string $salt Required nonce
	 * @return string Encoded and optionally base64 encoded string
	 */
	public function encode( $data, $salt ) {
		$enc = mcrypt_encrypt( MCRYPT_RIJNDAEL_256, $salt, $data, MCRYPT_MODE_ECB, '' );
		return base64_encode( $enc );
	}
	
	
	/**
	 * Decode as encoded above
	 *
	 * @param string $data The content to be decoded
	 * @param string $salt Required matching nonce to the encrypted data
	 * @return string Decoded content
	 */
	public function decode( $data, $salt ) {
		$data = base64_decode( $data );
		$dec = mcrypt_decrypt( MCRYPT_RIJNDAEL_256, $salt, $data, MCRYPT_MODE_ECB, '' );
		
		// Padding is automatic, but unpad is not
		return $this->_unpad( $dec, 32 );
	}
	
	
	/**
	 * Encrypt/Decrypt in Rijndael 256 with Base64 encoding/decoding
	 *
	 * @param string $str The content to be encrypted/decrypted
	 * @param string $key Encryption key
	 * @param string $mode Function mode ( 'encrypt' / 'decrypt' )
	 * @return string Encrypted or decrypted content
	 */
	public function encryption( $str, $key, $mode = 'encrypt' ) {
		// Open Rijndael-256 in CBC mode
		$td = mcrypt_module_open('rijndael-256', '', 'cbc', '');
		
		// Find the IV size for the mcrypt module
		$size = mcrypt_enc_get_iv_size( $td );
		
		// Block sizes are needed to calculate the padding ( further down )
		$bsize = mcrypt_enc_get_block_size( $td );
		
		// Key size
		$ksize = mcrypt_enc_get_key_size( $td );
		
		// Hashed key ( for consistency )
		$hkey = hash( 'ripemd160', $key );
		
		// Hash the key for consistency and use only the key size needed from the front
		$key = substr( $hkey, 0, $ksize );
		
		// Salt for encoding/decoding is extracted from the back of the key hashed again
		$salt = substr(  hash( 'ripemd160', $hkey ), ( -1 * $ksize ) );
		
		if ( $mode == 'encrypt' ) {
			/// We're encrypting. Create a new initialization vector
			$iv = $this->IV( $size );
		} else {
			// Unwrap the combined IV and data packet and extract the IV from the front (using IV size)
			$str = $this->decode( $str, $salt );
			$iv = mb_substr( $str, 0, $size );
			
			// Isolate the data by removing the IV altogether and decode in preparation for decryption
			$str = mb_substr( $str, mb_strlen( $iv ) );
			$str = base64_decode( $str );
		}
		
		// Initialize mcrypt
		mcrypt_generic_init( $td, $key, $iv );
		
		if ( $mode == 'encrypt' ) {
			// Prepare string by padding to match block size and encrypt
			$str = $this->_pad( $str, $bsize );
			$enc = mcrypt_generic( $td, $str );
			
			// Add the IV to the front and Encode
			$out = $this->encode( $iv . base64_encode( $enc ), $salt );
		} else {
			// Decrypt the data ( we removed the IV and decoded above ) and remove padding
			$str = mdecrypt_generic( $td, $str );
			$out = $this->_unpad( $str, $bsize );
		}
		
		// Clean up
		mcrypt_generic_deinit( $td );
		mcrypt_module_close( $td );
		
		// Return encrypted/decrypted string
		return $out;
	}
	
	/**#@-*/
	
	
	
	/**#@+
	 * Helpers
	 */
	
	
	/**
	 * Pad data to encryption block size.
	 * Michael Corleone says hello.
	 */
	private function _pad( $str, $bsize ) {
		// Find the pad size for this block size and string length
		$pad = $bsize - ( mb_strlen( $str ) % $bsize );
		
		// Repeat the equivalent character up to the pad size
		$str .= str_repeat( chr( $pad ), $pad );
		
		return $str;
	}
	
	
	/**
	 * Remove extra padding added during encryption.
	 * This is a bit of a hack, so if you have improvements, please add them [ and let me know :) ].
	 * Thanks!
	 */
	private function _unpad( $str, $bsize ) {
		$len = mb_strlen( $str );
		
		// Find the pad character ( last one )
		$pad = ord( $str[$len - 1] );

		// If padding would have been applied to the string...
		if ($pad && $pad < $bsize) {
			// ...find the pad
			$pm = preg_match( '/' . chr( $pad ) . '{' . $pad . '}$/', $str );

			// Pad found, strip it.
			if( $pm ) {
				return mb_substr( $str, 0, $len - $pad );
			}
		}
		return $str;
	}
	
	
	/**
	 * Workaround for mt_rand abnormalities
	 */
	public function _rnd( $min, $max ) {
		$r = 0;
		if ( $min > $max ) {
			$min ^= $max;
			$max ^= $min;
			$min ^= $max;
		}
		while( 0 === $r || $r < $min || $r > $max ) {
			$r = mt_rand( $min, $max );
		}
		
		return $r;
	}
	
	/**#@-*/
}

4 thoughts on “Crypto class 0.3

  1. Pingback: Very simple encryption class for PHP | This page intentionally left ugly

Leave a comment