Skip to content

Instantly share code, notes, and snippets.

@roukmoute
Created February 16, 2021 09:25
Show Gist options
  • Save roukmoute/c5d1ced6a4020ef6f0e790c92fbb23ba to your computer and use it in GitHub Desktop.
Save roukmoute/c5d1ced6a4020ef6f0e790c92fbb23ba to your computer and use it in GitHub Desktop.
LibSodium encrypt/decrypt
<?php
declare(strict_types=1);
use Exception;
use SodiumException;
use TypeError;
class Cryptography
{
/** @var string */
private $key;
/**
* @param string $base64Key Symmetric encryption key encoded in base64
* To generate a new one: base64_encode(sodium_crypto_secretbox_keygen())
*/
public function __construct(string $base64Key)
{
$this->key = base64_decode($base64Key, true);
if (!$this->key) {
throw new TypeError('Cryptography key expects to be a string encoded in base64.');
}
}
public function encrypt(string $message): string
{
try {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
} catch (Exception $exception) {
throw new Exception('It was not possible to gather sufficient entropy', $exception->getCode(), $exception);
}
try {
$cipher = base64_encode(
$nonce .
sodium_crypto_secretbox(
$message,
$nonce,
$this->key
)
);
} catch (SodiumException $exception) {
throw new Exception('Cannot encrypt message', $exception->getCode(), $exception);
}
try {
sodium_memzero($message);
} catch (SodiumException $e) {
}
return $cipher;
}
public function decrypt(string $encrypted): string
{
$decoded = base64_decode($encrypted, true);
if ($decoded === false) {
throw new Exception('The decoding failed');
}
if (mb_strlen($decoded, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
throw new Exception('The message was truncated');
}
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = false;
try {
$plain = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
} catch (SodiumException $e) {
}
if ($plain === false) {
throw new Exception('The message was tampered with in transit');
}
try {
sodium_memzero($ciphertext);
} catch (SodiumException $e) {
}
return $plain;
}
}
<?php
declare(strict_types=1);
namespace unit\Security;
use Codeception\Test\Unit;
use Exception;
use Library\Security\Cryptography;
use ReflectionProperty;
use Tests\UnitTester;
use TypeError;
class CryptographyTest extends Unit
{
/** @var string */
private $key;
/** @var UnitTester */
protected $tester;
protected function _before()
{
$this->key = base64_encode(sodium_crypto_secretbox_keygen());
}
public function testConstruct()
{
$this->tester->expectThrowable(
new TypeError('Cryptography key expects to be a string encoded in base64.'),
function () {
new Cryptography($this->key . '*');
}
);
}
public function testEncryptAndDecrypt()
{
$cryptography = new Cryptography($this->key);
$message = 'mon message';
$encrypted = $cryptography->encrypt($message);
$this->tester->assertSame($message, $cryptography->decrypt($encrypted));
}
public function testCannotEncryptMessage()
{
$this->tester->expectThrowable(
new Exception('Cannot encrypt message'),
function () {
$cryptography = new Cryptography($this->key);
$property = new ReflectionProperty($cryptography, 'key');
$property->setAccessible(true);
$property->setValue($cryptography, 'wrong_key');
$cryptography->encrypt('mon message');
}
);
}
public function testMessageWasTruncated()
{
$this->tester->expectThrowable(
new Exception('The message was truncated'),
function () {
(new Cryptography($this->key))->decrypt(base64_encode('mon message'));
}
);
}
public function testTheMessageWasTamperedWithInTransitCauseSodiumException()
{
$this->tester->expectThrowable(
new Exception('The message was tampered with in transit'),
function () {
$cryptography = new Cryptography($this->key);
$encrypted = $cryptography->encrypt('mon message');
$property = new ReflectionProperty($cryptography, 'key');
$property->setAccessible(true);
$property->setValue($cryptography, 'wrong_key');
$cryptography->decrypt($encrypted);
}
);
}
public function testTheMessageWasTamperedWithInTransitCauseDecryptHasFailed()
{
$this->tester->expectThrowable(
new Exception('The message was tampered with in transit'),
function () {
$cryptography = new Cryptography($this->key);
$encrypted = $cryptography->encrypt('mon message');
$property = new ReflectionProperty($cryptography, 'key');
$property->setAccessible(true);
$property->setValue($cryptography, sodium_crypto_secretbox_keygen());
$cryptography->decrypt($encrypted);
}
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment