How to encrypt data in browser with JavaScript and decrypt on server side with PHP

Client-server encryption-decryption using Advanced Encryption Algorithm (AES) in client and server is complicated because exactly the same algorithm must be implemented twice: once for client side in JavaScript and once for server side in PHP,C# etc.


AES is a symmetric block cipher for encrypting texts which can be decrypted with the original encryption key. In an addition to a cipher key it is recommended to use an initialisation vector. It is a fixed-size input for the cryptographic algorithm that is used for encryption and must be known for decryption.

Initialization vector is not a secret. Once it was generated on the crypt side (either client or server) it must be anyhow transferred to the decryption side. The easiest way to send vector to the decryption side is together with cipher. Just add it to cipher and then encode the result with base64 to prepare to transfer through HTTP link.
Note that server must know that received cipher is encoded with base64 and a part of it is an initialization vector.

The typical scenario:
There is data on the client side (some input from user, a password) that will be encrypted by JavaScript and then send to server side with HTTP request where it will be decrypted.

Components
We need 3 components to encrypt-decrypt successfully:
1. cipher – will be generated by JavaScript and send to the server
2. initialization vector – will be generated by JavaScript and send to the server together with cipher
3. key – must be available for both client and server

There are 3 things that we should take care about to encrypt-decrypt successfully:
1. Algorithm must be the same
2. Server must know how the cipher is encoded
3. Server must know how to get initialisation vector part from received cipher

Algorithm pair
The problem is to found a compatible combination of JavaScript and server side algorithms.
PHP has two groups of functions for encryption: mcrypt and OpenSSL. Each group can use different encryption algorithms. I tried lots of different combinations (including crypto-js library and different JS-implementations of mcrypt) until I finally come to the solution. The problem is that most of the libraries do not document how cipher is combined with vector. And how this combination is encoded (uue, base64, utf etc.). Thus it can be decrypted with the same library but it is absolutely no way to decrypt it even using the same algorithm in another library.

After lots of experiments I found a workable pair: GibberishAES JavaScript library on the client side with a custom PHP class uses php openssl extension on the server. It is known how GibberishAES encodes combination of cipher and initialisation vector thus we can decrypt it in PHP.
Here is my simple Gibberish PHP class. It is based on initial code proposed by nbari at dalmp dot com http://www.php.net/manual/en/function.openssl-decrypt.php#107210

/**
 * Gibberish AES, a PHP Implementation
 * See Gibberish AES javascript encryption library, @link https://github.com/mdp/gibberish-aes
 *
 * This implementation is based on initial code proposed by nbari at dalmp dot com
 * @link http://www.php.net/manual/en/function.openssl-decrypt.php#107210
 *
 * OpenSSL php extension is required */ 
class GibberishAES
{
    protected $_nKeySize = 256;            // The key size in bits
    protected static $valid_key_sizes = array(128, 192, 256);   // Sizes in bits

    function __construct() 
    {}

    /**
     * Encrypt AES (256, 192, 128)
     * @param $string string
     * @param $key string algorithm encryption 
     * @return string base64 encoded encrypted cipher
     */
    function encrypt($string, $key) 
    {
        $salt = openssl_random_pseudo_bytes(8);

        $salted = '';
        $dx = '';

        // Lengths in bytes:
        $key_length = (int) ($this->_nKeySize / 8);
        $block_length = 16; // 128 bits, iv has the same length.
        // $salted_length = $key_length (32, 24, 16) + $block_length (16) = (48, 40, 32)
        $salted_length = $key_length + $block_length;

        while (strlen($salted) < $salted_length) 
        {
            $dx = md5($dx.$key.$salt, true);
            $salted .= $dx;
        }

        $key = substr($salted, 0, $key_length);
        $iv = substr($salted, $key_length, $block_length);

        return base64_encode('Salted__' . $salt . openssl_encrypt($string, "aes-".$this->_nKeySize."-cbc", $key, true, $iv));
    }

    /**
     * Decrypt AES (256, 192, 128)
     * @param $string base64 encoded cipher  
     * @param $key string algorithm encryption 
     * @return dencrypted string
     */
    function decrypt($string, $key) 
    {
        // Lengths in bytes:
        $key_length = (int) ($this->_nKeySize / 8);
        $block_length = 16;

        $data = base64_decode($string);
        $salt = substr($data, 8, 8);
        $encrypted = substr($data, 16);

        /**
         * From https://github.com/mdp/gibberish-aes
         *
         * Number of rounds depends on the size of the AES in use
         * 3 rounds for 256
         *     2 rounds for the key, 1 for the IV
         * 2 rounds for 128
         *     1 round for the key, 1 round for the IV
         * 3 rounds for 192 since it's not evenly divided by 128 bits
         */
        $rounds = 3;
        if (128 === $this->_nKeySize) 
        {
            $rounds = 2;
        }

        $data00 = $key.$salt;
        $md5_hash = array();
        $md5_hash[0] = md5($data00, true);
        $result = $md5_hash[0];

        for ($i = 1; $i < $rounds; $i++) 
        {
            $md5_hash[$i] = md5($md5_hash[$i - 1].$data00, true);
            $result .= $md5_hash[$i];
        }

        $key = substr($result, 0, $key_length);
        $iv = substr($result, $key_length, $block_length);

        return openssl_decrypt($encrypted, "aes-".$this->_nKeySize."-cbc", $key, true, $iv);
    }

    /**
     * Sets the key-size for encryption/decryption in number of bits
     * @param  $nNewSize int The new key size. The valid integer values are: 128, 192, 256 (default) */
    function setMode($nNewSize) 
    {
        if (is_null($nNewSize) || empty($nNewSize) || !is_int($nNewSize) || !in_array($nNewSize, self::$valid_key_sizes)) 
            return;

        $this->_nKeySize = $nNewSize;
    }    
}

Both base64 encoding/decoding and initialisation vector is already included in this class. Thus it is very simple to use it:

//JavaScript
var stringEncrypted = GibberishAES.enc(stringOriginal, encryptionKey);

//PHP
$encryptor = new \GibberishAES();
$stringOriginal = $encryptor->decrypt($stringEncrypted, $encryptionKey);

All ciphers this library produces begins with the same combination:   U2FsdGVkX1
This is string “Salted__” encoded with base64. All ciphers created by GibebrishAES starts with this string. Do not worry about it.

Unit test

class GibberishAESTest extends \PHPUnit_Framework_TestCase 
{
    const KEY = '123';
    const CYPHER = 'U2FsdGVkX1/Jt+8DPq0t38YygU+8ZamsuekY7zdsO5M=';
    const STR = 'password';

    function testencrypt()
    {
        $obj = new \GibberishAES();
        $cypher = $obj->encrypt(self::STR, self::KEY);
        $this->assertEquals(self::STR, $obj->decrypt($cypher, self::KEY));
    }

    function testdecrypt()
    {
        $obj = new \GibberishAES();
        $this->assertEquals(self::STR, $obj->decrypt(self::CYPHER, self::KEY));
    }

    function testsetMode()
    {
        $obj = new \GibberishAES();
        $obj->setMode(128);
        $cypher = $obj->encrypt(self::STR, self::KEY);
        $this->assertEquals(self::STR, $obj->decrypt($cypher, self::KEY));

        $obj->setMode(256);
        $cypher = $obj->encrypt(self::STR, self::KEY);
        $this->assertEquals(self::STR, $obj->decrypt($cypher, self::KEY));

        $obj->setMode(192);
        $cypher = $obj->encrypt(self::STR, self::KEY);
        $this->assertEquals(self::STR, $obj->decrypt($cypher, self::KEY));
    }
}

After all I found another JavaScript/PHP combination AES-library, created by one developer, so it should work. But I did not test it.

P.S. Why do we need to encrypt a password and send it to the server and decrypt there instead of just send a hash? Sometimes it is needed because system authenticates users somewhere in a third-party system. In an Active Directory for instance. And a clear password is needed for authentication.

P.P.S. Note! Whatever hash- or encryption- algorithm you use always transfer sensitive data through HTTPS!