Skip to content

Commit

Permalink
Refactor to the key being an interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mnavarrocarter committed Dec 29, 2020
1 parent 0fc4a77 commit 434131d
Show file tree
Hide file tree
Showing 13 changed files with 515 additions and 139 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ composer require legatus/crypto
<?php

use Legatus\Support\LegatusCipher;
use Legatus\Support\SecretKey;
use Legatus\Support\SodiumKey;

$secret = SecretKey::generate()->getBytes();
$secret = SodiumKey::generate()->getBytes();
$cipher = new LegatusCipher($secret);

$encrypted = $cipher->encrypt('message');
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"autoload": {
"psr-4": {
"Legatus\\Support\\": ["src/cipher", "src/key", "src/rand", "src/token"]
}
},
"files": ["src/functions.php"]
},
"autoload-dev": {
"psr-4": {
Expand Down
4 changes: 2 additions & 2 deletions examples/quick-start.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
declare(strict_types=1);

use Legatus\Support\LegatusCipher;
use Legatus\Support\SecretKey;
use Legatus\Support\SodiumKey;

$secret = SecretKey::generate()->getBytes();
$secret = SodiumKey::generate()->getBytes();
$cipher = new LegatusCipher($secret);

$encrypted = $cipher->encrypt('message');
Expand Down
6 changes: 3 additions & 3 deletions src/cipher/Cipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
namespace Legatus\Support;

/**
* Interface Cipher.
* A Cipher provides the contract to generate a secure, time based, authenticated
* and encoded ciphertexts.
*/
interface Cipher
{
Expand All @@ -34,8 +35,7 @@ public function encrypt(string $plainText): string;
*
* @return string
*
*@throws ExpiredCipher
* @throws InvalidCipher
* @throws ExpiredCipher|InvalidCipher
*/
public function decrypt(string $encrypted, int $ttl = null): string;
}
4 changes: 1 addition & 3 deletions src/cipher/ExpiredCipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

namespace Legatus\Support;

use Exception;

/**
* Class ExpiredCipher.
*/
class ExpiredCipher extends Exception
class ExpiredCipher extends InvalidCipher
{
public function __construct()
{
Expand Down
50 changes: 24 additions & 26 deletions src/cipher/LegatusCipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@

namespace Legatus\Support;

use InvalidArgumentException;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use SodiumException;

/**
* Class LegatusCipher.
*/
final class LegatusCipher implements Cipher
{
private const VERSION = "\x64";
private const MAX_CLOCK_SKEW = 60;
private const MIN_LENGTH = 49;
private const MIN_LENGTH = 48;
private const NONCE_LENGTH = 24;

private string $key;
private SecretKey $key;
private Clock $clock;
private Random $random;

/**
* LegatusCipher constructor.
*
* @param string $key
* @param SecretKey $key
* @param Random|null $random
* @param Clock|null $clock
*/
public function __construct(string $key, Random $random = null, Clock $clock = null)
#*/
public function __construct(SecretKey $key, Random $random = null, Clock $clock = null)
{
$this->key = $key;
$this->clock = $clock ?? new SystemClock();
Expand All @@ -51,16 +51,14 @@ public function __construct(string $key, Random $random = null, Clock $clock = n
* @param string $plainText
*
* @return string
*
* @throws SodiumException
*/
public function encrypt(string $plainText): string
{
$time = $this->getUInt64Time();
$nonce = $this->random->read(SODIUM_CRYPTO_BOX_NONCEBYTES);
$cipher = sodium_crypto_secretbox($plainText, $nonce, $this->key);
$nonce = $this->random->read(self::NONCE_LENGTH);
$cipher = $this->key->encrypt($plainText, $nonce);

return sodium_bin2base64(self::VERSION.$time.$nonce.$cipher, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
return Base64\url_encode($this->key->authenticate($time.$nonce.$cipher));
}

/**
Expand All @@ -70,14 +68,13 @@ public function encrypt(string $plainText): string
* @return string
*
* @throws InvalidCipher
* @throws SodiumException
* @throws ExpiredCipher
*/
public function decrypt(string $encrypted, int $ttl = null): string
{
try {
$decoded = sodium_base642bin($encrypted, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
} catch (SodiumException $e) {
$decoded = Base64\url_decode($encrypted);
} catch (InvalidArgumentException $e) {
throw new InvalidCipher('Invalid base64 encoding');
}

Expand All @@ -86,16 +83,16 @@ public function decrypt(string $encrypted, int $ttl = null): string
throw new InvalidCipher('Cipher too short');
}

$version = $decoded[0];
$time = substr($decoded, 1, 8);
$nonce = substr($decoded, 9, SODIUM_CRYPTO_BOX_NONCEBYTES);
$cipher = substr($decoded, 9 + SODIUM_CRYPTO_BOX_NONCEBYTES);

// We ensure the first byte is 0xa0
if ($version !== self::VERSION) {
throw new InvalidCipher('Incorrect version');
try {
$base = $this->key->verify($decoded);
} catch (InvalidArgumentException $e) {
throw new InvalidCipher('The message has been modified');
}

$time = substr($base, 0, 8);
$nonce = substr($base, 8, self::NONCE_LENGTH);
$cipher = substr($base, 8 + self::NONCE_LENGTH);

// We extract the time and do future and expiration checks
$now = $this->clock->now()->getTimestamp();
$messageTime = $this->getTimestamp($time);
Expand All @@ -109,9 +106,10 @@ public function decrypt(string $encrypted, int $ttl = null): string
throw new InvalidCipher('Too far in the future');
}

$plain = sodium_crypto_secretbox_open($cipher, $nonce, $this->key);
if ($plain === false) {
throw new InvalidCipher('Bad ciphertext');
try {
$plain = $this->key->decrypt($cipher, $nonce);
} catch (InvalidArgumentException $e) {
throw new InvalidCipher('Decryption error: '.$e->getMessage(), $e);
}

return $plain;
Expand Down
53 changes: 53 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

/*
* @project Legatus Crypto
* @link https://github.com/legatus-php/crypto
* @package legatus/crypto
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2021 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Legatus\Support\Base64;

use InvalidArgumentException;
use RuntimeException;
use SodiumException;

/**
* @param string $message
*
* @return string
*
* @throws RuntimeException
*/
function url_encode(string $message): string
{
try {
return sodium_bin2base64($message, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
} catch (SodiumException $e) {
throw new RuntimeException('Could not encode message');
}
}

/**
* @param string $encoded
*
* @return string
*
* @throws InvalidArgumentException
*/
function url_decode(string $encoded): string
{
try {
return sodium_base642bin($encoded, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
} catch (SodiumException $e) {
throw new InvalidArgumentException('Invalid base64 encoded string');
}
}
125 changes: 125 additions & 0 deletions src/key/RotatedKeys.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

/*
* @project Legatus Crypto
* @link https://github.com/legatus-php/crypto
* @package legatus/crypto
* @author Matias Navarro-Carter mnavarrocarter@gmail.com
* @license MIT
* @copyright 2021 Matias Navarro-Carter
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Legatus\Support;

use InvalidArgumentException;

/**
* Class RotatedKeys.
*/
final class RotatedKeys implements SecretKey
{
/**
* @var SecretKey[]
*/
private array $keys;

/**
* RotatedKeys constructor.
*
* @param SecretKey ...$keys
*/
public function __construct(SecretKey ...$keys)
{
$this->keys = $keys;
}

/**
* @param SecretKey $key
*/
public function push(SecretKey $key): void
{
$this->keys[] = $key;
}

/**
* @return SecretKey
*/
public function getCurrentKey(): SecretKey
{
$count = count($this->keys);
if ($count === 0) {
throw new \RuntimeException('There are no keys present');
}

return $this->keys[$count - 1];
}

/**
* @return SecretKey[]
*/
public function getAllKeys(): array
{
return array_reverse($this->keys);
}

/**
* @param string $message
* @param string $nonce
*
* @return string
*/
public function encrypt(string $message, string $nonce): string
{
return $this->getCurrentKey()->encrypt($message, $nonce);
}

/**
* @param string $message
*
* @return string
*/
public function authenticate(string $message): string
{
return $this->getCurrentKey()->authenticate($message);
}

/**
* @param string $authenticatedMessage
*
* @return string
*/
public function verify(string $authenticatedMessage): string
{
foreach ($this->getAllKeys() as $key) {
try {
return $key->verify($authenticatedMessage);
} catch (InvalidArgumentException $e) {
continue;
}
}
throw new InvalidArgumentException('Could not verify message');
}

/**
* @param string $cipher
* @param string $nonce
*
* @return string
*/
public function decrypt(string $cipher, string $nonce): string
{
foreach ($this->getAllKeys() as $key) {
try {
return $key->decrypt($cipher, $nonce);
} catch (InvalidArgumentException $e) {
continue;
}
}
throw new InvalidArgumentException('Could not decrypt message');
}
}

0 comments on commit 434131d

Please sign in to comment.