One of the missing feature of the OpenSSL extension for PHP was the support for Authenticated Encryption. If you are wondering why the authentication is important for encryption, I suggest to have a look at this presentation.
The issue with OpenSSL was related to the API design of the functions openssl_encrypt and openssl_decrypt that didn't return the authentication hash of the encrypted data.
string openssl_encrypt(
string $data,
string $method,
string $password,
[ int $options = 0 ],
[ string $iv = "" ]
)
string openssl_decrypt(
string $data,
string $method,
string $password,
[ int $options = 0 ],
[ string $iv = "" ]
)
The output of openssl_encrypt is the encrypted data, without the authentication hash. This hash is required to decrypt a message, because the algorithm needs to authenticate the data before proceed with decryption.
This issue will be solved from PHP 7.1 thanks to RFC openssl_aead proposed by Jakub Zelenka. The idea is to add some optional parameters to the previous OpenSSL functions. The new API is reported below:
string openssl_encrypt(
string $data,
string $method,
string $password,
[ int $options = 0 ],
[ string $iv = "" ],
[ string &$tag = NULL ],
[ string $aad = "" ],
[ int $tag_length = 16 ]
)
string openssl_decrypt(
string $data,
string $method,
string $password,
[ int $options = 0 ],
[ string $iv = "" ],
[ string $tag = "" ],
[ string $aad = "" ]
)
The authentication hash is stored in the $tag variable. This value is filled by the openssl_encrypt function and returned as reference.
The other optional parameter $aad represents additional authentication data that you could use to protect the message against alterations, without the encryption part. For instance, if you need to encrypt an email leaving the header information in plaintext, like the sender and the receiver, you can pass the header in $aad.
The last optional parameter $tag_length is the length in bytes of the hash value, that is 16 by default. This value is related to the encryption algorithm used. I will give some details on it later.
To decrypt an authenticated message, you need to pass the $tag value to openssl_decrypt and optionally the additional authenticated data ($aad).
Until PHP 7.1 will not be available, you can test this new feature using a Release Candidate version of PHP 7.1, for instance 7.1.0RC05.
The OpenSSL extension provides the support for two authenticated encryption algorithm: GCM and CCM. I will show how to use it in the next sections.
Galois/Counter Mode (GCM)
The Galois/Counter Mode (GCM) is a mode of operation for symmetric key cryptographic block ciphers that provides encryption and authentication. If you are interested in the details of this algorithm you can read the Wikipedia page. This is an algorithm used in many applications like IPsec, SSH and TLS 1.2. Used together with AES (AES-GCM) is included in the NSA Suite B Cryptography . This algorithm is very fast because the execution can be parallelized. Moreover, the algorithm does not any patents and can be used without restrictions.
You can check if your OpenSSL extension supports the GCM mode using the openssl_get_cipher_methods function. If you see "-gcm" or "-GCM" at the end of a cipher name you can use the Galois/Counter Mode. You need to have at least OpenSSL 1.1 to support this algorithm.
Below is reported an example using aes-256-gcm algorithm (i.e. AES block cipher with 256 bit key):
$algo = 'aes-256-gcm';
$iv = random_bytes(openssl_cipher_iv_length($algo));
$key = random_bytes(32); // 256 bit
$data = random_bytes(1024); // 1 Kb of random data
$ciphertext = openssl_encrypt(
$data,
$algo,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
// Change 1 bit in ciphertext
// $i = rand(0, mb_strlen($ciphertext, '8bit') - 1);
// $ciphertext[$i] = $ciphertext[$i] ^ chr(1);
$decrypt = openssl_decrypt(
$ciphertext,
$algo,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if (false === $decrypt) {
throw new Exception(sprintf(
"OpenSSL error: %s", openssl_error_string()
));
}
printf ("Decryption %s\n", $data === $decrypt ? 'Ok' : 'Failed');
If you need to store the encrypted value somewhere you need to store $tag and $iv values concatenated with $ciphertext. This because you need to pass these data again to decrypt the ciphertext. If you store the $tag value you need to remember also the size of this value. I suggest to use the default value of 16 bytes to simplify the usage. The tag length for the GCM mode can be between 4 and 16 bytes.
During the decryption part, we can check for errors if the $decrypt value is false. For instance, if someone has altered the encrypted message we can recognize it because the authentication will fail. You can uncomment the lines 13 and 14 from the previous example to simulate a change in the $ciphertext. Unfortunately, in the case of authentication error the openssl_error_string function returns an empty string. I just opened this report at bugs.php.net.
If you want to use additional authenticated data you can pass the string to be authenticated in the $aad parameter, as reported in the following example:
$algo = 'aes-256-gcm';
$iv = random_bytes(openssl_cipher_iv_length($algo));
$key = random_bytes(32); // 256 bit
$email = 'This is the secret message!';
$aad = 'From: foo@domain.com, To: bar@domain.com';
$ciphertext = openssl_encrypt(
$email,
$algo,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad
);
// Change 1 bit in additional authenticated data
// $i = rand(0, mb_strlen($aad, '8bit') - 1);
// $aad[$i] = $aad[$i] ^ chr(1);
$decrypt = openssl_decrypt(
$ciphertext,
$algo,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag,
$aad
);
if (false === $decrypt) {
throw new Exception(sprintf(
"OpenSSL error: %s", openssl_error_string()
));
}
printf ("Decryption %s\n", $email === $decrypt ? 'Ok' : 'Failed');
In this case, if you uncomment the lines that change 1 bit in the authenticated data, you will have an authentication error regarding the $aad part.
The encrypted message in this case is composed by $tag . $iv . $aad . $ciphertext. Basically, you need to store also the $aad in plaintext to be able to perform the authentication, during the decryption of the message.
Counter with CBC-MAC (CCM)
Counter with CBC-MAC (CCM) is another authenticated encryption mode for symmetric block ciphers. The CCM mode is also used in many applications like IPsec and TLS 1.2 and is part of the IEEE 802.11i standard. CCM is an alternative implementations of the OCB mode, that was originally covered by patents. The CCM can be used without any restriction. I will not show the details of the CCM algorithm, if you are interested you can read the Wikipedia page. This encryption mode can be used in PHP 7.1 if your OpenSSL extension support the "-ccm" or "-CCM" in the name of the algorithm.
The previous example code works also in the case of CCM, you need only to replace the first line with :
$algo = 'aes-256-ccm';
The only difference with GCM, related to the OpenSSL usage, is the size of $tag. CCM has no limits of tag's length and also the resulted tag is different for each length.
Benchmark GCM vs. CCM
I provided a very simple benchmark script to test the performance of encryption and decryption using GCM and CCM mode. The result is that GCM is 3x faster than CCM. This results is as expected due to the differences between the two algorithms. That said, my suggestion is to use GCM in your code!
This is the script that I used for the benchmark:
$key = random_bytes(32);
$data = random_bytes(1024 * 1024 * 10); // 10 Mb
$iv = random_bytes(openssl_cipher_iv_length('aes-256-gcm'));
$start = microtime(true);
$ciphertext = openssl_encrypt($data, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
$gcmTimeEnc = microtime(true) - $start;
$start = microtime(true);
$decrypt = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
$gcmTimeDec = microtime(true) - $start;
if ($decrypt !== $data) {
throw new Exception("Decryption failed for GCM");
}
$iv = random_bytes(openssl_cipher_iv_length('aes-256-ccm'));
$start = microtime(true);
$ciphertext = openssl_encrypt($data, 'aes-256-ccm', $key, OPENSSL_RAW_DATA, $iv, $tag);
$ccmTimeEnc = microtime(true) - $start;
$start = microtime(true);
$decrypt = openssl_decrypt($ciphertext, 'aes-256-ccm', $key, OPENSSL_RAW_DATA, $iv, $tag);
$ccmTimeDec = microtime(true) - $start;
if ($decrypt !== $data) {
throw new Exception("Decryption failed for CCM");
}
printf("GCM (enc): %.4f, GCM (dec): %.4f\n", $gcmTimeEnc, $gcmTimeDec);
printf("CCM (enc): %.4f, CCM (dec): %.4f\n", $ccmTimeEnc, $ccmTimeDec);
I executed this script using PHP7.1RC5, CPU Intel Core i7, 8 GB RAM, 256 GB SSD, Ubuntu 16.04.
Note: all the PHP scripts used in this post are available here.