<?php
if (PHP_VERSION_ID < 70000) {
die('no');
}
/**
* Use HKDF to derive multiple keys from one.
* http://tools.ietf.org/html/rfc5869
*
* @param string $hash Hash Function
* @param string $ikm Initial Keying Material
* @param int $length How many bytes?
* @param string $info What sort of key are we deriving?
* @param string $salt
* @return string
* @throws Exception
*/
function hash_hkdf(string $algo, string $ikm, int $length, string $info = '', $salt = null)
{
$digest_length = mb_strlen(hash_hmac($algo, '', '', true), '8bit');
// Sanity-check the desired output length.
if (empty($length) || !is_int($length) ||
$length < 0 || $length > 255 * $digest_length) {
throw new Exception("Bad output length requested of HKDF.");
}
// "if [salt] not provided, is set to a string of HashLen zeroes."
if ($salt === null) {
$salt = str_repeat("\x00", $digest_length);
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = hash_hmac($algo, $ikm, $salt, true);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
if (mb_strlen($prk, '8bit') < $digest_length) {
throw new Exception('HKDF-Expand failed');
}
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; mb_strlen($t, '8bit') < $length; ++$block_index) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = hash_hmac(
$algo,
$last_block . $info . chr($block_index),
$prk,
true
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
$orm = mb_substr($t, 0, $length, '8bit');
if ($orm === false) {
throw new Exception('Unexpected error');
}
return $orm;
}
/**
* Encrypt a message with AES-256-CTR + HMAC-SHA256
* @param string $message
* @param string $key
* @return string
*/
function aes256ctr_hmacsha256_encrypt(string $message, string $key)
{
$iv = random_bytes(16);
$salt = random_bytes(16);
$eKey = hash_hkdf('sha256', $key, 32, 'Encryption Key', $salt);
$aKey = hash_hkdf('sha256', $key, 32, 'Authentication Key', $salt);
$ciphertext = $iv . $salt . openssl_encrypt(
$message,
'aes-256-ctr',
$eKey,
OPENSSL_RAW_DATA,
$iv
);
$mac = hash_hmac('sha256', $ciphertext, $aKey, true);
return base64_encode($mac . $ciphertext);
}
/**
* Decrypt a message with AES-256-CTR + HMAC-SHA256
* @param string $message
* @param string $key
* @return string
* @throws Exception
*/
function aes256ctr_hmacsha256_decrypt(string $ciphertext, string $key)
{
$decode = base64_decode($ciphertext);
if ($decode === false) {
throw new Exception("Encoding error");
}
$mac = mb_substr($decode, 0, 32, '8bit');
$iv = mb_substr($decode, 32, 16, '8bit');
$salt = mb_substr($decode, 48, 16, '8bit');
$ciphertext = mb_substr($decode, 64, null, '8bit');
$aKey = hash_hkdf('sha256', $key, 32, 'Authentication Key', $salt);
$calcMac = hash_hmac('sha256', $iv . $salt . $ciphertext, $aKey, true);
if (!hash_equals($calcMac, $mac)) {
throw new Exception("Invalid message");
}
$eKey = hash_hkdf('sha256', $key, 32, 'Encryption Key', $salt);
return openssl_decrypt(
$ciphertext,
'aes-256-ctr',
$eKey,
OPENSSL_RAW_DATA,
$iv
);
}
$key = random_bytes(32);
$message = "This code rocks";
$ciphertext = aes256ctr_hmacsha256_encrypt($message, $key);
var_dump($ciphertext);
$plaintext = aes256ctr_hmacsha256_decrypt($ciphertext, $key);
var_dump($plaintext);
preferences:
24.19 ms | 417 KiB | 5 Q