From e7a0d753764638341d97235ab11e9492a3733a6c Mon Sep 17 00:00:00 2001 From: Matias Navarro Carter Date: Mon, 9 Nov 2020 20:49:28 +0000 Subject: [PATCH] Version 2.0.0 implemented. This simplifies the api a lot and makes it easier to test by using abstractions over randomness and time. --- .github/workflows/main.yml | 44 ------- .github/workflows/pr.yml | 112 ++++++++++++++++++ README.md | 13 +- composer.json | 6 +- functions.php | 67 +++++++++++ src/EncoderInterface.php | 27 ----- src/Fernet.php | 72 ----------- src/KeyInterface.php | 41 ------- src/{DecoderInterface.php => Marshaller.php} | 20 +++- src/Random/EntropyError.php | 19 +++ src/Random/FixedRandomSource.php | 53 +++++++++ src/Random/PhpRandomSource.php | 28 +++++ src/Random/RandomSource.php | 27 +++++ src/UrlSafeBase64.php | 53 --------- src/Version/AbstractVersion.php | 99 ---------------- src/{Version => }/Vx80Key.php | 35 +++--- .../Vx80Version.php => Vx80Marshaller.php} | 78 ++++++++---- tests/FernetDeterministic.php | 52 -------- tests/SpecComplianceAcceptanceTest.php | 33 ++++-- 19 files changed, 425 insertions(+), 454 deletions(-) delete mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pr.yml create mode 100644 functions.php delete mode 100644 src/EncoderInterface.php delete mode 100644 src/Fernet.php delete mode 100644 src/KeyInterface.php rename src/{DecoderInterface.php => Marshaller.php} (55%) create mode 100644 src/Random/EntropyError.php create mode 100644 src/Random/FixedRandomSource.php create mode 100644 src/Random/PhpRandomSource.php create mode 100644 src/Random/RandomSource.php delete mode 100644 src/UrlSafeBase64.php delete mode 100644 src/Version/AbstractVersion.php rename src/{Version => }/Vx80Key.php (80%) rename src/{Version/Vx80Version.php => Vx80Marshaller.php} (54%) delete mode 100644 tests/FernetDeterministic.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index bf746d1..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI - -on: push - -jobs: - test: - runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, 'ci-skip')" - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v1 - with: - php-version: '7.2' - extensions: dom, soap, openssl #optional, setup extensions - coverage: xdebug #optional, setup coverage driver - tools: composer # setup tools globally - - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Load Composer Cache - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer - restore-keys: | - ${{ runner.os }}-composer - - - name: Install Dependencies - run: composer install --prefer-dist - - - name: Test Code Style - run: composer ci:style - - - name: Test Units - run: composer ci:unit - - - name: Test Types - run: composer ci:types diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..f670080 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,112 @@ +name: "Review PR" + +on: + pull_request: + +jobs: + + # This job ensures the coding standard is followed + coding-standards: + name: "Coding Standards" + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + php-version: + - "7.2" + operating-system: + - "ubuntu-latest" + steps: + - name: "Checkout Code" + uses: "actions/checkout@v2" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + - name: "Use Cache" + uses: "actions/cache@v2" + with: + path: | + ~/.composer/cache + vendor + key: "php-${{ matrix.php-version }}-locked" + restore-keys: "php-${{ matrix.php-version }}-locked" + - name: "Install locked dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Coding Standard" + run: "vendor/bin/php-cs-fixer fix --dry-run -vvv" + + type-analysis: + name: "Type Coverage" + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + php-version: + - "7.2" + operating-system: + - "ubuntu-latest" + steps: + - name: "Checkout Code" + uses: "actions/checkout@v2" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + - name: "Use Cache" + uses: "actions/cache@v2" + with: + path: | + ~/.composer/cache + vendor + key: "php-${{ matrix.php-version }}-locked" + restore-keys: "php-${{ matrix.php-version }}-locked" + - name: "Install locked dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Run Psalm" + run: "vendor/bin/psalm --output-format=github --shepherd --stats" + + unit-test: + name: "Unit Testing" + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + dependencies: + - "locked" + - "highest" + php-version: + - "7.2" + - "7.3" + - "7.4" + operating-system: + - "ubuntu-latest" + steps: + - name: "Checkout Code" + uses: "actions/checkout@v2" + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + - name: "Use Cache" + uses: "actions/cache@v2" + with: + path: | + ~/.composer/cache + vendor + key: "php-${{ matrix.php-version }}-locked" + restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" + - name: "Install lowest dependencies" + if: ${{ matrix.dependencies == 'lowest' }} + run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + - name: "Install highest dependencies" + if: ${{ matrix.dependencies == 'highest' }} + run: "composer update --no-interaction --no-progress --no-suggest" + - name: "Install locked dependencies" + if: ${{ matrix.dependencies == 'locked' }} + run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Run PHPUnit" + run: "vendor/bin/phpunit --testdox --coverage-text" \ No newline at end of file diff --git a/README.md b/README.md index 05a2766..12fdb15 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,21 @@ composer require mnavarrocarter/fernet At the moment, there is only one version of Fernet: ```php -use MNC\Fernet\Fernet; +use MNC\Fernet\Vx80Marshaller; +use MNC\Fernet\Vx80Key; require_once __DIR__. '/vendor/autoload.php'; -// Instantiate the version x80 passing the key -$fernet = Fernet::vx80('eLh6lGOYbbHvTHhI-nd_s76mZ7NZi9L5AA_bQNI_KoE'); +// Instantiate a key for version x80 +$key = Vx80Key::fromString('eLh6lGOYbbHvTHhI-nd_s76mZ7NZi9L5AA_bQNI_KoE'); +// Then, create the marshaller +$marshaller = new Vx80Marshaller($key); // Encode a message and get a token -$token = $fernet->encode('hello'); +$token = $marshaller->encode('hello'); // You can then decrypt that token back to the message -$message = $fernet->decode($token); +$message = $marshaller->decode($token); ``` ## What is Fernet? diff --git a/composer.json b/composer.json index b930a93..e73392a 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,14 @@ ], "require": { "php": ">=7.2", - "ext-openssl": "*" + "ext-openssl": "*", + "lcobucci/clock": "^1.2" }, "autoload": { "psr-4": { "MNC\\Fernet\\": "src" - } + }, + "files": ["functions.php"] }, "autoload-dev": { "psr-4": { diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..8384311 --- /dev/null +++ b/functions.php @@ -0,0 +1,67 @@ + - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace MNC\Fernet; - -/** - * Interface EncoderInterface. - */ -interface EncoderInterface -{ - /** - * Encodes a Fernet token from a message. - * - * @param string $message The message to encode - * - * @return string The encoded fernet token - */ - public function encode(string $message): string; -} diff --git a/src/Fernet.php b/src/Fernet.php deleted file mode 100644 index 71a40cf..0000000 --- a/src/Fernet.php +++ /dev/null @@ -1,72 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace MNC\Fernet; - -use MNC\Fernet\Version\Vx80Key; -use MNC\Fernet\Version\Vx80Version; - -/** - * Class Fernet. - */ -class Fernet implements EncoderInterface, DecoderInterface -{ - /** - * @var EncoderInterface - */ - private $encoder; - /** - * @var DecoderInterface - */ - private $decoder; - - /** - * @param string $key - * - * @return Fernet - * - * @throws FernetException - */ - public static function vx80(string $key): Fernet - { - $fernet = new Vx80Version(Vx80Key::fromString($key)); - - return new self($fernet, $fernet); - } - - /** - * Fernet constructor. - * - * @param EncoderInterface $encoder - * @param DecoderInterface $decoder - */ - public function __construct(EncoderInterface $encoder, DecoderInterface $decoder) - { - $this->encoder = $encoder; - $this->decoder = $decoder; - } - - /** - * {@inheritdoc} - */ - public function decode(string $token, int $ttl = null): string - { - return $this->decoder->decode($token); - } - - /** - * {@inheritdoc} - */ - public function encode(string $message): string - { - return $this->encoder->encode($message); - } -} diff --git a/src/KeyInterface.php b/src/KeyInterface.php deleted file mode 100644 index 87c494a..0000000 --- a/src/KeyInterface.php +++ /dev/null @@ -1,41 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace MNC\Fernet; - -/** - * Interface KeyInterface. - */ -interface KeyInterface -{ - /** - * @param string $data - * - * @return string - */ - public function sign(string $data): string; - - /** - * @param string $data - * @param string $iv - * - * @return string - */ - public function encrypt(string $data, string $iv): string; - - /** - * @param string $cipher - * @param string $iv - * - * @return string - */ - public function decrypt(string $cipher, string $iv): string; -} diff --git a/src/DecoderInterface.php b/src/Marshaller.php similarity index 55% rename from src/DecoderInterface.php rename to src/Marshaller.php index 09af834..7429895 100644 --- a/src/DecoderInterface.php +++ b/src/Marshaller.php @@ -1,7 +1,5 @@ @@ -12,17 +10,27 @@ namespace MNC\Fernet; /** - * Interface TokenVerifier. + * A Marshaller is responsible fot managing encoding/decoding of + * tokens according to the Fernet Spec. */ -interface DecoderInterface +interface Marshaller { /** + * Encodes a message into a fernet token. + * + * @param string $message + * + * @return string + */ + public function encode(string $message): string; + + /** + * Decodes a fernet token into the original message. + * * @param string $token * @param int|null $ttl * * @return string - * - * @throws FernetException on token validation error */ public function decode(string $token, int $ttl = null): string; } diff --git a/src/Random/EntropyError.php b/src/Random/EntropyError.php new file mode 100644 index 0000000..cd82dad --- /dev/null +++ b/src/Random/EntropyError.php @@ -0,0 +1,19 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MNC\Fernet\Random; + +use Exception; + +/** + * Class EntropyError. + */ +class EntropyError extends Exception +{ +} diff --git a/src/Random/FixedRandomSource.php b/src/Random/FixedRandomSource.php new file mode 100644 index 0000000..2bad399 --- /dev/null +++ b/src/Random/FixedRandomSource.php @@ -0,0 +1,53 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MNC\Fernet\Random; + +/** + * Class FixedRandomSource. + */ +final class FixedRandomSource implements RandomSource +{ + /** + * @var string + */ + private $random; + + /** + * @param array $array + * + * @return FixedRandomSource + */ + public static function fromUint8Array(array $array): FixedRandomSource + { + return new self(implode('', array_map('chr', $array))); + } + + /** + * FixedRandomSource constructor. + * + * @param string $random + */ + public function __construct(string $random) + { + $this->random = $random; + } + + /** + * {@inheritdoc} + */ + public function read(int $bytes): string + { + if ($bytes !== strlen($this->random)) { + throw new EntropyError('Not the same random length'); + } + + return $this->random; + } +} diff --git a/src/Random/PhpRandomSource.php b/src/Random/PhpRandomSource.php new file mode 100644 index 0000000..a123afc --- /dev/null +++ b/src/Random/PhpRandomSource.php @@ -0,0 +1,28 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MNC\Fernet\Random; + +/** + * Class PhpRandomSource. + */ +final class PhpRandomSource implements RandomSource +{ + /** + * {@inheritdoc} + */ + public function read(int $bytes): string + { + try { + return random_bytes($bytes); + } catch (\Exception $e) { + throw new EntropyError('Not enough entropy', 0, $e); + } + } +} diff --git a/src/Random/RandomSource.php b/src/Random/RandomSource.php new file mode 100644 index 0000000..7696534 --- /dev/null +++ b/src/Random/RandomSource.php @@ -0,0 +1,27 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MNC\Fernet\Random; + +/** + * Interface RandomSource. + */ +interface RandomSource +{ + /** + * Read random bytes from somewhere. + * + * @param int $bytes + * + * @return string + * + * @throws EntropyError if not enough entropy can be gathered for randomness + */ + public function read(int $bytes): string; +} diff --git a/src/UrlSafeBase64.php b/src/UrlSafeBase64.php deleted file mode 100644 index 9b50bde..0000000 --- a/src/UrlSafeBase64.php +++ /dev/null @@ -1,53 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace MNC\Fernet; - -/** - * Class UrlSafeBase64. - */ -final class UrlSafeBase64 -{ - /** - * @param string $data - * - * @return string - */ - public static function encode(string $data): string - { - return str_replace(['+', '/'], ['-', '_'], base64_encode($data)); - } - - /** - * @param string $base64 - * - * @return string - * - * @throws FernetException - */ - public static function decode(string $base64): string - { - $result = base64_decode(str_replace(['-', '_'], ['+', '/'], $base64), true); - if (!is_string($result)) { - throw FernetException::invalidBase64(); - } - - return $result; - } - - private function __construct() - { - } - - private function __clone() - { - } -} diff --git a/src/Version/AbstractVersion.php b/src/Version/AbstractVersion.php deleted file mode 100644 index 00ab81a..0000000 --- a/src/Version/AbstractVersion.php +++ /dev/null @@ -1,99 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace MNC\Fernet\Version; - -use MNC\Fernet\DecoderInterface; -use MNC\Fernet\EncoderInterface; -use MNC\Fernet\FernetException; -use MNC\Fernet\KeyInterface; - -/** - * Class AbstractVersion. - */ -abstract class AbstractVersion implements DecoderInterface, EncoderInterface -{ - /** - * @var KeyInterface - */ - protected $key; - - /** - * AbstractVersion constructor. - * - * @param KeyInterface $key - */ - public function __construct(KeyInterface $key) - { - $this->key = $key; - $this->guard(); - } - - /** - * Generates random 16 bytes. - * - * @return string - */ - protected function iv(): string - { - try { - return random_bytes(16); - } catch (\Exception $e) { - throw new \RuntimeException('Not enough entropy for IV'); - } - } - - /** - * Pads a message to a multiple of 16 bytes. - * - * @param string $message - * - * @return string - */ - protected function pad(string $message): string - { - $pad = 16 - (strlen($message) % 16); - $message .= str_repeat(chr($pad), $pad); - - return $message; - } - - /** - * Removed the padding of a message. - * - * @param string $paddedMessage - * - * @return string - * - * @throws FernetException - */ - protected function unpad(string $paddedMessage): string - { - $pad = ord($paddedMessage[strlen($paddedMessage) - 1]); - if ($pad !== substr_count(substr($paddedMessage, -$pad), chr($pad))) { - throw FernetException::payloadPaddingError(); - } - - return substr($paddedMessage, 0, -$pad); - } - - /** - * Gets the time as a 64 bit unsigned big endian. - * - * @return string - */ - protected function getBinaryTime(): string - { - return pack('NN', 0, time()); - } - - abstract protected function guard(): void; -} diff --git a/src/Version/Vx80Key.php b/src/Vx80Key.php similarity index 80% rename from src/Version/Vx80Key.php rename to src/Vx80Key.php index d341cd6..c5cece8 100644 --- a/src/Version/Vx80Key.php +++ b/src/Vx80Key.php @@ -1,7 +1,5 @@ @@ -9,19 +7,18 @@ * file that was distributed with this source code. */ -namespace MNC\Fernet\Version; +namespace MNC\Fernet; -use Exception; use InvalidArgumentException; -use MNC\Fernet\FernetException; -use MNC\Fernet\KeyInterface; -use MNC\Fernet\UrlSafeBase64; -use RuntimeException; +use MNC\Fernet\Random\PhpRandomSource; +use MNC\Fernet\Random\RandomSource; +use function MNC\Fernet\UrlBase64\decode; +use function MNC\Fernet\UrlBase64\encode; /** * Class Vx80Key. */ -final class Vx80Key implements KeyInterface +class Vx80Key { private const FLAGS = OPENSSL_ZERO_PADDING + OPENSSL_RAW_DATA; @@ -35,15 +32,17 @@ final class Vx80Key implements KeyInterface private $encryptionKey; /** + * @param RandomSource|null $randomSource + * * @return Vx80Key + * + * @throws Random\EntropyError */ - public static function random(): Vx80Key + public static function random(RandomSource $randomSource = null): Vx80Key { - try { - return new self(random_bytes(16), random_bytes(16)); - } catch (Exception $e) { - throw new RuntimeException('Not enough entropy for the token'); - } + $randomSource = $randomSource ?? new PhpRandomSource(); + + return new self($randomSource->read(16), $randomSource->read(16)); } /** @@ -51,11 +50,11 @@ public static function random(): Vx80Key * * @return Vx80Key * - * @throws FernetException + * @throws InvalidArgumentException invalid key provided */ public static function fromString(string $key): Vx80Key { - $bytes = UrlSafeBase64::decode($key); + $bytes = decode($key); if (strlen($bytes) !== 32) { throw new InvalidArgumentException('Invalid key provided. Key must be 32 bytes encoded in base64 (url-safe)'); } @@ -131,6 +130,6 @@ private function guard(): void public function toString(): string { - return UrlSafeBase64::encode($this->signingKey.$this->encryptionKey); + return encode($this->signingKey.$this->encryptionKey); } } diff --git a/src/Version/Vx80Version.php b/src/Vx80Marshaller.php similarity index 54% rename from src/Version/Vx80Version.php rename to src/Vx80Marshaller.php index affdea2..69cd4c7 100644 --- a/src/Version/Vx80Version.php +++ b/src/Vx80Marshaller.php @@ -1,7 +1,5 @@ @@ -9,35 +7,70 @@ * file that was distributed with this source code. */ -namespace MNC\Fernet\Version; +namespace MNC\Fernet; -use InvalidArgumentException; -use MNC\Fernet\FernetException; -use MNC\Fernet\UrlSafeBase64; +use Lcobucci\Clock\Clock; +use Lcobucci\Clock\SystemClock; +use MNC\Fernet\Random\PhpRandomSource; +use MNC\Fernet\Random\RandomSource; +use function MNC\Fernet\Str\pad; +use function MNC\Fernet\Str\unpad; +use function MNC\Fernet\UrlBase64\decode; +use function MNC\Fernet\UrlBase64\encode; /** - * Class Vx80Fernet. + * Class Vx80Marshaller. */ -class Vx80Version extends AbstractVersion +final class Vx80Marshaller implements Marshaller { private const VERSION = "\x80"; private const MIN_LENGTH = 73; private const MAX_CLOCK_SKEW = 60; + private const IV_SIZE = 16; + + /** + * @var Vx80Key + */ + private $key; + /** + * @var Clock + */ + private $clock; + /** + * @var RandomSource + */ + private $randomSource; + + /** + * Vx80Marshaller constructor. + * + * @param Vx80Key $key + * @param Clock|null $clock + * @param RandomSource|null $randomSource + */ + public function __construct(Vx80Key $key, Clock $clock = null, RandomSource $randomSource = null) + { + $this->key = $key; + $this->clock = $clock ?? SystemClock::fromUTC(); + $this->randomSource = $randomSource ?? new PhpRandomSource(); + } /** * @param string $message * * @return string + * + * @throws Random\EntropyError */ public function encode(string $message): string { - $time = $this->getBinaryTime(); - $iv = $this->iv(); - $cipher = $this->key->encrypt($this->pad($message), $iv); + $time = pack('J', $this->clock->now()->getTimestamp()); + $iv = $this->randomSource->read(self::IV_SIZE); + $cipher = $this->key->encrypt(pad($message), $iv); $base = self::VERSION.$time.$iv.$cipher; $hmac = $this->key->sign($base); - return UrlSafeBase64::encode($base.$hmac); + return encode($base.$hmac); } /** @@ -51,7 +84,11 @@ public function encode(string $message): string public function decode(string $token, int $ttl = null): string { // We base64 decode the token - $decoded = UrlSafeBase64::decode($token); + try { + $decoded = decode($token); + } catch (\InvalidArgumentException $exception) { + throw FernetException::invalidBase64(); + } $length = strlen($decoded); @@ -61,7 +98,7 @@ public function decode(string $token, int $ttl = null): string $base = substr($decoded, 0, -32); $version = $base[0]; - $tokenTime = unpack('N/N', substr($base, 1, 8))[1]; + $tokenTime = unpack('J', substr($base, 1, 8))[1]; // We ensure the first byte is 0x80 if ($version !== self::VERSION) { @@ -69,7 +106,7 @@ public function decode(string $token, int $ttl = null): string } // We extract the time and do future and expiration checks - $currentTime = unpack('N/N', $this->getBinaryTime())[1]; + $currentTime = $this->clock->now()->getTimestamp(); $timeDiff = $currentTime - $tokenTime; if ($ttl > 0 && $timeDiff > $ttl) { @@ -93,13 +130,10 @@ public function decode(string $token, int $ttl = null): string $message = $this->key->decrypt($cipher, $iv); // Unpad decrypted, returning original message - return $this->unpad($message); - } - - final protected function guard(): void - { - if (!$this->key instanceof Vx80Key) { - throw new InvalidArgumentException('Invalid key for Fernet 0x80 version'); + try { + return unpad($message); + } catch (\InvalidArgumentException $exception) { + throw FernetException::payloadPaddingError(); } } } diff --git a/tests/FernetDeterministic.php b/tests/FernetDeterministic.php deleted file mode 100644 index 185ecf7..0000000 --- a/tests/FernetDeterministic.php +++ /dev/null @@ -1,52 +0,0 @@ -key = $key; - $this->time = $time; - $this->iv = $iv; - } - - /** - * @return string - */ - protected function iv(): string - { - return implode(array_map('chr', $this->iv)); - } - - protected function getBinaryTime(): string - { - return pack('NN', 0, $this->time->getTimestamp()); - } -} \ No newline at end of file diff --git a/tests/SpecComplianceAcceptanceTest.php b/tests/SpecComplianceAcceptanceTest.php index fd93c08..581ff61 100644 --- a/tests/SpecComplianceAcceptanceTest.php +++ b/tests/SpecComplianceAcceptanceTest.php @@ -3,8 +3,12 @@ namespace MNC\Fernet\Tests; +use DateTimeImmutable; +use Lcobucci\Clock\FrozenClock; use MNC\Fernet\FernetException; -use MNC\Fernet\Version\Vx80Key; +use MNC\Fernet\Random\FixedRandomSource; +use MNC\Fernet\Vx80Key; +use MNC\Fernet\Vx80Marshaller; use PHPUnit\Framework\TestCase; /** @@ -24,11 +28,12 @@ public function testDeterministicGeneration(): void $tests = $this->readJson(__DIR__ . '/generate.json'); foreach ($tests as $test) { $key = Vx80Key::fromString($test['secret']); - $time = \DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now']); - $fernet = new FernetDeterministic($key, $time, $test['iv'] ?? []); + $clock = new FrozenClock(DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now'])); + $random = FixedRandomSource::fromUint8Array($test['iv']); + $marshaller = new Vx80Marshaller($key, $clock, $random); - $token = $fernet->encode($test['src']); - $this->assertSame($test['token'], $token); + $token = $marshaller->encode($test['src']); + self::assertSame($test['token'], $token); } } @@ -37,13 +42,14 @@ public function testDeterministicInvalid(): void $tests = $this->readJson(__DIR__ . '/invalid.json'); foreach ($tests as $test) { $key = Vx80Key::fromString($test['secret']); - $time = \DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now']); - $fernet = new FernetDeterministic($key, $time, $test['iv'] ?? []); + $clock = new FrozenClock(DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now'])); + $random = FixedRandomSource::fromUint8Array($test['iv'] ?? []); + $marshaller = new Vx80Marshaller($key, $clock, $random); try { - $fernet->decode($test['token'], $test['ttl_sec']); + $marshaller->decode($test['token'], $test['ttl_sec']); } catch (FernetException $e) { - $this->assertInstanceOf(FernetException::class, $e); + self::assertInstanceOf(FernetException::class, $e); } } } @@ -53,11 +59,12 @@ public function testDeterministicVerify(): void $tests = $this->readJson(__DIR__ . '/verify.json'); foreach ($tests as $test) { $key = Vx80Key::fromString($test['secret']); - $time = \DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now']); - $fernet = new FernetDeterministic($key, $time, $test['iv'] ?? []); + $clock = new FrozenClock(DateTimeImmutable::createFromFormat(DATE_ATOM, $test['now'])); + $random = FixedRandomSource::fromUint8Array($test['iv'] ?? []); + $marshaller = new Vx80Marshaller($key, $clock, $random); - $message = $fernet->decode($test['token'], $test['ttl_sec']); - $this->assertSame($test['src'], $message); + $message = $marshaller->decode($test['token'], $test['ttl_sec']); + self::assertSame($test['src'], $message); } }