You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

135 lines
4.3 KiB

<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\ParagonCrypto;
use IOcornerstone\Framework\RandomEngine;
/**
* @license MIT/ISC
* @author by Paragon Initiative Enterprises
* @link https://github.com/paragonie/pecl-libsodium-doc/blob/master/chapters/09-recipes.md
*
* I've added HKDF extracts a pseudorandom key (PRK) using an HMAC hash function
* (e.g. HMAC-SHA256) on salt (acting as a key) to secure the input key
*
*
* Encrypted Storage
* Problem: We want to store data in a cookie such that user cannot read nor alter its contents.
* Desired Solution: Authenticated secret-key encryption, wherein the nonce is stored with the ciphertext.
* Each encryption and authentication key should be attached to the cookie name.
* This strategy combines both sodium_crypto_stream_xor() with sodium_crypto_auth().
*/
class SodiumStorage {
private $randomEngine;
const SALT_SIZE_IN_BYTES = 16;
/**
* Sets the encryption key
*/
public function __construct(#[\SensitiveParameter] private string $key) {
$this->randomEngine = new RandomEngine();
}
public function encrypt(#[\SensitiveParameter] string $plain_text, string $item_name = ""): string {
$nonce = $this->randomEngine->get_bytes(
SODIUM_CRYPTO_STREAM_NONCEBYTES
);
$salt = $this->randomEngine->get_bytes(self::SALT_SIZE_IN_BYTES);
list ($enc_key, $auth_key) = $this->splitKeys($item_name, sodium_bin2hex($salt));
$cipher_text = sodium_crypto_stream_xor(
$plain_text,
$nonce,
$enc_key
);
sodium_memzero($plain_text);
$mac = sodium_crypto_auth($nonce . $cipher_text, $auth_key);
sodium_memzero($enc_key);
sodium_memzero($auth_key);
return sodium_bin2hex($mac . $nonce . $salt . $cipher_text);
}
public function decrypt(string $cypher_data, string $item_name = ""): string {
$bin_data = sodium_hex2bin($cypher_data);
$mac = mb_substr(
$bin_data,
0,
SODIUM_CRYPTO_AUTH_BYTES,
'8bit'
);
$nonce = mb_substr(
$bin_data,
SODIUM_CRYPTO_AUTH_BYTES,
SODIUM_CRYPTO_STREAM_NONCEBYTES,
'8bit'
);
$salt = mb_substr(
$bin_data,
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES,
self::SALT_SIZE_IN_BYTES,
'8bit'
);
$cipher_text = mb_substr(
$bin_data,
SODIUM_CRYPTO_AUTH_BYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES + self::SALT_SIZE_IN_BYTES,
null,
'8bit'
);
list ($enc_key, $auth_key) = $this->splitKeys($item_name, sodium_bin2hex($salt));
if (sodium_crypto_auth_verify($mac, $nonce . $cipher_text, $auth_key)) {
sodium_memzero($auth_key);
$plaintext = sodium_crypto_stream_xor($cipher_text, $nonce, $enc_key);
sodium_memzero($enc_key);
if ($plaintext !== false) {
return $plaintext;
}
} else {
sodium_memzero($auth_key);
sodium_memzero($enc_key);
}
throw new \Exception('Decryption failed.');
}
/**
* @return array(2) [encryption key, authentication key]
*/
private function splitKeys(string $item_name, #[\SensitiveParameter] string $salt): array {
$enc_key = hash_hkdf('sha256', $this->key, SODIUM_CRYPTO_STREAM_KEYBYTES, md5('encryption' . $item_name), $salt);
$auth_key = hash_hkdf('sha256', $this->key, SODIUM_CRYPTO_AUTH_KEYBYTES, md5('authentication' . $item_name), $salt);
return [$enc_key, $auth_key];
}
}
/*
* Example:
$secretkey = "78a5011b9997cd03a28a3412c66565b7c32715b35e055d7abfc228236308d3b2";
$plain_text = "Hello World!";
$sc = new ParagonCrypto\SodiumStorage($secretkey);
$index = "sensitive";
$encoded = $sc->encode($index, $plain_text);
setcookie($index, $encoded);
// On the next page load:
try {
if (!array_key_exists($index, $_COOKIE)) {
throw new \Exception('No Cookie!');
}
$data = $_COOKIE[$index];
$plain_text = $sc->decode($index, $data);
echo $plain_text;
} catch (Exception $ex) {
// Handle the exception here
echo $ex->getMessage();
}
*/