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
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();
|
|
}
|
|
*/ |