PHP Crypto

I often have to work on projects that use passwords so poorly, or use encryption so haphazardly, that I just throw my hands up and just rewrite the whole damn thing. This is a replacement for just such a damn thing. And while, I don’t expect to be a drop-in replacement¬†for most projects, I figure it gives the basics to what most people want. If you’re starting out with a new application, maybe this will help you.

There are some things done here that may be overkill (some would say¬†“underkill”?) but I’ll leave it up to your discretion. If you find any errors/corrections/improvements, please let me know.

<?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 email me.
 * 
 * @author Eksith Rodrigo <reksith at gmail.com>
 * @license http://opensource.org/licenses/ISC ISC License
 */

class Crypto {
	
	/**
	 * Application specific random string to encode salts
	 * IMPORTANT:This should be changed in your own deployments (and never repeated)
	 * It will also be easier to move this to a defined variable
	 */
	private $saltKey = 'i^-ZsV[+vCHl)3D?-S7GiLSK~iv=qzQO%k`x%0?:Lps-[+88M//J+z- >+KKY``*';
	
	
	/**#@+
	 * Instance getter, constructor
	 */
	
	private static $instance;
	static function GetInstance() {
		if ( !isset( $instance ) ) {
			self::$instance = new Crypto();
		}
		return self::$instance;
	}
	
	private function __construct() {}
	
	/**#@-*/
	
	
	
	
	/**#@+
	 * Password related functions
	 */
	
	
	/**
	 * Hash password with blowfish + salt
	 *
	 * @param string $pass Raw password
	 * @param string $decodedSalt Stored salt after being decoded or freshly generated
	 */
	public function Password( $pass, $decodedSalt ) {
		// This will reject NULL, 0, FALSE etc... but these are terrible passwords anyway
		if ( empty( $pass ) || empty( $decodedSalt ) ) {
			return false;
		}
		
		return crypt( $pass, '$2y$09$'. $decodedSalt . '$' );
	}
	
	
	/**
	 * Verify password hash against the stored value
	 * 
	 * @param string $pass Raw password
	 * @param string $encodedSalt Password salt as stored in the database
	 * @param string $storedPass Hashed password to match against 
	 */
	public function VerifyPassword( $pass, $encodedSalt, $storedPass ) {
		if ( empty( $pass ) || empty( $encodedSalt ) || empty( $storedPass ) ) {
			return false;
		}
		
		$salt = $this->Decode( $encodedSalt, $this->saltKey );
		$pass = $this->Password( $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 $pass User provided password
	 * @param string $salt Generated salt (encoded using the password)
	 */
	public function PairPasswordSalt( &$pass, &$salt ) {
		if ( empty( $pass ) ) {
			return false;
		}
		
		$salt = bin2hex( $this->IV( 30 ) ); // Boyz II Men
		$pass = $this->Password( $pass, $salt );
		
		$salt = $this->Encode( $salt, $this->saltKey );
		
		return true;
	}
	
	
	/**
	 * Random password generator
	 * 
	 * @param int $len Password length
	 * @return string Random password with commonly confused characters removed
	 */
	public function RandomPassword( $len ) {
		// Extra padding to accomodate removed chars
		$r = $this->RandomStr( $len + 15 );
		
		// Remove confusing chars
		$r = str_replace( array( '1', 'i', 'l', '0', 'o', '5', 's' ), '', $r );
		
		return substr( $r, 0, $len );
	}
	
	/**#@-*/

	
	/**#@+
	 * Random number/string generators
	 */
	
	/**
	 * Generates a random string of a given length (or random between 5 and 10 if length is empty)
	 * 
	 * @param int $len Random string length. If empty, will revert to a number between 10 - 20
	 * @return string Alphanumeric string
	 */
	public function RandomStr( $len ) {
		if ( empty( $len ) ) {
			$len = mt_rand( 10, 20 );
		}
		
		$r = base64_encode( $this->IV( 30 ) );
		$r = str_replace( array( '+', '/', '-', '_', '=' ), '', $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 );
		$p = unpack( "Nint", $r );
		$f = ( float ) ( $p['int'] & 0x7FFFFFFF ) / 2147483647.0;
		
		return  round( $f * $d ) + $min;
	}
	
	/**#@-*/
	
	
	
	/**
	 * 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->Crypt( $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->Crypt( $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 );
	}

	/**
	 * Generates a random IV for encryption and password/string generation
	 * 
	 * @returns mixed Binary IV
	 */
	public function IV( $size ) {
		return mcrypt_create_iv( $size, MCRYPT_DEV_URANDOM );
	}
	
	
	/**
	 * Decode as encoded above in Rijndael 256 with optional Base64 decoding 
	 *
	 * @param string $data The content to be decoded.
	 * @return string Decrypted content
	 */
	public function Crypt( $str, $key, $mode = 'encrypt', $time = null ) {
		// 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;
	}
		
	/**#@-*/
}

 

The CryptDeserialize and CryptSerialize functions were originally meant for storing encrypted cookies/sessions. You can use them for something similar, however the string values must be UTF-8 for it to work correctly (due to json_decode/encode).

7:30AM… Time for bed!

Advertisement