PHP AES-128 CBC with HMAC File Encryption


Make sure mcrypt is installed and enabled.
#ubuntu:
sudo apt-get install php5-mcrypt;
sudo php5enmod mcrypt; 
sudo service apache2 restart;#apache
php -r "print_r(stream_get_filters());";  #cli
php -r "print_r(mcrypt_list_algorithms());"; #cli

This class will use the mcrypt php library to encrypt a string (or file) using AES-128 CBC 128 with an HMAC. For AES-128 use a 16 byte key, AES-192 use a 24 byte key, and AES-256 use an 32 byte key.
<?php
$key16 = openssl_random_pseudo_bytes($length=16);
$encrypted = AES::encrypt128CBC($input, $key16);
$output    = AES::decrypt128CBC($encrypted, $key16);
echo $encrypted."\n";
echo $output."\n"; 
 
$input = 'filecontents';
file_put_contents('f.txt', $input);
AES::encryptFile128CBC($finput='f.txt', $foutput='f.aes', $key16);
AES::decryptFile128CBC($finput='f.aes', $foutput='f.out', $key16);
echo file_get_contents($foutput)."\n";
?>
<?php
//http://php.net/manual/en/book.mcrypt.php
//http://php.net/manual/en/filters.encryption.php
//http://php.net/manual/en/function.stream-get-filters.php
 
class AES
{
    public static function encrypt128CBC($data, $key){ return self::encryptCBC($data, $key, 16); }
    public static function encrypt192CBC($data, $key){ return self::encryptCBC($data, $key, 24); }
    public static function encrypt256CBC($data, $key){ return self::encryptCBC($data, $key, 32); }
 
    public static function decrypt128CBC($data, $key){ return self::decryptCBC($data, $key, 16); }
    public static function decrypt192CBC($data, $key){ return self::decryptCBC($data, $key, 24); }
    public static function decrypt256CBC($data, $key){ return self::decryptCBC($data, $key, 32); }
 
    public static function encryptFile128CBC($ifname, $ofname, $key){ return self::encryptFileCBC($ifname, $ofname, $key, 16); }
    public static function encryptFile192CBC($ifname, $ofname, $key){ return self::encryptFileCBC($ifname, $ofname, $key, 24); }
    public static function encryptFile256CBC($ifname, $ofname, $key){ return self::encryptFileCBC($ifname, $ofname, $key, 32); }
 
    public static function decryptFile128CBC($ifname, $ofname, $key){ return self::decryptFileCBC($ifname, $ofname, $key, 16); }
    public static function decryptFile192CBC($ifname, $ofname, $key){ return self::decryptFileCBC($ifname, $ofname, $key, 24); }
    public static function decryptFile256CBC($ifname, $ofname, $key){ return self::decryptFileCBC($ifname, $ofname, $key, 32); }
 
    private static function encryptCBC($data, $key, $key_size)
    {
        $cipher = MCRYPT_RIJNDAEL_128;
        $mode = MCRYPT_MODE_CBC;
        $iv_size = mcrypt_get_iv_size($cipher, $mode);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM);
        $block_size = mcrypt_get_block_size($cipher, $mode);
        $padding = $block_size - (strlen($data) % $block_size);
        $data .= str_repeat(chr($padding), $padding);//PKCS7 Padding
        $encrypted = mcrypt_encrypt($cipher, $key, $data, $mode, $iv);
        $hmac = hash_hmac('sha256', $encrypted, $key, $raw=true);
        $encoded = strtr(base64_encode($hmac.$iv.$encrypted), '+/=', '._-');
        return strlen($key)==$key_size ? $encoded : '';
    }
 
    private static function decryptCBC($data, $key, $key_size)
    {
        $hash_size = 32;
        $cipher = MCRYPT_RIJNDAEL_128;
        $mode = MCRYPT_MODE_CBC;
        $iv_size = mcrypt_get_iv_size($cipher, $mode);
        $block_size = mcrypt_get_block_size($cipher, $mode);
        $decoded = base64_decode( strtr($data, '._-', '+/=') );
        $hmac = substr($decoded, 0, $hash_size);
        $iv = substr($decoded, $hash_size, $iv_size);
        $cmac = hash_hmac('sha256', substr($decoded, $iv_size+$hash_size), $key, $raw=true);
        $decrypted = mcrypt_decrypt($cipher, $key, substr($decoded, $iv_size+$hash_size), $mode, $iv);
        $padding = ord($decrypted[strlen($decrypted) - 1]);
        return $hmac==$cmac ? substr($decrypted, 0, 0-$padding) : '';
    }
 
    //see: http://php.net/manual/en/filters.encryption.php
    //see: http://php.net/manual/en/function.stream-get-filters.php
    private static function encryptFileCBC($input_stream, $aes_filename, $key, $key_size)
    {
        $hash_size = 32;
        $cipher = MCRYPT_RIJNDAEL_128;
        $mode = MCRYPT_MODE_CBC;
        $block_size = mcrypt_get_block_size($cipher, $mode);
        $iv_size = mcrypt_get_iv_size($cipher, $mode);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_DEV_URANDOM);
        $opts= array('mode'=>$mode, 'iv'=>$iv, 'key'=>$key);
 
        $infilesize = 0;
        $fin = fopen($input_stream, "rb");
        $fcrypt = fopen($aes_filename, "wb+");
        if (!empty($fin) && !empty($fcrypt) && strlen($key)==$key_size)
        {
            fwrite($fcrypt, str_repeat("_", $hash_size) );//placeholder, HMAC will go here later
            fwrite($fcrypt, $iv);
            stream_filter_append($fcrypt, 'mcrypt.'.$cipher, STREAM_FILTER_WRITE, $opts);
            while (!feof($fin))
            {
                $block = fread($fin, 8192);
                $infilesize+=strlen($block);
                fwrite($fcrypt, $block);
            }
            $padding = $block_size - ($infilesize % $block_size);//$padding is a number from 0-15
            if (feof($fin) && $padding>0)
            {
                fwrite($fcrypt, str_repeat(chr($padding), $padding) );//perform PKCS7 padding
            }
            fclose($fin);
            fclose($fcrypt);
 
            $stream = 'php://filter/read=user-filter.ignorefirst32bytes/resource=' . $aes_filename;
            $hmac_raw = hash_hmac_file('sha256', $stream, $key, $raw=true);
            $fcrypt = fopen($aes_filename, "rb+");
            fwrite($fcrypt, $hmac_raw);
            fclose($fcrypt);
            return 1;
        }
        return 0;
    }
 
    private static function decryptFileCBC($aes_filename, $out_stream, $key, $key_size)
    {
        $hash_size = 32;
        $cipher = MCRYPT_RIJNDAEL_128;
        $mode = MCRYPT_MODE_CBC;
        $iv_size = mcrypt_get_iv_size($cipher, $mode);
 
        $stream = 'php://filter/read=user-filter.ignorefirst32bytes/resource=' . $aes_filename;
        $hmac_calc = hash_hmac_file('sha256', $stream, $key, $raw=true);
 
        $fcrypt = fopen($aes_filename, "rb");
        $fout = fopen($out_stream, 'wb');
        if (!empty($fout) && !empty($fcrypt) && strlen($key)==$key_size)
        {
            $hmac_raw = fread($fcrypt, $hash_size);
            $iv = fread($fcrypt, $iv_size);
            $opts = $hmac_calc==$hmac_raw ? array('mode'=>$mode, 'iv'=>$iv, 'key'=>$key) : array();
            stream_filter_append($fcrypt, 'mdecrypt.'.$cipher, STREAM_FILTER_READ, $opts);
            while (!feof($fcrypt))
            {
                $block = fread($fcrypt, 8192);
                if (feof($fcrypt))
                {
                    $padding = ord($block[strlen($block) - 1]);//assume PKCS7 padding
                    $block = substr($block, 0, 0-$padding);
                }
                fwrite($fout, $block);
            }
            fclose($fout);
            fclose($fcrypt);
            return 1;
        }
        return 0;
    }
}
 
class AES_HMAC_Skip32Bytes extends PHP_User_Filter
{
    private $skipped=0;
    function filter($in, $out, &$consumed, $closing)
    {
        while ($bucket = stream_bucket_make_writeable($in))
        {
            $outlen = $bucket->datalen;
            if ($this->skipped<32)
            {
                $outlen = min($bucket->datalen,32-$this->skipped);
                $bucket->data = substr($bucket->data, $outlen);
                $bucket->datalen = $bucket->datalen-$outlen;
                $this->skipped+=$outlen;
            }
            $consumed += $outlen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}
 
stream_filter_register("user-filter.ignorefirst32bytes", "AES_HMAC_Skip32Bytes");
code snippets are licensed under Creative Commons CC-By-SA 3.0 (unless otherwise specified)