Created
February 16, 2021 09:25
-
-
Save roukmoute/c5d1ced6a4020ef6f0e790c92fbb23ba to your computer and use it in GitHub Desktop.
LibSodium encrypt/decrypt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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