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

Hi, Your Encryption Sucks

This is a result of programmers trying to reinvent the wheel, more often than not, when existing tools are more that sufficient to get the job done. But since these are efforts by those who either don’t understand what they’re doing or think they can do better, let me clarify one glaringly obvious fact that a lot of them overlook.

Anything you create will be worse

Not that someone isn’t clever to come across a better algorithm or a better technique. It’s all down to a simple matter of logistics and probability (another thing inherent to encryption). Your own concoctions will be seen by your eyes, your boss’ (maybe) and a few colleagues.

This is vastly below the scrutiny existing libraries receive. There are countless eyes scanning existing libraries whereas yours won’t and it won’t be a fair comparison. Which do you think is more likely to expose bugs?

So how do you do it properly?

There are countless forum threads out there complaining about decryption being corrupted. This is usually because block ciphers need their input strings in… ready? Specific size blocks. The solution is to pad the data to match the block size of the cipher before encryption and the remove the pad after encryption.

Here are two functions in PHP that do just that when given the string to pad and the block size.

function _pad( $str, $bsize ) {

	// Find the pad size
	$pad = $bsize - ( strlen( $str ) % $bsize );

	// Repeat the equivalent character up to the pad size
	$str .= str_repeat( chr( $pad ), $pad );

	return $str;
}

function _unpad( $str, $bsize ) {

	// Michael Corleone says hello
	$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;
}

And now of course, you need an encryption function that uses it and gets back the expected result. I’ve seen many examples of multiple functions doing this, but I don’t see why one can’t accomplish both encryption and decryption. The critical aspect of IV based (initialization vector) encryption is packaging it along with the encrypted data and making sure both are in a format that’s not prone to corruption, yet is still easy to decrypt. Base64 will work nicely for this.

So here’s how you don’t reinvent the wheel…

Keep in mind that I may have been suffering from mild sleep-deprivation when I wrote this.

function _crypt( $str, $key, $encrypt = true ) {

	// Open Rijndael-256 in CBC mode (was CFB; code fixed 12/08/2012, comment changed 12/13/2012).
	// Change the encryption mode to 'ecb' if you enjoy Herpes
	$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 );

	// Hash the key for consistency and use only the key size needed
	$key = substr( hash( 'ripemd160', $key ), 0, mcrypt_enc_get_key_size( $td ) );

	if( $encrypt ) {

		/*
			We're encrypting. Create a new initialization vector.

		 	If you change this to MCRYPT_RAND for any reason other than
			for testing or because your platform doesn't support it,
			I'll hunt you down and kick you in the groin.
		*/
		$iv = mcrypt_create_iv( $size, MCRYPT_DEV_URANDOM);

	} else {

		// Unwrap the combined IV and data packet
		$str = base64_decode( $str );

		// Extract the IV from the front (using IV size)
		$iv = mb_substr( $str, 0, $size );

		/*
			Isolate the data by removing the IV altogether.

			There's some controversy about using str_replace on multibyte
			characters, but as far as I can tell, there seem to be no ill effects.
			Besides, there's no equivalent "mb_replace" in PHP unless it's DIY.
		*/
		$str = str_replace( $iv, '', $str );

		// Decode in preparation for decryption
		$str = base64_decode( $str );
	}

	// Initialize mcrypt
	// I keep calling this "MyCrypt" in my head. Probably because of "MySQL"
	mcrypt_generic_init( $td, $key, $iv );

	if ( $encrypt ) {

		// Prepare string by padding to match block size
		$str = _pad( $str, $bsize );

		/*
			Encrypt the data.

			I'm supposed to say "this is where the magic happens" here, except I'll never say,
			"this is where the magic happens". That is a tired and old cliche with no useful
			purpose other than to engender ire and abject contempt to whoever says it. If
			"this is where the magic happens", then why are you trying desperately to point it
			out to everyone? Shouldn't you be hiding the location "where the magic happens"?
			The phrase "this is where the magic happens" was created to let everyone know that
			the speaker is constantly getting sex and this is their favorite
			[ bed / hooker / other object ] on which they receive it. No one realy believes it
			because people who say it are either never seeing this "magic" or are date-rapists.
			Now, despite the dubious origin of the phrase "this is where the magic happens",
			people have started using it at every inappropriate instance. E.G. While pointing
			to a chair in the office "this is where the magic happens". Unless the tool who says
			it is looking at porn at work, he's not performing "magic". In fact, that's not
			"magic", it's just disturbing. Not just because it's a public place, but people
			will be borrowing his office supplies without knowing he's touched them after he's
			touched something else... repeatedly.

			BTW, this is where the magic happens.
		*/
		$enc = mcrypt_generic( $td, $str );

		// Base64 the encrypted data, add the IV to the front and base64 again.
		$out = base64_encode( $iv . base64_encode( $enc ) );
		/*
			This ensures that the data remains portable because both the IV and the encrypted
			data contain special chracters ( unicode / multibyte ) that may be lost if handled
			as-is. This is especially true if the functions you're passing the data to can only
			handle ASCII data.
		*/

	} else {

		// Decrypt the data ( we removed the IV and decoded above )
		$str = mdecrypt_generic( $td, $str );

		// Remove the padding added to fit the block size
		$out = _unpad( $str, $bsize );
	}

	// Clean up
	mcrypt_generic_deinit( $td );
	mcrypt_module_close( $td );

	// Return encrypted/decrypted string
	return $out;
}

Very simple way to test this is to put it in a loop.

for( $i = 0; $i < 30; $i++ ) {

	$key = md5( $i ); // Simple key generation
	$data = "This is some test data";

	$enc = _crypt( $data, $key );
	$dec = _crypt( $enc, $key, false );

	echo "Test data :  $data <br />";
	echo "Key : $key; <br />";
	echo "Encrypted data : $enc <br />";
	echo "Decrypted data : $dec <br />";
	echo "Match : " . ( ( $data == $dec )? "True" : "False" ) . "<br /><br /><br />";
}

I’ve tested this a couple of times and it worked. I don’t have the time or the energy to keep testing, but trust me, this is how you do encryption properly. If there are bugs, scan through and see where I’ve missed a semicolon or something, but rest assured, this is “standard”.

Now… bedtime!