From fbfe41d0ee3f6f32c3911ab6edc6958660339411 Mon Sep 17 00:00:00 2001 From: omatamix <47761650+omatamix@users.noreply.github.com> Date: Tue, 19 Feb 2019 16:12:30 -0600 Subject: [PATCH 001/134] Bump version. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 877d3e86..2875b239 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ }, "require-dev": { "phpunit/phpunit": "^7", - "vimeo/psalm": "^1|^2" + "vimeo/psalm": "^3" }, "support": { "docs": "https://github.com/paragonie/halite/tree/master/doc" From 164b02fdcb3da3899281a2ae11ea005e96ada89a Mon Sep 17 00:00:00 2001 From: omatamix <47761650+omatamix@users.noreply.github.com> Date: Tue, 19 Feb 2019 21:28:51 -0600 Subject: [PATCH 002/134] Removed in phpunit 7. --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5627e4a4..affc6cbd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,7 +12,6 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" forceCoversAnnotation="false" - mapTestClassNameToCoveredClassName="false" processIsolation="false" stopOnError="false" stopOnFailure="false" From ca1ac7a483bb13fb8f79a2045db2ce3dc5c19f7a Mon Sep 17 00:00:00 2001 From: omatamix <47761650+omatamix@users.noreply.github.com> Date: Mon, 11 Mar 2019 15:17:51 -0500 Subject: [PATCH 003/134] Change phpunit xml scheme to 8.0. --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index affc6cbd..05248df2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ Date: Mon, 11 Mar 2019 15:20:04 -0500 Subject: [PATCH 004/134] Bump phpunit version. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2875b239..62f0112d 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8", "vimeo/psalm": "^3" }, "support": { From 39ef9f0b7cea471529234c9708a405c9d94ec79e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 11 Mar 2019 16:35:29 -0400 Subject: [PATCH 005/134] Fix Psalm --- psalm.xml | 1 + src/File.php | 9 +++++++-- src/Stream/ReadOnlyFile.php | 1 - src/Util.php | 5 +++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/psalm.xml b/psalm.xml index a3d39b55..d9b9a9eb 100644 --- a/psalm.xml +++ b/psalm.xml @@ -9,5 +9,6 @@ + diff --git a/src/File.php b/src/File.php index 719cdfec..239be2b7 100644 --- a/src/File.php +++ b/src/File.php @@ -1205,6 +1205,7 @@ protected static function splitKeys( * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified + * @throws \SodiumException * @throws \TypeError */ final private static function streamEncrypt( @@ -1235,7 +1236,9 @@ final private static function streamEncrypt( $written += $output->writeBytes($encrypted); \sodium_increment($nonce); } - \sodium_memzero($nonce); + if (\is_string($nonce)) { + \sodium_memzero($nonce); + } // Check that our input file was not modified before we MAC it if (!\hash_equals($input->getHash(), $initHash)) { @@ -1338,7 +1341,9 @@ final private static function streamDecrypt( $output->writeBytes($decrypted); \sodium_increment($nonce); } - \sodium_memzero($nonce); + if (\is_string($nonce)) { + \sodium_memzero($nonce); + } return true; } diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 7dd0e143..18ae74af 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -145,7 +145,6 @@ public function close(): void * Calculate a BLAKE2b hash of a file * * @return string - * @throws */ public function getHash(): string { diff --git a/src/Util.php b/src/Util.php index 57fc5e17..7e27f05f 100644 --- a/src/Util.php +++ b/src/Util.php @@ -65,6 +65,7 @@ public static function chrToInt(string $chr): int * @param int $length * @return string * @throws CannotPerformOperation + * @throws \SodiumException * @throws \TypeError */ public static function hash( @@ -85,6 +86,7 @@ public static function hash( * @param int $length * @return string * @throws CannotPerformOperation + * @throws \SodiumException */ public static function raw_hash( string $input, @@ -111,6 +113,7 @@ public static function raw_hash( * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws \TypeError + * @throws \SodiumException */ public static function hkdfBlake2b( string $ikm, @@ -204,6 +207,7 @@ public static function intToChr(int $int): string * @return string * @throws CannotPerformOperation * @throws \TypeError + * @throws \SodiumException */ public static function keyed_hash( string $input, @@ -226,6 +230,7 @@ public static function keyed_hash( * @param int $length * @return string * @throws CannotPerformOperation + * @throws \SodiumException */ public static function raw_keyed_hash( string $input, From 0160ecb97046e9634062b2a29ff27f84095f8819 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 11 Mar 2019 16:36:24 -0400 Subject: [PATCH 006/134] Update README with link to halite-legacy. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a51efe71..9aff86f2 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ If your company requires support for an older version of Halite, [contact Paragon Initiative Enterprises](https://paragonie.com/contact) to inquire about commercial support options. +If you need an easy way to migrate from older versions of Halite, check out [halite-legacy](https://github.com/paragonie/halite-legacy). + ## Using Halite in Your Project Check out the [documentation](doc). The basic Halite API is designed for simplicity: From 56ccdcac236259208a19ecb0231791db8da2791e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 11 Mar 2019 16:38:32 -0400 Subject: [PATCH 007/134] Update CHANGELOG for v4.5.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca47cdb9..a64f7434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 4.5.3 (2019-03-11) + +* Fixed some minor nuisances with Psalm and PHPUnit. +* Added reference to Halite-Legacy to the README. +* Updated docblocks. + ## Version 4.5.2 (2019-02-11) * Fixed [#116](https://github.com/paragonie/halite/issues/116). If the output file From a66199d4fd569296c5871216e103f52acde8fb9b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 11 Mar 2019 16:41:33 -0400 Subject: [PATCH 008/134] Add void typ declaration to setUp method --- test/unit/FileTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 38070855..004d01de 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -16,7 +16,8 @@ final class FileTest extends TestCase { - public function setUp() + + public function setUp(): void { chmod(__DIR__.'/tmp/', 0777); } From 464e0ce233190c60635f2820349d3c8394a3dde3 Mon Sep 17 00:00:00 2001 From: omatamix <47761650+omatamix@users.noreply.github.com> Date: Mon, 11 Mar 2019 19:56:59 -0500 Subject: [PATCH 009/134] Extends throwable to ensure all exceptions are throwable. --- src/Alerts/HaliteAlertInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Alerts/HaliteAlertInterface.php b/src/Alerts/HaliteAlertInterface.php index c2ee1472..0e8bc6fe 100644 --- a/src/Alerts/HaliteAlertInterface.php +++ b/src/Alerts/HaliteAlertInterface.php @@ -15,6 +15,6 @@ /** * HaliteAlertInterface */ -interface HaliteAlertInterface +interface HaliteAlertInterface extends \Throwable { } From d8f9a1bdb0323a90e6141df7eb6e10cae94e5724 Mon Sep 17 00:00:00 2001 From: elliot sawyer <354793+elliot-sawyer@users.noreply.github.com> Date: Sun, 5 May 2019 21:45:40 +1200 Subject: [PATCH 010/134] Docs: File::encryptData and File::decryptData The latest version of Halite contains updated method names --- doc/Features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/Features.md b/doc/Features.md index c5aa8c65..d2446147 100644 --- a/doc/Features.md +++ b/doc/Features.md @@ -85,7 +85,7 @@ $keyed_checksum = \ParagonIE\Halite\File::checksum('/source/file/path', null, tr If you need to encrypt a file larger than the amount of memory available to PHP, you'll run into problems with just the basic `\ParagonIE\Halite\Symmetric\Crypto` -API. To work around these limitations, use `File::encryptFile()` instead. +API. To work around these limitations, use `File::encryptData()` instead. For example: @@ -103,7 +103,7 @@ its contents to `$outputFilename`. Decryption is straightforward as well: ```php -\ParagonIE\Halite\File::decryptFile( +\ParagonIE\Halite\File::decryptData( $inputFilename, $outputFilename, $enc_key From a63ab77898da4e4a39ef268e13e4dca49667e0a6 Mon Sep 17 00:00:00 2001 From: Christiaan Baartse Date: Tue, 14 May 2019 16:12:43 +0200 Subject: [PATCH 011/134] Decrypting a file to php://output is not allowed by MutableFile It complains that 'wb' is no valid mode, however when wb is added all tests still pass. --- src/Stream/MutableFile.php | 2 +- test/unit/FileTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index d4e9de6d..c5c8342a 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -28,7 +28,7 @@ */ class MutableFile implements StreamInterface { - const ALLOWED_MODES = ['r+b', 'w+b', 'cb', 'c+b']; + const ALLOWED_MODES = ['r+b', 'w+b', 'cb', 'c+b', 'wb']; const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default /** diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 004d01de..1dca937b 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -715,4 +715,35 @@ public function testNonExistingOutputFile() ); $this->assertTrue(\file_exists(__DIR__.'/tmp/empty116.encrypted.txt')); } + + public function testOutputToOutputbuffer() + { + $stream = fopen('php://output', 'wb'); + + touch(__DIR__.'/tmp/paragon_avatar.encrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.encrypted.png', 0777); + + $key = new EncryptionKey( + new HiddenString(\str_repeat('B', 32)) + ); + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.encrypted.png', + $key + ); + + ob_start(); + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted.png', + $stream, + $key + ); + $contents = ob_get_clean(); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash('sha256', $contents) + ); + unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png'); + } } From b535780ef0be5250430b92d40805321dda5438a9 Mon Sep 17 00:00:00 2001 From: elliot sawyer <354793+elliot-sawyer@users.noreply.github.com> Date: Mon, 27 May 2019 11:39:07 +1200 Subject: [PATCH 012/134] use File::encrypt and File::decrypt --- doc/Features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/Features.md b/doc/Features.md index d2446147..0e1ffcdd 100644 --- a/doc/Features.md +++ b/doc/Features.md @@ -85,7 +85,7 @@ $keyed_checksum = \ParagonIE\Halite\File::checksum('/source/file/path', null, tr If you need to encrypt a file larger than the amount of memory available to PHP, you'll run into problems with just the basic `\ParagonIE\Halite\Symmetric\Crypto` -API. To work around these limitations, use `File::encryptData()` instead. +API. To work around these limitations, use `File::encrypt()` instead. For example: @@ -103,7 +103,7 @@ its contents to `$outputFilename`. Decryption is straightforward as well: ```php -\ParagonIE\Halite\File::decryptData( +\ParagonIE\Halite\File::decrypt( $inputFilename, $outputFilename, $enc_key From 96a181620cf9cf275c734049ff02070b6f9b4015 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:39:53 -0400 Subject: [PATCH 013/134] Add PHP 7.4 to unit tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a5bb1317..e3d7b4cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ dist: trusty php: - "7.2" - "7.3" + - "7.4" - "master" - "nightly" matrix: From 0617d7ade6ded8160f53e7305a124d3864d4dd9e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:40:10 -0400 Subject: [PATCH 014/134] Update CHANGELOG for 4.5.4 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64f7434..b6751c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Version 4.5.4 (2019-06-05) + +* Merged [#132](https://github.com/paragonie/halite/pull/132), which ensures + all Halite exceptions implement `Throwable`. +* Merged [#133](https://github.com/paragonie/halite/pull/133), which updates + the documentation for the `File` API. + Thanks [@elliot-sawyer](https://github.com/elliot-sawyer). +* Merged [#134](https://github.com/paragonie/halite/pull/134), which allows + `MutableFile` to be used on resources opened in `wb` mode. + Thanks [@christiaanbaartse](christiaanbaartse). + + ## Version 4.5.3 (2019-03-11) * Fixed some minor nuisances with Psalm and PHPUnit. From 3807734dad7eee9cb5bd0e7d6647d6b0fef8e198 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:45:35 -0400 Subject: [PATCH 015/134] PHP 7.4 isn't available on Travis yet. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e3d7b4cb..a5bb1317 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ dist: trusty php: - "7.2" - "7.3" - - "7.4" - "master" - "nightly" matrix: From ee3f51ff07ac9a770d2fe878dcca72328d2a678e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:47:29 -0400 Subject: [PATCH 016/134] Update link for HiddenString --- doc/Basic.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Basic.md b/doc/Basic.md index 057b6d96..a3e964a5 100644 --- a/doc/Basic.md +++ b/doc/Basic.md @@ -41,7 +41,7 @@ For authentication functions, Halite will typically just return `false`. ## Encryption Encryption functions expect your message to be encapsulated in an instance -of the [`HiddenString`](Classes/HiddenString.md) class. Decryption functions +of the [`HiddenString`](https://github.com/paragonie/hidden-string) class. Decryption functions will return the decrypted plaintext in a `HiddenString` object. ### Symmetric-Key Encryption From 8f224d0f17383e5ef3fedf56cf1fa711f4944c1e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:50:45 -0400 Subject: [PATCH 017/134] Cleanup examples a little. Possibly needs a rewrite in the future. --- doc/Basic.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/Basic.md b/doc/Basic.md index a3e964a5..39724878 100644 --- a/doc/Basic.md +++ b/doc/Basic.md @@ -50,20 +50,21 @@ First, you'll need is an encryption key. The easiest way to obtain one is to generate it: ```php -$enc_key = \ParagonIE\Halite\KeyFactory::generateEncryptionKey(); +use ParagonIE\Halite\KeyFactory; +$enc_key = KeyFactory::generateEncryptionKey(); ``` This generates a strong random key. If you'd like to reuse it, you can simply store it in a file. ```php -\ParagonIE\Halite\KeyFactory::save($enc_key, '/path/to/encryption.key'); +KeyFactory::save($enc_key, '/path/to/encryption.key'); ``` Later, you can load it like so: ```php -$enc_key = \ParagonIE\Halite\KeyFactory::loadEncryptionKey('/path/to/encryption.key'); +$enc_key = KeyFactory::loadEncryptionKey('/path/to/encryption.key'); ``` Or if you want to store it in a string @@ -83,6 +84,8 @@ $enc_key = KeyFactory::importEncryptionKey(new HiddenString($key_hex)); **Encryption** should be rather straightforward: ```php +use ParagonIE\HiddenString\HiddenString; + $ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encrypt( new HiddenString( "Your message here. Any string content will do just fine." @@ -128,6 +131,8 @@ $send_to_bob = sodium_bin2hex($alice_public->getRawKeyMaterial()); Alice will then load Bob's public key into the appropriate object like so: ```php +use ParagonIE\HiddenString\HiddenString; + $bob_public = new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey( new HiddenString( sodium_hex2bin($recv_from_bob) @@ -183,6 +188,8 @@ You want to only keep `$seal_public` stored outside of the trusted environment. **Encrypting** an anonymous message: ```php +use ParagonIE\HiddenString\HiddenString; + $sealed = \ParagonIE\Halite\Asymmetric\Crypto::seal( new HiddenString( "Your message here. Any string content will do just fine." From e571a26b9da1e123d2221ecac4b9b98b15e20b8f Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 5 Jun 2019 11:52:53 -0400 Subject: [PATCH 018/134] Release v4.5.4 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6751c3a..ec2b0204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ * Merged [#134](https://github.com/paragonie/halite/pull/134), which allows `MutableFile` to be used on resources opened in `wb` mode. Thanks [@christiaanbaartse](christiaanbaartse). - +* Other minor documentation improvements. ## Version 4.5.3 (2019-03-11) From e249216bbe916ad5973eb9ec1784543e53238c91 Mon Sep 17 00:00:00 2001 From: Padraig Doran Date: Tue, 25 Jun 2019 17:16:32 +0100 Subject: [PATCH 019/134] Update Sodium.stub.php Correct param name for hex2bin() function --- stub/Sodium.stub.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stub/Sodium.stub.php b/stub/Sodium.stub.php index aaac7def..fd199ce9 100644 --- a/stub/Sodium.stub.php +++ b/stub/Sodium.stub.php @@ -967,14 +967,14 @@ function compare( /** * Convert from hex without side-chanels * - * @param string $binary + * @param string $hex * @return string */ function hex2bin( - string $binary + string $hex ): string { if (\extension_loaded('sodium')) { - return \sodium_hex2bin($binary); + return \sodium_hex2bin($hex); } return ''; } @@ -1089,4 +1089,4 @@ function crypto_scalarmult_base( } return ''; } -} \ No newline at end of file +} From 1a3020020356a6653c79bc30813e22bc4c6036d0 Mon Sep 17 00:00:00 2001 From: Padraig Doran Date: Tue, 25 Jun 2019 17:19:10 +0100 Subject: [PATCH 020/134] Update sodium_stub.php Correct param name for sodium_hex2bin() function --- stub/sodium_stub.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stub/sodium_stub.php b/stub/sodium_stub.php index 46beb953..621b6166 100644 --- a/stub/sodium_stub.php +++ b/stub/sodium_stub.php @@ -970,14 +970,14 @@ function sodium_compare( /** * Convert from hex without side-chanels * - * @param string $binary + * @param string $hex * @return string */ function sodium_hex2bin( - string $binary + string $hex ): string { if (\extension_loaded('libsodium')) { - return \Sodium\hex2bin($binary); + return \Sodium\hex2bin($hex); } return ''; } @@ -1092,4 +1092,4 @@ function sodium_crypto_scalarmult_base( } return ''; } -} \ No newline at end of file +} From b9d54d53979658706f6b406d8c72d080492b277d Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 2 Aug 2019 11:17:15 +1000 Subject: [PATCH 021/134] Support stream wrappers that don't support \fstat() like php://input --- src/Stream/ReadOnlyFile.php | 44 +++++++++++++++------ test/unit/FileTest.php | 47 ++++++++++++++++++++++ test/unit/RemoteStream.php | 79 +++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 test/unit/RemoteStream.php diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 18ae74af..980344a0 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -95,7 +95,7 @@ public function __construct($file, Key $key = null) $this->closeAfter = true; $this->pos = 0; - $this->stat = \fstat($this->fp); + $this->stat = $this->fstat(); } elseif (\is_resource($file)) { /** @var array $metadata */ $metadata = \stream_get_meta_data($file); @@ -106,7 +106,7 @@ public function __construct($file, Key $key = null) } $this->fp = $file; $this->pos = \ftell($this->fp); - $this->stat = \fstat($this->fp); + $this->stat = $this->fstat(); } else { throw new InvalidType( 'Argument 1: Expected a filename or resource' @@ -176,7 +176,7 @@ public function getHash(): string /** * Where are we in the buffer? - * + * * @return int */ public function getPos(): int @@ -186,7 +186,7 @@ public function getPos(): int /** * How big is this buffer? - * + * * @return int */ public function getSize(): int @@ -203,13 +203,13 @@ public function getStreamMetadata(): array { return \stream_get_meta_data($this->fp); } - + /** * Read from a stream; prevent partial reads (also uses run-time testing to * prevent partial reads -- you can turn this off if you need performance * and aren't concerned about race condition attacks, but this isn't a * decision to make lightly!) - * + * * @param int $num * @param bool $skipTests Only set this to TRUE if you're absolutely sure * that you don't want to defend against TOCTOU / @@ -258,10 +258,10 @@ public function readBytes(int $num, bool $skipTests = false): string } while ($remaining > 0); return $buf; } - + /** * Get number of bytes remaining - * + * * @return int */ public function remainingBytes(): int @@ -310,17 +310,17 @@ public function toctouTest() ); // @codeCoverageIgnoreEnd } - $stat = \fstat($this->fp); + $stat = $this->fstat(); if ($stat['size'] !== $this->stat['size']) { throw new FileModified( 'Read-only file has been modified since it was opened for reading' ); } } - + /** * This is a meaningless operation for a Read-Only File! - * + * * @param string $buf * @param int $num (number of bytes) * @return int @@ -334,4 +334,26 @@ public function writeBytes(string $buf, int $num = null): int 'This is a read-only file handle.' ); } + + /** + * Wraps fstat to allow calculation of file-size on stream wrappers. + * + * @return array + */ + private function fstat() : array { + $stat = \fstat($this->fp); + if ($stat) { + return $stat; + } + // The resource is remote or a stream wrapper like php://input + $stat = [ + 'size' => 0, + ]; + \fseek($this->fp, 0); + while (!feof($this->fp)) { + $stat['size'] += \strlen(\fread($this->fp, 8192)); + } + \fseek($this->fp, $this->pos); + return $stat; + } } diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 1dca937b..5396729a 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -338,6 +338,53 @@ public function testSeal() unlink(__DIR__.'/tmp/paragon_avatar.opened.png'); } + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws Exception + * @throws TypeError + */ + public function testSealFromStreamWrapper() + { + require_once __DIR__ . '/RemoteStream.php'; + stream_register_wrapper('haliteTest', RemoteStream::class); + touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); + chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.opened.png'); + chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777); + + $keypair = KeyFactory::generateEncryptionKeyPair(); + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + + $file = new ReadOnlyFile(fopen('haliteTest://paragon_avatar.png', 'rb')); + File::seal( + $file, + __DIR__.'/tmp/paragon_avatar.sealed.png', + $publickey + ); + + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed.png', + __DIR__.'/tmp/paragon_avatar.opened.png', + $secretkey + ); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png') + ); + + unlink(__DIR__.'/tmp/paragon_avatar.sealed.png'); + unlink(__DIR__.'/tmp/paragon_avatar.opened.png'); + $this->assertEquals($file->getHash(), (new ReadOnlyFile(__DIR__.'/tmp/paragon_avatar.png'))->getHash()); + } + /** * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileAccessDenied diff --git a/test/unit/RemoteStream.php b/test/unit/RemoteStream.php new file mode 100644 index 00000000..00b18278 --- /dev/null +++ b/test/unit/RemoteStream.php @@ -0,0 +1,79 @@ +contents = \file_get_contents(__DIR__ . '/tmp/' . parse_url($path, PHP_URL_HOST)); + return true; + } + + function stream_read($count) + { + $return = \substr($this->contents, $this->position, $count); + $this->position += strlen($return); + return $return; + } + + function stream_write($data) + { + return false; + } + + function stream_tell() + { + return $this->position; + } + + function stream_eof() + { + return $this->position >= \strlen($this->contents); + } + + function stream_seek($offset, $whence) + { + switch ($whence) { + case SEEK_SET: + if ($offset < strlen($this->contents) && $offset >= 0) { + $this->position = $offset; + return true; + } + return false; + + case SEEK_CUR: + if ($offset >= 0) { + $this->position += $offset; + return true; + } + return false; + + case SEEK_END: + if (strlen($this->contents) + $offset >= 0) { + $this->position = strlen($this->contents) + $offset; + return true; + } + return false; + + default: + return false; + } + } + + function stream_metadata($path, $option, $var) + { + return false; + } + + function stream_stat() + { + return false; + } + +} From 72d226cd8a77d30ecd49e7154bd596521ccb1a54 Mon Sep 17 00:00:00 2001 From: Lee Rowlands Date: Fri, 9 Aug 2019 10:18:07 +1000 Subject: [PATCH 022/134] Don't recalculate hash if it exists and file is unmodified --- src/Stream/ReadOnlyFile.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 18ae74af..6258a7ce 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -148,6 +148,10 @@ public function close(): void */ public function getHash(): string { + if ($this->hash) { + $this->toctouTest(); + return $this->hash; + } $init = $this->pos; \fseek($this->fp, 0, SEEK_SET); From 67f93796fcf8596539de3185c7912872c5d1c9e8 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 12 Sep 2019 07:42:20 -0400 Subject: [PATCH 023/134] Update minimum sodium_compat --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 62f0112d..55983d8c 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "php": "^7.2", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1", - "paragonie/sodium_compat": "^1.6" + "paragonie/sodium_compat": "^1.11" }, "autoload": { "psr-4": { From 7b96cdcf355fc7d09c58ee602ba22724bc8545d3 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 12 Sep 2019 07:49:55 -0400 Subject: [PATCH 024/134] Update changelog for 4.6.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2b0204..abd2ce36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Version 4.6.0 (2019-09-12) + +* Merged [#138](https://github.com/paragonie/halite/pull/138), which adds + remote stream support to `ReadOnlyFile`. +* Merged [#140](https://github.com/paragonie/halite/pull/140), which saves + some overhead on hash recalculation. +* Merged [#136](https://github.com/paragonie/halite/pull/136) and + [#137](https://github.com/paragonie/halite/pull/137), which updated the + sodium stub files. These aren't strictly necessary anymore; with the + adoption of libsodium in PHP 7.2 and sodium_compat, most IDEs autocomplete + correctly. But fixing nits is always appreciated. +* Update minimum sodium_compat to v1.11.0. + ## Version 4.5.4 (2019-06-05) * Merged [#132](https://github.com/paragonie/halite/pull/132), which ensures From c51a3ca4f971f0e893f254445fbaa9196d4089e2 Mon Sep 17 00:00:00 2001 From: Klemens Ullmann-Marx Date: Mon, 27 Jan 2020 12:12:41 +0100 Subject: [PATCH 025/134] Fixed copy/paste error in method docblock --- src/Cookie.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cookie.php b/src/Cookie.php index 3436bd37..300e8fd4 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -67,7 +67,7 @@ public function __debugInfo() } /** - * Store a value in an encrypted cookie + * Fetch a value from an encrypted cookie * * @param string $name * @return mixed|null (typically an array) From 6b21367ecd02c6aa844ad791a0d985b953aa2550 Mon Sep 17 00:00:00 2001 From: "Paragon Initiative Enterprises, LLC" Date: Sat, 1 Feb 2020 23:00:10 -0500 Subject: [PATCH 026/134] Fix Travis CI --- .travis.yml | 1 + composer.json | 3 +++ src/Halite.php | 2 ++ test/unit/StreamTest.php | 14 +++++++++----- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5bb1317..e3d7b4cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ dist: trusty php: - "7.2" - "7.3" + - "7.4" - "master" - "nightly" matrix: diff --git a/composer.json b/composer.json index 55983d8c..e7beaa23 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,9 @@ "phpunit/phpunit": "^8", "vimeo/psalm": "^3" }, + "scripts": { + "test": "phpunit && psalm" + }, "support": { "docs": "https://github.com/paragonie/halite/tree/master/doc" }, diff --git a/src/Halite.php b/src/Halite.php index f5fa60d5..9bb37afe 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -72,6 +72,8 @@ final private function __construct() * @param bool $decode * @return callable|null * @throws InvalidType + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType */ public static function chooseEncoder($chosen, bool $decode = false) { diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index dfe54c6f..9703e1e6 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -20,7 +20,7 @@ final class StreamTest extends TestCase */ public function testFileHash() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -46,13 +46,17 @@ public function testFileHash() */ public function testUnreadableFile() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); chmod($filename, 0000); try { new ReadOnlyFile($filename); + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Windows permissions are weird.'); + return; + } $this->fail('File should not be readable'); } catch (CryptoException\FileAccessDenied $ex) { $this->assertSame('Could not open file for reading', $ex->getMessage()); @@ -90,7 +94,7 @@ public function testUnreadableFile() */ public function testResource() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -142,7 +146,7 @@ public function testResource() */ public function testFileRead() { - $filename = tempnam('/tmp', 'x'); + $filename = @tempnam('/tmp', 'x'); $buf = random_bytes(65537); file_put_contents($filename, $buf); @@ -189,7 +193,7 @@ public function testFileRead() foreach ([255, 65537] as $size) { $buffer = random_bytes($size); - $fileWrite = tempnam('/tmp', 'x'); + $fileWrite = @tempnam('/tmp', 'x'); $mStream = new MutableFile($fileWrite); $mStream->writeBytes($buffer); $mStream->reset(0); From 1e6a93e3dc5f704c53b9949cc9796883c8c701a3 Mon Sep 17 00:00:00 2001 From: Alex Korn Date: Sat, 20 Jun 2020 21:45:28 -0500 Subject: [PATCH 027/134] Issue #153: Add support for SameSite cookie flag Add support for SameSite cookie flag in PHP >= 7.3.0. I split this out as a separate if statement to keep existing functionality in 7.2.x as similar as possible. When dropping support for 7.2.x, the idea would be to turn the method into just what's in side the version-comparing `if`. --- src/Cookie.php | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Cookie.php b/src/Cookie.php index 300e8fd4..9303e797 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -140,6 +140,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @param string $domain (defaults to NULL) * @param bool $secure (defaults to TRUE) * @param bool $httpOnly (defaults to TRUE) + * @param string $samesite (defaults to ''; PHP >= 7.3.0) * @return bool * * @throws InvalidDigestLength @@ -156,16 +157,34 @@ public function store( string $path = '/', string $domain = '', bool $secure = true, - bool $httpOnly = true + bool $httpOnly = true, + string $sameSite = '' ): bool { + $val = Crypto::encrypt( + new HiddenString( + (string) \json_encode($value) + ), + $this->key + ); + if (\version_compare(PHP_VERSION, '7.3.0') >= 0) { + $options = [ + 'expires' => (int) $expire, + 'path' => (string) $path, + 'domain' => (string) $domain, + 'secure' => (bool) $secure, + 'httponly' => (bool) $httpOnly, + ]; + if ($sameSite !== '') { + $options['samesite'] = (string) $sameSite; + } + return \setcookie( + $name, + $val, + $options); + } return \setcookie( $name, - Crypto::encrypt( - new HiddenString( - (string) \json_encode($value) - ), - $this->key - ), + $val, (int) $expire, (string) $path, (string) $domain, From ebfc04034ecafecbdd846d08f0129302fb310f8a Mon Sep 17 00:00:00 2001 From: Padraig Doran Date: Wed, 5 Aug 2020 23:04:22 +0100 Subject: [PATCH 028/134] Updated comment to refer to Argon2id, not Argon2i --- src/Password.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Password.php b/src/Password.php index 6443f930..ebded383 100644 --- a/src/Password.php +++ b/src/Password.php @@ -111,7 +111,7 @@ public static function needsRehash( $config->ENCODING )->getString(); - // Upon successful decryption, verify that we're using Argon2i + // Upon successful decryption, verify that we're using Argon2id if (!\hash_equals( Binary::safeSubstr($hash_str, 0, 10), \SODIUM_CRYPTO_PWHASH_STRPREFIX From dbc6eea055ec68997f987ff2e20b49a721b14546 Mon Sep 17 00:00:00 2001 From: Jose Diaz Date: Thu, 27 Aug 2020 11:47:13 +0100 Subject: [PATCH 029/134] Change 'Cryptographic doom principle' link https://moxie.org/blog/the-cryptographic-doom-principle throws 404 --- doc/Primitives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Primitives.md b/doc/Primitives.md index cc61b8fe..b7d04d7d 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -8,4 +8,4 @@ * Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md) * Password-Based Key Derivation: [**Argon2**](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str) -In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](http://www.thoughtcrime.org/blog/the-cryptographic-doom-principle). +In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). From b06175665519aff97c43d9b6df3abb935f680a3b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 10:34:49 -0500 Subject: [PATCH 030/134] Update to support PHP 8 --- .travis.yml | 2 ++ composer.json | 8 ++--- phpunit.xml.dist | 66 +++++++++++++----------------------- psalm.xml | 2 ++ src/Stream/MutableFile.php | 2 ++ src/Stream/ReadOnlyFile.php | 5 +++ src/Structure/MerkleTree.php | 6 ++-- 7 files changed, 41 insertions(+), 50 deletions(-) diff --git a/.travis.yml b/.travis.yml index e3d7b4cb..b0f83d82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,13 @@ php: - "7.2" - "7.3" - "7.4" + - "8.0" - "master" - "nightly" matrix: fast_finish: true allow_failures: + - php: "8.0" - php: "master" - php: "nightly" diff --git a/composer.json b/composer.json index e7beaa23..99c529c2 100644 --- a/composer.json +++ b/composer.json @@ -32,10 +32,10 @@ } ], "require": { - "php": "^7.2", + "php": "^7.2|^8", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1", - "paragonie/sodium_compat": "^1.11" + "paragonie/sodium_compat": "^1.13" }, "autoload": { "psr-4": { @@ -43,8 +43,8 @@ } }, "require-dev": { - "phpunit/phpunit": "^8", - "vimeo/psalm": "^3" + "phpunit/phpunit": "^8|^9", + "vimeo/psalm": "^3|^4" }, "scripts": { "test": "phpunit && psalm" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 05248df2..7b78565c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,45 +1,25 @@ - - - - ./src - - ./libsodium - ./vendor - ./build - ./stub - ./test - ./doc - - - - - - - - - ./test/unit - - + + + + ./src + + + ./libsodium + ./vendor + ./build + ./stub + ./test + ./doc + + + + + + + + + ./test/unit + + diff --git a/psalm.xml b/psalm.xml index d9b9a9eb..fb40e6c7 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,5 +10,7 @@ + + diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index c5c8342a..48050e6a 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -111,6 +111,8 @@ public function __construct($file) /** * Close the file handle. + * + * @psalm-suppress InvalidPropertyAssignmentValue */ public function close(): void { diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 528dc7da..40e48f89 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -131,6 +131,8 @@ public function __destruct() /** * Close the file handle. * @return void + * + * @psalm-suppress InvalidPropertyAssignmentValue */ public function close(): void { @@ -145,6 +147,9 @@ public function close(): void * Calculate a BLAKE2b hash of a file * * @return string + * @throws \SodiumException + * @throws FileModified + * @throws FileError */ public function getHash(): string { diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index dcd94c0a..1607a591 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -73,10 +73,8 @@ public function __construct(Node ...$nodes) * * @return string * @throws CannotPerformOperation - * - * @return string - * @throws CannotPerformOperation * @throws \TypeError + * @throws \SodiumException */ public function getRoot(bool $raw = false): string { @@ -159,6 +157,7 @@ public function setPersonalizationString(string $str = ''): self * @return self * @throws CannotPerformOperation * @throws \TypeError + * @throws \SodiumException * @codeCoverageIgnore */ public function triggerRootCalculation(): self @@ -175,6 +174,7 @@ public function triggerRootCalculation(): self * @return string * @throws CannotPerformOperation * @throws \TypeError + * @throws \SodiumException */ protected function calculateRoot(): string { From e34585e48d595ff21704f91e4dae73a48b5634be Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 10:43:46 -0500 Subject: [PATCH 031/134] Wrap memzero call to prevent uncaught exceptions --- src/Asymmetric/Crypto.php | 15 +++++---------- src/File.php | 26 +++++++++++++------------- src/Key.php | 2 +- src/KeyFactory.php | 23 ++++++++++++++--------- src/Symmetric/Crypto.php | 33 ++++++++++++++------------------- src/Util.php | 19 +++++++++++++++++++ 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 8191f6a3..2bbe709e 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -11,12 +11,7 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{ - Halite, - Key, - Symmetric\Crypto as SymmetricCrypto, - Symmetric\EncryptionKey -}; +use ParagonIE\Halite\{Halite, Key, Symmetric\Crypto as SymmetricCrypto, Symmetric\EncryptionKey, Util}; use ParagonIE\HiddenString\HiddenString; /** @@ -318,7 +313,7 @@ public static function signAndEncrypt( } $signature = self::sign($message->getString(), $secretKey, true); $plaintext = new HiddenString($signature . $message->getString()); - \sodium_memzero($signature); + Util::memzero($signature); $myEncKey = $secretKey->getEncryptionSecretKey(); return self::encrypt($plaintext, $myEncKey, $publicKey, $encoding); @@ -364,8 +359,8 @@ public static function unseal( ); // Wipe these immediately: - \sodium_memzero($secret_key); - \sodium_memzero($public_key); + Util::memzero($secret_key); + Util::memzero($public_key); // Now let's open that sealed box $message = \sodium_crypto_box_seal_open( @@ -374,7 +369,7 @@ public static function unseal( ); // Always memzero after retrieving a value - \sodium_memzero($key_pair); + Util::memzero($key_pair); if (!\is_string($message)) { // @codeCoverageIgnoreStart throw new InvalidKey( diff --git a/src/File.php b/src/File.php index 239be2b7..d72f96b0 100644 --- a/src/File.php +++ b/src/File.php @@ -611,8 +611,8 @@ protected static function encryptData( \sodium_crypto_generichash_update($mac, $hkdfSalt); /** @var string $mac */ - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); return self::streamEncrypt( $input, @@ -690,8 +690,8 @@ protected static function decryptData( $old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config); - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); $ret = self::streamDecrypt( $input, @@ -705,7 +705,7 @@ protected static function decryptData( $old_macs ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($authKey); unset($firstNonce); @@ -793,14 +793,14 @@ protected static function sealData( $mac = \sodium_crypto_generichash_init($authKey); // We no longer need $authKey after we set up the hash context - \sodium_memzero($authKey); + Util::memzero($authKey); \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); \sodium_crypto_generichash_update($mac, $hkdfSalt); unset($ephPublic); - \sodium_memzero($hkdfSalt); + Util::memzero($hkdfSalt); $ret = self::streamEncrypt( $input, @@ -812,7 +812,7 @@ protected static function sealData( (string) $mac, $config ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($nonce); return $ret; @@ -908,8 +908,8 @@ protected static function unsealData( $oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config); // We no longer need these: - \sodium_memzero($authKey); - \sodium_memzero($hkdfSalt); + Util::memzero($authKey); + Util::memzero($hkdfSalt); $ret = self::streamDecrypt( $input, @@ -923,7 +923,7 @@ protected static function unsealData( $oldMACs ); - \sodium_memzero($encKey); + Util::memzero($encKey); unset($encKey); unset($nonce); unset($mac); @@ -1237,7 +1237,7 @@ final private static function streamEncrypt( \sodium_increment($nonce); } if (\is_string($nonce)) { - \sodium_memzero($nonce); + Util::memzero($nonce); } // Check that our input file was not modified before we MAC it @@ -1342,7 +1342,7 @@ final private static function streamDecrypt( \sodium_increment($nonce); } if (\is_string($nonce)) { - \sodium_memzero($nonce); + Util::memzero($nonce); } return true; } diff --git a/src/Key.php b/src/Key.php index 0af8adad..dbe62ce3 100644 --- a/src/Key.php +++ b/src/Key.php @@ -90,7 +90,7 @@ public function __debugInfo() public function __destruct() { if (!$this->isPublicKey) { - \sodium_memzero($this->keyMaterial); + Util::memzero($this->keyMaterial); $this->keyMaterial = ''; } } diff --git a/src/KeyFactory.php b/src/KeyFactory.php index ec80affe..cb0295cb 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -96,6 +96,7 @@ public static function generateEncryptionKey(): EncryptionKey * * @throws InvalidKey * @throws \TypeError + * @throws \SodiumException */ public static function generateEncryptionKeyPair(): EncryptionKeyPair { @@ -104,7 +105,7 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair $secretKey = \sodium_crypto_box_secretkey($kp); // Let's wipe our $kp variable - \sodium_memzero($kp); + Util::memzero($kp); return new EncryptionKeyPair( new EncryptionSecretKey( new HiddenString($secretKey) @@ -117,6 +118,7 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair * * @return SignatureKeyPair * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function generateSignatureKeyPair(): SignatureKeyPair @@ -126,7 +128,7 @@ public static function generateSignatureKeyPair(): SignatureKeyPair $secretKey = \sodium_crypto_sign_secretkey($kp); // Let's wipe our $kp variable - \sodium_memzero($kp); + Util::memzero($kp); return new SignatureKeyPair( new SignatureSecretKey( new HiddenString($secretKey) @@ -148,6 +150,7 @@ public static function generateSignatureKeyPair(): SignatureKeyPair * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function deriveAuthenticationKey( @@ -193,6 +196,7 @@ public static function deriveAuthenticationKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function deriveEncryptionKey( @@ -238,6 +242,7 @@ public static function deriveEncryptionKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function deriveEncryptionKeyPair( @@ -269,7 +274,7 @@ public static function deriveEncryptionKeyPair( $secretKey = \sodium_crypto_box_secretkey($keyPair); // Let's wipe our $kp variable - \sodium_memzero($keyPair); + Util::memzero($keyPair); return new EncryptionKeyPair( new EncryptionSecretKey( new HiddenString($secretKey) @@ -322,7 +327,7 @@ public static function deriveSignatureKeyPair( $secretKey = \sodium_crypto_sign_secretkey($keyPair); // Let's wipe our $kp variable - \sodium_memzero($keyPair); + Util::memzero($keyPair); return new SignatureKeyPair( new SignatureSecretKey( new HiddenString($secretKey) @@ -800,7 +805,7 @@ protected static function loadKeyFile(string $filePath): HiddenString // @codeCoverageIgnoreEnd } $data = Hex::decode($fileData); - \sodium_memzero($fileData); + Util::memzero($fileData); return new HiddenString( self::getKeyDataFromString($data) ); @@ -840,10 +845,10 @@ public static function getKeyDataFromString(string $data): string ); // @codeCoverageIgnoreEnd } - \sodium_memzero($data); - \sodium_memzero($versionTag); - \sodium_memzero($calc); - \sodium_memzero($checksum); + Util::memzero($data); + Util::memzero($versionTag); + Util::memzero($calc); + Util::memzero($checksum); return $keyData; } diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 98dd0e18..6632e102 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -10,12 +10,7 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{ - Config as BaseConfig, - Halite, - Symmetric\Config as SymmetricConfig, - Util as CryptoUtil -}; +use ParagonIE\Halite\{Config as BaseConfig, Halite, Symmetric\Config as SymmetricConfig, Util as CryptoUtil, Util}; use ParagonIE\HiddenString\HiddenString; /** @@ -192,8 +187,8 @@ public static function decryptWithAd( 'Invalid message authentication code' ); } - \sodium_memzero($salt); - \sodium_memzero($authKey); + Util::memzero($salt); + Util::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt /** @var string $plaintext */ @@ -202,9 +197,9 @@ public static function decryptWithAd( (string) $nonce, (string) $encKey ); - \sodium_memzero($encrypted); - \sodium_memzero($nonce); - \sodium_memzero($encKey); + Util::memzero($encrypted); + Util::memzero($nonce); + Util::memzero($encKey); return new HiddenString($plaintext); } @@ -284,7 +279,7 @@ public static function encryptWithAd( $nonce, $encKey ); - \sodium_memzero($encKey); + Util::memzero($encKey); // Calculate an authentication tag: $auth = self::calculateMAC( @@ -292,16 +287,16 @@ public static function encryptWithAd( $authKey, $config ); - \sodium_memzero($authKey); + Util::memzero($authKey); /** @var string $message */ $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory - \sodium_memzero($nonce); - \sodium_memzero($salt); - \sodium_memzero($encrypted); - \sodium_memzero($auth); + Util::memzero($nonce); + Util::memzero($salt); + Util::memzero($encrypted); + Util::memzero($auth); $encoder = Halite::chooseEncoder($encoding); if ($encoder) { @@ -421,7 +416,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array ); // We don't need this anymore. - \sodium_memzero($ciphertext); + Util::memzero($ciphertext); // Now we return the pieces in a specific order: return [$version, $config, $salt, $nonce, $encrypted, $auth]; @@ -537,7 +532,7 @@ protected static function verifyMAC( (int) $config->MAC_SIZE ); $res = \hash_equals($mac, $calc); - \sodium_memzero($calc); + Util::memzero($calc); return $res; } // @codeCoverageIgnoreStart diff --git a/src/Util.php b/src/Util.php index 7e27f05f..1a4d6acd 100644 --- a/src/Util.php +++ b/src/Util.php @@ -319,4 +319,23 @@ public static function xorStrings(string $left, string $right): string } return (string) ($left ^ $right); } + + /** + * Wrap memzero() without breaking on sodium_compat + * + * @param string &$var + * @psalm-param-out null $var + * @psalm-suppress InvalidOperand + */ + public static function memzero(string &$var): void + { + try { + \sodium_memzero($var); + } catch (\Throwable $ex) { + /** @var string $var */ + // Best-effort: + $var ^= $var; + } + $var = null; + } } From 88615b798c9900261ae1bec31b55871da6d21758 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 10:49:48 -0500 Subject: [PATCH 032/134] Boyscouting --- src/Asymmetric/Crypto.php | 19 +++++++++++++++++- src/KeyFactory.php | 22 ++++++++++++++++++++ src/Password.php | 3 +++ src/Symmetric/Crypto.php | 42 ++++++++++++++++++++++++++------------- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 2bbe709e..5f05d73f 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -11,7 +11,13 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{Halite, Key, Symmetric\Crypto as SymmetricCrypto, Symmetric\EncryptionKey, Util}; +use ParagonIE\Halite\{ + Halite, + Key, + Symmetric\Crypto as SymmetricCrypto, + Symmetric\EncryptionKey, + Util +}; use ParagonIE\HiddenString\HiddenString; /** @@ -58,6 +64,7 @@ final private function __construct() * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function encrypt( @@ -90,6 +97,7 @@ public static function encrypt( * @throws InvalidMessage * @throws InvalidDigestLength * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function encryptWithAd( @@ -131,6 +139,7 @@ public static function encryptWithAd( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function decrypt( @@ -164,6 +173,7 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function decryptWithAd( @@ -201,6 +211,7 @@ public static function decryptWithAd( * @return HiddenString|Key * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function getSharedSecret( @@ -235,6 +246,7 @@ public static function getSharedSecret( * @return string Ciphertext * * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function seal( @@ -262,6 +274,7 @@ public static function seal( * @return string Signature (detached) * * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function sign( @@ -294,6 +307,7 @@ public static function sign( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function signAndEncrypt( @@ -330,6 +344,7 @@ public static function signAndEncrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function unseal( @@ -393,6 +408,7 @@ public static function unseal( * * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function verify( @@ -437,6 +453,7 @@ public static function verify( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function verifyAndDecrypt( diff --git a/src/KeyFactory.php b/src/KeyFactory.php index cb0295cb..0005aecc 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -296,6 +296,7 @@ public static function deriveEncryptionKeyPair( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function deriveSignatureKeyPair( @@ -390,6 +391,7 @@ public static function getSecurityLevels( * @return AuthenticationKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importAuthenticationKey(HiddenString $keyData): AuthenticationKey @@ -410,6 +412,7 @@ public static function importAuthenticationKey(HiddenString $keyData): Authentic * @return EncryptionKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey @@ -430,6 +433,7 @@ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey * @return EncryptionPublicKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importEncryptionPublicKey(HiddenString $keyData): EncryptionPublicKey @@ -450,6 +454,7 @@ public static function importEncryptionPublicKey(HiddenString $keyData): Encrypt * @return EncryptionSecretKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importEncryptionSecretKey(HiddenString $keyData): EncryptionSecretKey @@ -470,6 +475,7 @@ public static function importEncryptionSecretKey(HiddenString $keyData): Encrypt * @return SignaturePublicKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importSignaturePublicKey(HiddenString $keyData): SignaturePublicKey @@ -490,6 +496,7 @@ public static function importSignaturePublicKey(HiddenString $keyData): Signatur * @return SignatureSecretKey * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importSignatureSecretKey(HiddenString $keyData): SignatureSecretKey @@ -510,6 +517,7 @@ public static function importSignatureSecretKey(HiddenString $keyData): Signatur * @return EncryptionKeyPair * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importEncryptionKeyPair(HiddenString $keyData): EncryptionKeyPair @@ -532,6 +540,7 @@ public static function importEncryptionKeyPair(HiddenString $keyData): Encryptio * @return SignatureKeyPair * * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function importSignatureKeyPair(HiddenString $keyData): SignatureKeyPair @@ -555,6 +564,7 @@ public static function importSignatureKeyPair(HiddenString $keyData): SignatureK * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -578,6 +588,7 @@ public static function loadAuthenticationKey(string $filePath): AuthenticationKe * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -601,6 +612,7 @@ public static function loadEncryptionKey(string $filePath): EncryptionKey * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -624,6 +636,7 @@ public static function loadEncryptionPublicKey(string $filePath): EncryptionPubl * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -647,6 +660,7 @@ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecr * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -670,6 +684,7 @@ public static function loadSignaturePublicKey(string $filePath): SignaturePublic * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -693,6 +708,7 @@ public static function loadSignatureSecretKey(string $filePath): SignatureSecret * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -718,6 +734,7 @@ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPai * * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError * @codeCoverageIgnore */ @@ -743,6 +760,7 @@ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair * * @throws CannotPerformOperation * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function export($key): HiddenString @@ -772,6 +790,7 @@ public static function export($key): HiddenString * @param Key|KeyPair $key * @param string $filename * @return bool + * @throws \SodiumException * @throws \TypeError */ public static function save($key, string $filename = ''): bool @@ -792,6 +811,7 @@ public static function save($key, string $filename = ''): bool * @return HiddenString * @throws CannotPerformOperation * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ protected static function loadKeyFile(string $filePath): HiddenString @@ -818,6 +838,7 @@ protected static function loadKeyFile(string $filePath): HiddenString * @param string $data * @return string * @throws InvalidKey + * @throws \SodiumException * @throws \TypeError */ public static function getKeyDataFromString(string $data): string @@ -859,6 +880,7 @@ public static function getKeyDataFromString(string $data): string * @param string $keyData * @return bool * + * @throws \SodiumException * @throws \TypeError */ protected static function saveKeyFile( diff --git a/src/Password.php b/src/Password.php index 6443f930..3978f078 100644 --- a/src/Password.php +++ b/src/Password.php @@ -51,6 +51,7 @@ final class Password * @throws CannotPerformOperation * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function hash( @@ -90,6 +91,7 @@ public static function hash( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function needsRehash( @@ -190,6 +192,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function verify( diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 6632e102..9e88d655 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -10,7 +10,12 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{Config as BaseConfig, Halite, Symmetric\Config as SymmetricConfig, Util as CryptoUtil, Util}; +use ParagonIE\Halite\{ + Config as BaseConfig, + Halite, + Symmetric\Config as SymmetricConfig, + Util as CryptoUtil +}; use ParagonIE\HiddenString\HiddenString; /** @@ -52,6 +57,7 @@ final private function __construct() * * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function authenticate( @@ -89,6 +95,7 @@ public static function authenticate( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function decrypt( @@ -118,6 +125,7 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function decryptWithAd( @@ -187,8 +195,8 @@ public static function decryptWithAd( 'Invalid message authentication code' ); } - Util::memzero($salt); - Util::memzero($authKey); + CryptoUtil::memzero($salt); + CryptoUtil::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt /** @var string $plaintext */ @@ -197,9 +205,9 @@ public static function decryptWithAd( (string) $nonce, (string) $encKey ); - Util::memzero($encrypted); - Util::memzero($nonce); - Util::memzero($encKey); + CryptoUtil::memzero($encrypted); + CryptoUtil::memzero($nonce); + CryptoUtil::memzero($encKey); return new HiddenString($plaintext); } @@ -218,6 +226,7 @@ public static function decryptWithAd( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function encrypt( @@ -244,6 +253,7 @@ public static function encrypt( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function encryptWithAd( @@ -279,7 +289,7 @@ public static function encryptWithAd( $nonce, $encKey ); - Util::memzero($encKey); + CryptoUtil::memzero($encKey); // Calculate an authentication tag: $auth = self::calculateMAC( @@ -287,16 +297,16 @@ public static function encryptWithAd( $authKey, $config ); - Util::memzero($authKey); + CryptoUtil::memzero($authKey); /** @var string $message */ $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory - Util::memzero($nonce); - Util::memzero($salt); - Util::memzero($encrypted); - Util::memzero($auth); + CryptoUtil::memzero($nonce); + CryptoUtil::memzero($salt); + CryptoUtil::memzero($encrypted); + CryptoUtil::memzero($auth); $encoder = Halite::chooseEncoder($encoding); if ($encoder) { @@ -316,6 +326,7 @@ public static function encryptWithAd( * * @throws CannotPerformOperation * @throws InvalidDigestLength + * @throws \SodiumException * @throws \TypeError */ public static function splitKeys( @@ -416,7 +427,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array ); // We don't need this anymore. - Util::memzero($ciphertext); + CryptoUtil::memzero($ciphertext); // Now we return the pieces in a specific order: return [$version, $config, $salt, $nonce, $encrypted, $auth]; @@ -435,6 +446,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType + * @throws \SodiumException * @throws \TypeError */ public static function verify( @@ -479,6 +491,7 @@ public static function verify( * @param SymmetricConfig $config * @return string * @throws InvalidMessage + * @throws \SodiumException */ protected static function calculateMAC( string $message, @@ -511,6 +524,7 @@ protected static function calculateMAC( * * @throws InvalidMessage * @throws InvalidSignature + * @throws \SodiumException */ protected static function verifyMAC( string $mac, @@ -532,7 +546,7 @@ protected static function verifyMAC( (int) $config->MAC_SIZE ); $res = \hash_equals($mac, $calc); - Util::memzero($calc); + CryptoUtil::memzero($calc); return $res; } // @codeCoverageIgnoreStart From e7adebcc7ee4dd67cc5d1d624ea47d203a5a33d7 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 10:54:29 -0500 Subject: [PATCH 033/134] Add note about message commitment --- doc/Primitives.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/Primitives.md b/doc/Primitives.md index b7d04d7d..5873dbd6 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -9,3 +9,6 @@ * Password-Based Key Derivation: [**Argon2**](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str) In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). + +As a consequence of our use of a keyed BLAKE2b hash as a MAC, instead of GCM/Poly1305, +Halite ciphertexts are [**message committing**](https://eprint.iacr.org/2020/1456) which makes ciphertexts random key robust. From bcacdbe90b450660d78cbc48c1052b934fc183be Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 11:31:57 -0500 Subject: [PATCH 034/134] Use sodium_compat 1.14+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99c529c2..4da1b748 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "php": "^7.2|^8", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1", - "paragonie/sodium_compat": "^1.13" + "paragonie/sodium_compat": "^1.14" }, "autoload": { "psr-4": { From 907ba473060308d1231062645d9553eace41e785 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 3 Dec 2020 11:32:56 -0500 Subject: [PATCH 035/134] Prepare for v4.7.0 --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abd2ce36..390b5e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Version 4.7.0 (2020-12-03) + +* Merged [#154](https://github.com/paragonie/halite/pull/154), which supports + the SameSite cookie arguments on PHP 7.3+. +* Create a wrapper for `sodium_memzero()` to support sodium_compat. +* Added support for PHP 8. +* [#146](https://github.com/paragonie/halite/pull/146), + [#155](https://github.com/paragonie/halite/pull/155), + [#156](https://github.com/paragonie/halite/pull/156) -- + Various documentation improvements. + ## Version 4.6.0 (2019-09-12) * Merged [#138](https://github.com/paragonie/halite/pull/138), which adds @@ -22,7 +33,7 @@ Thanks [@elliot-sawyer](https://github.com/elliot-sawyer). * Merged [#134](https://github.com/paragonie/halite/pull/134), which allows `MutableFile` to be used on resources opened in `wb` mode. - Thanks [@christiaanbaartse](christiaanbaartse). + Thanks [@christiaanbaartse](https://github.com/christiaanbaartse). * Other minor documentation improvements. ## Version 4.5.3 (2019-03-11) From 538a0579e86ac8222973be55d17615a7be8f29cb Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 6 Dec 2020 10:18:22 -0500 Subject: [PATCH 036/134] Allow hidden-string v2 --- CHANGELOG.md | 4 ++++ composer.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 390b5e16..b43934a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 4.7.1 (2020-12-06) + +* Allow v2 of `paragonie/hidden-string` to be installed. + ## Version 4.7.0 (2020-12-03) * Merged [#154](https://github.com/paragonie/halite/pull/154), which supports diff --git a/composer.json b/composer.json index 4da1b748..8b2ab44f 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require": { "php": "^7.2|^8", "paragonie/constant_time_encoding": "^2", - "paragonie/hidden-string": "^1", + "paragonie/hidden-string": "^1|^2", "paragonie/sodium_compat": "^1.14" }, "autoload": { From 74c70f23dfb435b71f0e9ee6fd08c55b5ce67187 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 6 Dec 2020 10:33:32 -0500 Subject: [PATCH 037/134] Add ext/sodium to build instructions --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b0f83d82..d4858eac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ matrix: - php: "nightly" install: + - pecl install sodium - travis_retry composer install --no-interaction - wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar - chmod +x php-coveralls.phar From 9ff2c45d3cb3cf1c071e9a5e40d9fdcccd7229e7 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 6 Dec 2020 12:39:42 -0500 Subject: [PATCH 038/134] Remove ext/sodium from build instructions --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d4858eac..b0f83d82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ matrix: - php: "nightly" install: - - pecl install sodium - travis_retry composer install --no-interaction - wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar - chmod +x php-coveralls.phar From 158562965b038155d855ad3e948d59aee95659e8 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 6 Dec 2020 13:17:37 -0500 Subject: [PATCH 039/134] Don't run slow tests unless ext/sodium is installed --- test/unit/AsymmetricTest.php | 30 ++++++++++++++++++++++++++++++ test/unit/FileTest.php | 24 ++++++++++++++++++++++++ test/unit/HiddenStringTest.php | 2 +- test/unit/KeyPairTest.php | 21 +++++++++++++++++++++ test/unit/KeyTest.php | 33 +++++++++++++++++++++++++++++++++ test/unit/PasswordTest.php | 12 ++++++++++++ 6 files changed, 121 insertions(+), 1 deletion(-) diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index e59833f6..76c8489a 100644 --- a/test/unit/AsymmetricTest.php +++ b/test/unit/AsymmetricTest.php @@ -25,6 +25,9 @@ final class AsymmetricTest extends TestCase */ public function testEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -59,6 +62,9 @@ public function testEncrypt() */ public function testEncryptWithAd() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -124,6 +130,9 @@ public function testEncryptWithAd() */ public function testEncryptEmpty() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -149,6 +158,9 @@ public function testEncryptEmpty() public function testEncryptFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateEncryptionKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -186,6 +198,9 @@ public function testEncryptFail() */ public function testSeal() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } if ( SODIUM_LIBRARY_MAJOR_VERSION < 7 || (SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5) @@ -242,6 +257,9 @@ public function testSeal() */ public function testSealFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } if ( SODIUM_LIBRARY_MAJOR_VERSION < 7 || (SODIUM_LIBRARY_MAJOR_VERSION == 7 && SODIUM_LIBRARY_MINOR_VERSION < 5) @@ -281,6 +299,9 @@ public function testSealFail() */ public function testSign() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $message = 'test message'; @@ -304,6 +325,9 @@ public function testSign() */ public function testSignEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -344,6 +368,9 @@ public function testSignEncrypt() */ public function testSignEncryptFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); @@ -414,6 +441,9 @@ public function testSignEncryptFail() */ public function testSignFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $alice = KeyFactory::generateSignatureKeyPair(); $message = 'test message'; diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 5396729a..9cc485b5 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -308,6 +308,9 @@ public function testEncryptVarious() */ public function testSeal() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); @@ -351,6 +354,9 @@ public function testSeal() */ public function testSealFromStreamWrapper() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } require_once __DIR__ . '/RemoteStream.php'; stream_register_wrapper('haliteTest', RemoteStream::class); touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); @@ -398,6 +404,9 @@ public function testSealFromStreamWrapper() */ public function testSealEmpty() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } file_put_contents(__DIR__.'/tmp/empty.txt', ''); chmod(__DIR__.'/tmp/empty.txt', 0777); touch(__DIR__.'/tmp/empty.sealed.txt'); @@ -444,6 +453,9 @@ public function testSealEmpty() */ public function testSealFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } touch(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); chmod(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.open_fail.png'); @@ -502,6 +514,9 @@ public function testSealFail() */ public function testSealSmallFail() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } touch(__DIR__.'/tmp/empty.sealed.txt'); chmod(__DIR__.'/tmp/empty.sealed.txt', 0777); touch(__DIR__.'/tmp/empty.unsealed.txt'); @@ -556,6 +571,9 @@ public function testSealSmallFail() */ public function testSealVarious() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); @@ -615,6 +633,9 @@ public function testSealVarious() */ public function testSign() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::generateSignatureKeyPair(); $secretkey = $keypair->getSecretKey(); $publickey = $keypair->getPublicKey(); @@ -656,6 +677,9 @@ public function testSign() */ public function testSignVarious() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::generateSignatureKeyPair(); $secretkey = $keypair->getSecretKey(); $publickey = $keypair->getPublicKey(); diff --git a/test/unit/HiddenStringTest.php b/test/unit/HiddenStringTest.php index e0268bda..78ca4fb9 100644 --- a/test/unit/HiddenStringTest.php +++ b/test/unit/HiddenStringTest.php @@ -18,4 +18,4 @@ public function testConstructor() $x = new HiddenString('test'); $this->assertInstanceOf(Outsourced::class, $x); } -} \ No newline at end of file +} diff --git a/test/unit/KeyPairTest.php b/test/unit/KeyPairTest.php index a750cd26..31da70d3 100644 --- a/test/unit/KeyPairTest.php +++ b/test/unit/KeyPairTest.php @@ -27,6 +27,9 @@ final class KeyPairTest extends TestCase */ public function testDeriveSigningKey() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -68,6 +71,9 @@ public function testDeriveSigningKey() */ public function testDeriveSigningKeyOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -108,6 +114,9 @@ public function testDeriveSigningKeyOldArgon2i() */ public function testEncryptionKeyPair() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $boxKeypair = KeyFactory::generateEncryptionKeyPair(); $boxSecret = $boxKeypair->getSecretKey(); $boxPublic = $boxKeypair->getPublicKey(); @@ -215,6 +224,9 @@ public function testEncryptionKeyPair() */ public function testFileStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $filename = tempnam(__DIR__.'/tmp/', 'key'); $key = KeyFactory::generateEncryptionKeyPair(); KeyFactory::save($key, $filename); @@ -234,6 +246,9 @@ public function testFileStorage() */ public function testMutation() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $sign_kp = KeyFactory::generateSignatureKeyPair(); $box_kp = $sign_kp->getEncryptionKeyPair(); $sign_sk = $sign_kp->getSecretKey(); @@ -258,6 +273,9 @@ public function testMutation() */ public function testSignatureKeyPair() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $signKeypair = KeyFactory::generateSignatureKeyPair(); $signSecret = $signKeypair->getSecretKey(); $signPublic = $signKeypair->getPublicKey(); @@ -364,6 +382,9 @@ public function testSignatureKeyPair() */ public function testPublicDerivation() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $enc_kp = KeyFactory::generateEncryptionKeyPair(); $enc_secret = $enc_kp->getSecretKey(); $enc_public = $enc_kp->getPublicKey(); diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index 28822163..3cfeea70 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -25,6 +25,9 @@ class KeyTest extends TestCase */ public function testDerive() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -68,6 +71,9 @@ public function testDerive() */ public function testDeriveOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -102,6 +108,9 @@ public function testDeriveOldArgon2i() */ public function testDeriveSigningKey() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -138,6 +147,9 @@ public function testDeriveSigningKey() */ public function testDeriveSigningKeyOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -175,6 +187,9 @@ public function testDeriveSigningKeyOldArgon2i() */ public function testImport() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::generateAuthenticationKey(); $export = KeyFactory::export($key); $import = KeyFactory::importAuthenticationKey($export); @@ -261,6 +276,9 @@ public function testImport() */ public function testKeyTypes() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::generateAuthenticationKey(); $this->assertFalse($key->isAsymmetricKey()); $this->assertFalse($key->isEncryptionKey()); @@ -315,6 +333,9 @@ public function testKeyTypes() */ public function testEncKeyStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $enc_keypair = KeyFactory::deriveEncryptionKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -361,6 +382,9 @@ public function testEncKeyStorage() */ public function testSignKeyStorage() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $sign_keypair = KeyFactory::deriveSignatureKeyPair( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -405,6 +429,9 @@ public function testSignKeyStorage() */ public function testInvalidKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } try { KeyFactory::deriveEncryptionKey( new HiddenString('apple'), @@ -428,6 +455,9 @@ public function testInvalidKeyLevels() */ public function testKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", @@ -457,6 +487,9 @@ public function testKeyLevels() */ public function testKeyLevelsOldArgon2i() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = KeyFactory::deriveEncryptionKey( new HiddenString('apple'), "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", diff --git a/test/unit/PasswordTest.php b/test/unit/PasswordTest.php index ec2ee2bb..dfc1eb75 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -22,6 +22,9 @@ final class PasswordTest extends TestCase */ public function testEncrypt() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $hash = Password::hash(new HiddenString('test password'), $key); @@ -56,6 +59,9 @@ public function testEncrypt() */ public function testEncryptWithAd() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $aad = '{"userid":12}'; @@ -130,6 +136,9 @@ public function testEncryptWithAd() */ public function testKeyLevels() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $aad = '{"userid":12}'; @@ -153,6 +162,9 @@ public function testKeyLevels() */ public function testRehash() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); try { From 6a7fbd76883563f1b2f5feac47711fc76f44147a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 6 Dec 2020 13:44:57 -0500 Subject: [PATCH 040/134] Don't run any file tests unless libsodium is installed --- test/unit/FileTest.php | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 9cc485b5..2a199088 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -20,6 +20,9 @@ final class FileTest extends TestCase public function setUp(): void { chmod(__DIR__.'/tmp/', 0777); + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } } /** @@ -308,9 +311,6 @@ public function testEncryptVarious() */ public function testSeal() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); @@ -354,9 +354,6 @@ public function testSeal() */ public function testSealFromStreamWrapper() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } require_once __DIR__ . '/RemoteStream.php'; stream_register_wrapper('haliteTest', RemoteStream::class); touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); @@ -404,9 +401,6 @@ public function testSealFromStreamWrapper() */ public function testSealEmpty() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } file_put_contents(__DIR__.'/tmp/empty.txt', ''); chmod(__DIR__.'/tmp/empty.txt', 0777); touch(__DIR__.'/tmp/empty.sealed.txt'); @@ -453,9 +447,6 @@ public function testSealEmpty() */ public function testSealFail() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } touch(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); chmod(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.open_fail.png'); @@ -514,9 +505,6 @@ public function testSealFail() */ public function testSealSmallFail() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } touch(__DIR__.'/tmp/empty.sealed.txt'); chmod(__DIR__.'/tmp/empty.sealed.txt', 0777); touch(__DIR__.'/tmp/empty.unsealed.txt'); @@ -571,9 +559,6 @@ public function testSealSmallFail() */ public function testSealVarious() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); @@ -633,9 +618,6 @@ public function testSealVarious() */ public function testSign() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } $keypair = KeyFactory::generateSignatureKeyPair(); $secretkey = $keypair->getSecretKey(); $publickey = $keypair->getPublicKey(); @@ -677,9 +659,6 @@ public function testSign() */ public function testSignVarious() { - if (!\extension_loaded('sodium')) { - $this->markTestSkipped('Libsodium not installed'); - } $keypair = KeyFactory::generateSignatureKeyPair(); $secretkey = $keypair->getSecretKey(); $publickey = $keypair->getPublicKey(); From 1fc48289b3759191c4f912d532b3691bf42a1eb4 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 7 Dec 2020 07:42:21 -0500 Subject: [PATCH 041/134] Don't run these tests without ext/sodium --- test/unit/HaliteTest.php | 3 +++ test/unit/UtilTest.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/unit/HaliteTest.php b/test/unit/HaliteTest.php index 6539e7e5..4ed80dda 100644 --- a/test/unit/HaliteTest.php +++ b/test/unit/HaliteTest.php @@ -16,6 +16,9 @@ final class HaliteTest extends TestCase { public function testLibsodiumDetection() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $this->assertTrue( Halite::isLibsodiumSetupCorrectly() ); diff --git a/test/unit/UtilTest.php b/test/unit/UtilTest.php index efe94082..685fe4f0 100644 --- a/test/unit/UtilTest.php +++ b/test/unit/UtilTest.php @@ -161,6 +161,9 @@ public function testBlake2bKDF() */ public function testSafeStrcpy() { + if (!\extension_loaded('sodium')) { + $this->markTestSkipped('Libsodium not installed'); + } $unique = random_bytes(128); $clone = Util::safeStrcpy($unique); $this->assertSame($unique, $clone); From 698df651adae582477ed41e8efef6bd47204d933 Mon Sep 17 00:00:00 2001 From: Junaid Farooq Date: Thu, 10 Dec 2020 17:40:03 +0530 Subject: [PATCH 042/134] Remove access modifier final from private methods Remove access modifier `final` from private methods as such methods are never overridden by other classes. --- src/File.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index d72f96b0..2b6d96e2 100644 --- a/src/File.php +++ b/src/File.php @@ -1208,7 +1208,7 @@ protected static function splitKeys( * @throws \SodiumException * @throws \TypeError */ - final private static function streamEncrypt( + private static function streamEncrypt( ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, @@ -1276,7 +1276,7 @@ final private static function streamEncrypt( * @throws \TypeError * @throws \SodiumException */ - final private static function streamDecrypt( + private static function streamDecrypt( ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, @@ -1363,7 +1363,7 @@ final private static function streamDecrypt( * @throws \TypeError * @throws \SodiumException */ - final private static function streamVerify( + private static function streamVerify( ReadOnlyFile $input, $mac, Config $config From 95575b8f9a32c17da37fcb301d5bdede96f3bc32 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 18 Apr 2021 15:04:53 -0400 Subject: [PATCH 043/134] Migrate from Travis CI to Github Actions --- .github/workflows/ci.yml | 67 +++++++++++++++++++++++++++++ README.md | 2 +- composer.json | 2 +- psalm.xml | 1 + src/Cookie.php | 1 - src/File.php | 9 ---- src/KeyFactory.php | 6 +-- src/Password.php | 1 - src/Stream/MutableFile.php | 1 - src/Structure/MerkleTree.php | 6 +-- src/Structure/TrimmedMerkleTree.php | 1 - src/Symmetric/Crypto.php | 3 -- src/Util.php | 2 - 13 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ac0aea08 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: [push] + +jobs: + old: + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['7.2', '7.3'] + phpunit-versions: ['latest'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium + ini-values: post_max_size=256M, max_execution_time=180 + tools: psalm, phpunit:${{ matrix.phpunit-versions }} + + - name: Install dependencies + run: composer install + + - name: PHPUnit tests + uses: php-actions/phpunit@v2 + timeout-minutes: 30 + with: + memory_limit: 256M + - name: Static Analysis + run: vendor/bin/psalm + + modern: + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['7.4', '8.0'] + phpunit-versions: ['latest'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium + ini-values: post_max_size=256M, max_execution_time=180 + tools: psalm, phpunit:${{ matrix.phpunit-versions }} + + - name: Install dependencies + run: composer install + + - name: PHPUnit tests + uses: php-actions/phpunit@v2 + timeout-minutes: 30 + with: + memory_limit: 256M + + - name: Static Analysis + run: vendor/bin/psalm diff --git a/README.md b/README.md index 9aff86f2..c935fc00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Halite -[![Build Status](https://travis-ci.org/paragonie/halite.svg?branch=master)](https://travis-ci.org/paragonie/halite) +[![Build Status](https://github.com/paragonie/halite/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/halite/actions) [![Latest Stable Version](https://poser.pugx.org/paragonie/halite/v/stable)](https://packagist.org/packages/paragonie/halite) [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) diff --git a/composer.json b/composer.json index 8b2ab44f..fa038512 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "php": "^7.2|^8", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1|^2", - "paragonie/sodium_compat": "^1.14" + "paragonie/sodium_compat": "^1.15" }, "autoload": { "psr-4": { diff --git a/psalm.xml b/psalm.xml index fb40e6c7..69c5b316 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,5 +12,6 @@ + diff --git a/src/Cookie.php b/src/Cookie.php index 9303e797..97991f15 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -119,7 +119,6 @@ protected static function getConfig(string $stored): SymmetricConfig ); } if (\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { - /** @var string $decoded */ $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( $decoded, diff --git a/src/File.php b/src/File.php index d72f96b0..415b974f 100644 --- a/src/File.php +++ b/src/File.php @@ -521,11 +521,9 @@ protected static function checksumData( while ($fileStream->remainingBytes() > 0) { // Don't go past the file size even if $config->BUFFER is not an even multiple of it: if (($fileStream->getPos() + (int) $config->BUFFER) > $size) { - /** @var int $amount_to_read */ $amount_to_read = ($size - $fileStream->getPos()); } else { // @codeCoverageIgnoreStart - /** @var int $amount_to_read */ $amount_to_read = (int) $config->BUFFER; // @codeCoverageIgnoreEnd } @@ -659,7 +657,6 @@ protected static function decryptData( ); } // Parse the header, ensuring we get 4 bytes - /** @var string $header */ $header = $input->readBytes(Halite::VERSION_TAG_LEN); // Load the config @@ -673,9 +670,7 @@ protected static function decryptData( } // Let's grab the first nonce and salt - /** @var string $firstNonce */ $firstNonce = $input->readBytes((int) $config->NONCE_BYTES); - /** @var string $hkdfSalt */ $hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN); // Split our keys, begin the HMAC instance @@ -1286,7 +1281,6 @@ final private static function streamDecrypt( array &$chunk_macs ): bool { $start = $input->getPos(); - /** @var int $cipher_end */ $cipher_end = $input->getSize() - (int) $config->MAC_SIZE; // Begin the streaming decryption $input->reset($start); @@ -1368,11 +1362,9 @@ final private static function streamVerify( $mac, Config $config ): array { - /** @var int $start */ $start = $input->getPos(); // Grab the stored MAC: - /** @var int $cipher_end */ $cipher_end = $input->getSize() - (int) $config->MAC_SIZE; $input->reset($cipher_end); $stored_mac = $input->readBytes((int) $config->MAC_SIZE); @@ -1402,7 +1394,6 @@ final private static function streamVerify( \sodium_crypto_generichash_update($mac, $read); $mac = (string) $mac; // Copy the hash state then store the MAC of this chunk - /** @var string $chunkMAC */ $chunkMAC = Util::safeStrcpy($mac); $chunkMACs []= \sodium_crypto_generichash_final( // @codeCoverageIgnoreStart diff --git a/src/KeyFactory.php b/src/KeyFactory.php index 0005aecc..9d3d72a6 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -168,7 +168,6 @@ public static function deriveAuthenticationKey( ); // @codeCoverageIgnoreEnd } - /** @var string $secretKey */ $secretKey = @\sodium_crypto_pwhash( \SODIUM_CRYPTO_AUTH_KEYBYTES, $password->getString(), @@ -214,8 +213,7 @@ public static function deriveEncryptionKey( ); // @codeCoverageIgnoreEnd } - /** @var string $secretKey */ - $secretKey = @\sodium_crypto_pwhash( + $secretKey = \sodium_crypto_pwhash( \SODIUM_CRYPTO_STREAM_KEYBYTES, $password->getString(), $salt, @@ -261,7 +259,6 @@ public static function deriveEncryptionKeyPair( // @codeCoverageIgnoreEnd } // Diffie Hellman key exchange key pair - /** @var string $seed */ $seed = @\sodium_crypto_pwhash( \SODIUM_CRYPTO_BOX_SEEDBYTES, $password->getString(), @@ -315,7 +312,6 @@ public static function deriveSignatureKeyPair( // @codeCoverageIgnoreEnd } // Digital signature keypair - /** @var string $seed */ $seed = @\sodium_crypto_pwhash( \SODIUM_CRYPTO_SIGN_SEEDBYTES, $password->getString(), diff --git a/src/Password.php b/src/Password.php index cacd3fac..6c0b0b70 100644 --- a/src/Password.php +++ b/src/Password.php @@ -165,7 +165,6 @@ protected static function getConfig(string $stored): SymmetricConfig || \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) ) { - /** @var string $decoded */ $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( $decoded, diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index 48050e6a..acdcb9b6 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -191,7 +191,6 @@ public function readBytes(int $num, bool $skipTests = false): string break; // @codeCoverageIgnoreEnd } - /** @var int $bufSize */ $bufSize = \min($remaining, self::CHUNK); /** @var string|bool $read */ $read = \fread($this->fp, $bufSize); diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 1607a591..65b3e7a3 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -225,10 +225,8 @@ protected function calculateRoot(): string ); // @codeCoverageIgnoreEnd } else { - /** @var string $curr */ - $curr = (string) ($hash[$i] ?? ''); - /** @var string $next */ - $next = (string) ($hash[$i + 1] ?? ''); + $curr = ($hash[$i] ?? ''); + $next = ($hash[$i + 1] ?? ''); $tmp[$j] = Util::raw_hash( self::MERKLE_BRANCH . $this->personalization . diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index 90a22882..a3944505 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -78,7 +78,6 @@ protected function calculateRoot(): string } ++$j; } - /** @var array $hash */ $hash = $tmp; $size >>= 1; } while ($size > 1); diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 9e88d655..241f3cb0 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -69,7 +69,6 @@ public static function authenticate( Halite::HALITE_VERSION, 'auth' ); - /** @var string $mac */ $mac = self::calculateMAC( $message, $secretKey->getRawKeyMaterial(), @@ -199,7 +198,6 @@ public static function decryptWithAd( CryptoUtil::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt - /** @var string $plaintext */ $plaintext = \sodium_crypto_stream_xor( (string) $encrypted, (string) $nonce, @@ -283,7 +281,6 @@ public static function encryptWithAd( list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: - /** @var string $encrypted */ $encrypted = \sodium_crypto_stream_xor( $plaintext->getString(), $nonce, diff --git a/src/Util.php b/src/Util.php index 1a4d6acd..4503cbf1 100644 --- a/src/Util.php +++ b/src/Util.php @@ -161,7 +161,6 @@ public static function hkdfBlake2b( $t .= $last_block; } // ORM = first L octets of T - /** @var string $orm */ $orm = Binary::safeSubstr($t, 0, $length); return $orm; } @@ -268,7 +267,6 @@ public static function safeStrcpy(string $string): string { $length = Binary::safeStrlen($string); $return = ''; - /** @var int $chunk */ $chunk = $length >> 1; if ($chunk < 1) { $chunk = 1; From 6a2f6aafb677c051be664d39a7a08842a41d489f Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 18 Apr 2021 15:11:51 -0400 Subject: [PATCH 044/134] Fix tests --- src/File.php | 2 +- test/unit/StreamTest.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 7c12b2d8..9e8b5cd5 100644 --- a/src/File.php +++ b/src/File.php @@ -51,7 +51,7 @@ final class File * @throws \Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { throw new \Error('Do not instantiate'); } diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index 9703e1e6..624d5fd2 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -50,6 +50,11 @@ public function testUnreadableFile() $buf = random_bytes(65537); file_put_contents($filename, $buf); chmod($filename, 0000); + $perms = fileperms($filename); + if (!is_int($perms) || ($perms & 0777) !== 0) { + $this->markTestSkipped('chmod failed to remove read access, so the test will fail; skipping'); + return; + } try { new ReadOnlyFile($filename); From 68531b9729cba5614b6830f8dd7a20fd60dc8cef Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 18 Apr 2021 15:17:36 -0400 Subject: [PATCH 045/134] Let's troubleshoot CI --- test/unit/StreamTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index 624d5fd2..f93e1103 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -55,6 +55,9 @@ public function testUnreadableFile() $this->markTestSkipped('chmod failed to remove read access, so the test will fail; skipping'); return; } + var_dump($perms); + $this->markTestSkipped('This is broken in CI; why?!'); + return; try { new ReadOnlyFile($filename); From b6fd5bd75bd815afa071062bdf7b7699899d4bb9 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 18 Apr 2021 15:20:41 -0400 Subject: [PATCH 046/134] Skip this test if CI environment has weird perms --- test/unit/StreamTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index f93e1103..c433fa72 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -51,13 +51,10 @@ public function testUnreadableFile() file_put_contents($filename, $buf); chmod($filename, 0000); $perms = fileperms($filename); - if (!is_int($perms) || ($perms & 0777) !== 0) { + if (!is_int($perms) || ($perms & 0777) !== 0 || is_readable($filename)) { $this->markTestSkipped('chmod failed to remove read access, so the test will fail; skipping'); return; } - var_dump($perms); - $this->markTestSkipped('This is broken in CI; why?!'); - return; try { new ReadOnlyFile($filename); From 7596d5cb25154092b524c34cb9ce2201db612ffc Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Sun, 18 Apr 2021 15:24:56 -0400 Subject: [PATCH 047/134] Prepare for v4.8.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b43934a8..72579df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 4.8.0 (2021-04-18) + +* Merged [#158](https://github.com/paragonie/halite/pull/158), which removes + the `final` access modifier from private methods and guarantees PHP 8 support. +* Migrated tests off of Travis CI, onto Github Actions instead. + ## Version 4.7.1 (2020-12-06) * Allow v2 of `paragonie/hidden-string` to be installed. From f37b9282618c68b278ddf3be6e03df57e135cd09 Mon Sep 17 00:00:00 2001 From: Faizan Akram Dar Date: Thu, 29 Jul 2021 19:39:33 +0530 Subject: [PATCH 048/134] Fixes documentation --- doc/Features.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/Features.md b/doc/Features.md index 0e1ffcdd..815f3108 100644 --- a/doc/Features.md +++ b/doc/Features.md @@ -43,15 +43,15 @@ authenticated encryption and digital signatures. `File` allows developers to perform secure cryptographic operations on large files with a low memory footprint. -The `File` API looks like this: +The [`File`](Classes/File.md) API looks like this: * `File::checksum`(`file`, [`AuthenticationKey?`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` * `File::encrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) * `File::decrypt`(`file`, `file`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) * `File::seal`(`file`, `file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)) * `File::unseal`(`file`, `file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)) -* `File::sign`(`file`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): `string` -* `File::verify`(`file`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `bool` +* `File::sign`(`file`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md)): `string` +* `File::verify`(`file`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md)): `bool` The `file` type indicates that the argument can be either a `string` containing the file's path, or a `resource` (open file handle). From 688934183401b2933edf0a5a9da5add852700d1b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 4 Oct 2021 04:47:04 -0400 Subject: [PATCH 049/134] Fix Psalm nits --- src/Cookie.php | 2 ++ src/Util.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Cookie.php b/src/Cookie.php index 97991f15..f3a0dbce 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -147,6 +147,8 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidMessage * @throws InvalidType * @throws \TypeError + * + * @psalm-suppress InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument */ public function store( diff --git a/src/Util.php b/src/Util.php index 4503cbf1..16edcb91 100644 --- a/src/Util.php +++ b/src/Util.php @@ -323,6 +323,7 @@ public static function xorStrings(string $left, string $right): string * * @param string &$var * @psalm-param-out null $var + * @psalm-suppress UnnecessaryVarAnnotation * @psalm-suppress InvalidOperand */ public static function memzero(string &$var): void @@ -330,7 +331,6 @@ public static function memzero(string &$var): void try { \sodium_memzero($var); } catch (\Throwable $ex) { - /** @var string $var */ // Best-effort: $var ^= $var; } From d8549c2dba32632f86b790038d9843214afdad94 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 11:56:51 -0400 Subject: [PATCH 050/134] Update event trigger --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac0aea08..e2b36363 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [push] +on: [push, pull_request] jobs: old: From ca1d9a412d2c4d492b07a7bae0f49c60ded08726 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 08:56:42 -0400 Subject: [PATCH 051/134] Begin v5 branch: Raise PHP minimum to 8.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fa038512..17b754de 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ } ], "require": { - "php": "^7.2|^8", + "php": "^8", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1|^2", "paragonie/sodium_compat": "^1.15" From 8d53eee136e4c964ef4aca9b4b7ab61df7e32c22 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 08:58:10 -0400 Subject: [PATCH 052/134] Update dependencies --- composer.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 17b754de..f159981b 100644 --- a/composer.json +++ b/composer.json @@ -33,9 +33,10 @@ ], "require": { "php": "^8", + "ext-json": "*", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1|^2", - "paragonie/sodium_compat": "^1.15" + "paragonie/sodium_compat": "^1.17" }, "autoload": { "psr-4": { @@ -43,8 +44,8 @@ } }, "require-dev": { - "phpunit/phpunit": "^8|^9", - "vimeo/psalm": "^3|^4" + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4" }, "scripts": { "test": "phpunit && psalm" From 00a2ecc3cb89b657861931ca1016cca2c1e20c99 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 08:58:46 -0400 Subject: [PATCH 053/134] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72579df8..7cd6f1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 5.0.0 (Unreleased) + +* Increased minimum PHP version to 8.0. + ## Version 4.8.0 (2021-04-18) * Merged [#158](https://github.com/paragonie/halite/pull/158), which removes From 7cc6672be2e0cdf639a7327cf7634fb4b8a56886 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 09:54:19 -0400 Subject: [PATCH 054/134] Switch to XChaCha20 --- CHANGELOG.md | 3 + doc/Primitives.md | 4 +- src/Config.php | 4 + src/File.php | 436 +++++++++++++++++-------------------- src/Halite.php | 12 +- src/Symmetric/Config.php | 36 ++- src/Symmetric/Crypto.php | 99 ++++++--- src/Util.php | 16 ++ test/unit/FileTest.php | 46 +--- test/unit/PasswordTest.php | 34 +-- 10 files changed, 345 insertions(+), 345 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd6f1a8..c275ae0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Version 5.0.0 (Unreleased) * Increased minimum PHP version to 8.0. +* Encryption now uses XChaCha20 instead of XSalsa20. +* The `File` class no longer supports the `resource` type. To migrate code, wrap your + `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. ## Version 4.8.0 (2021-04-18) diff --git a/doc/Primitives.md b/doc/Primitives.md index 5873dbd6..bec1e7ce 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -1,6 +1,8 @@ # Cryptography Primitives used in Halite -* Symmetric-key encryption: [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite) +* Symmetric-key encryption: (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite) + * [**XChaCha20**](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20) + * Previously, [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) * Symmetric-key authentication: **[BLAKE2b](https://download.libsodium.org/doc/hashing/generic_hashing.html#singlepart-example-with-a-key)** (keyed) * Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) followed by symmetric-key authenticated encryption * Asymmetric-key digital signatures: [**Ed25519**](https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md#crypto-sign) diff --git a/src/Config.php b/src/Config.php index 0f23dd92..f39f1391 100644 --- a/src/Config.php +++ b/src/Config.php @@ -19,6 +19,10 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * @property bool CHECKSUM_PUBKEY + * @property int BUFFER + * @property int HASH_LEN */ class Config { diff --git a/src/File.php b/src/File.php index 9e8b5cd5..b64e801a 100644 --- a/src/File.php +++ b/src/File.php @@ -23,6 +23,7 @@ Stream\MutableFile, Stream\ReadOnlyFile, Symmetric\AuthenticationKey, + Symmetric\Config as SymmetricConfig, Symmetric\EncryptionKey }; use ParagonIE\HiddenString\HiddenString; @@ -61,10 +62,10 @@ private function __construct() * the entire file into memory. You may optionally supply a key to use in * the BLAKE2b hash. * - * @param string|resource|ReadOnlyFile $filePath - * @param Key $key (optional; expects SignaturePublicKey or + * @param string|ReadOnlyFile $filePath + * @param ?Key $key (optional; expects SignaturePublicKey or * AuthenticationKey) - * @param mixed $encoding Which encoding scheme to use for the checksum? + * @param bool|string $encoding Which encoding scheme to use for the checksum? * @return string The checksum * * @throws CannotPerformOperation @@ -76,9 +77,9 @@ private function __construct() * @throws \TypeError */ public static function checksum( - $filePath, + string|ReadonlyFile $filePath, Key $key = null, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($filePath instanceof ReadOnlyFile) { $pos = $filePath->getPos(); @@ -92,31 +93,26 @@ public static function checksum( return $checksum; } - if (\is_resource($filePath) || \is_string($filePath)) { + if (is_string($filePath)) { $readOnly = new ReadOnlyFile($filePath); try { - $checksum = self::checksumData( + return self::checksumData( $readOnly, $key, $encoding ); - return $checksum; } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); + throw new InvalidType('Argument 1: Expected a filename'); } /** * Encrypt a file using symmetric authenticated encryption. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file * @param EncryptionKey $key Symmetric encryption key * @return int Number of bytes written * @@ -128,54 +124,44 @@ public static function checksum( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws \SodiumException */ public static function encrypt( - $input, - $output, + string|ReadOnlyFile $input, + string|MutableFile $output, EncryptionKey $key ): int { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::encryptData( - $readOnly, - $mutable, - $key - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::encryptData( + $readOnly, + $mutable, + $key + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Decrypt a file using symmetric-key authenticated encryption. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file * @param EncryptionKey $key Symmetric encryption key * @return bool TRUE if successful * @@ -187,55 +173,45 @@ public static function encrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws \SodiumException */ public static function decrypt( - $input, - $output, + string|ReadOnlyFile $input, + string|MutableFile $output, EncryptionKey $key ): bool { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::decryptData( - $readOnly, - $mutable, - $key - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::decryptData( + $readOnly, + $mutable, + $key + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Strings or file handles expected' - ); } /** * Encrypt a file using anonymous public-key encryption (with ciphertext * authentication). * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file * @param EncryptionPublicKey $publicKey Recipient's encryption public key * @return int * @@ -249,52 +225,42 @@ public static function decrypt( * @throws \TypeError */ public static function seal( - $input, - $output, + string|ReadOnlyFile $input, + string|MutableFile $output, EncryptionPublicKey $publicKey ): int { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::sealData( - $readOnly, - $mutable, - $publicKey - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::sealData( + $readOnly, + $mutable, + $publicKey + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Decrypt a file using anonymous public-key encryption. Ciphertext * integrity is still assured thanks to the Encrypt-then-MAC construction. * - * @param string|resource|ReadOnlyFile $input Input file - * @param string|resource|MutableFile $output Output file + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file * @param EncryptionSecretKey $secretKey Recipient's encryption secret key * @return bool TRUE on success * @@ -309,44 +275,34 @@ public static function seal( * @throws \TypeError */ public static function unseal( - $input, - $output, + string|ReadOnlyFile $input, + string|MutableFile $output, EncryptionSecretKey $secretKey ): bool { - if ( - (\is_resource($input) || \is_string($input) || ($input instanceof ReadOnlyFile)) - && - (\is_resource($output) || \is_string($output) || ($output instanceof MutableFile)) - ) { - try { - if ($input instanceof ReadOnlyFile) { - $readOnly = $input; - } else { - $readOnly = new ReadOnlyFile($input); - } - if ($output instanceof MutableFile) { - $mutable = $output; - } else { - $mutable = new MutableFile($output); - } - $data = self::unsealData( - $readOnly, - $mutable, - $secretKey - ); - return $data; - } finally { - if (isset($readOnly)) { - $readOnly->close(); - } - if (isset($mutable)) { - $mutable->close(); - } + try { + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::unsealData( + $readOnly, + $mutable, + $secretKey + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** @@ -357,9 +313,9 @@ public static function unseal( * Ed25519 public key used as a BLAKE2b key. * 2. Sign the checksum with Ed25519, using the corresponding public key. * - * @param string|resource|ReadOnlyFile $filename File name or file handle + * @param string|ReadOnlyFile $filename File name or ReadOnlyFile object * @param SignatureSecretKey $secretKey Secret key for digital signatures - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * @return string Detached signature for the file * * @throws CannotPerformOperation @@ -371,9 +327,9 @@ public static function unseal( * @throws \TypeError */ public static function sign( - $filename, + string|ReadOnlyFile $filename, SignatureSecretKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($filename instanceof ReadOnlyFile) { $pos = $filename->getPos(); @@ -385,34 +341,27 @@ public static function sign( ); $filename->reset($pos); return $signature; - } - if (\is_resource($filename) || \is_string($filename)) { + } else { $readOnly = new ReadOnlyFile($filename); try { - $signature = self::signData( + return self::signData( $readOnly, $secretKey, $encoding ); - return $signature; } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Verify a digital signature for a file. * - * @param string|resource|ReadOnlyFile $filename File name or file handle + * @param string|ReadOnlyFile $filename File name or ReadOnlyFile object * @param SignaturePublicKey $publicKey Other party's signature public key * @param string $signature The signature we received - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * * @return bool * @throws CannotPerformOperation @@ -425,10 +374,10 @@ public static function sign( * @throws \TypeError */ public static function verify( - $filename, + string|ReadOnlyFile $filename, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { if ($filename instanceof ReadOnlyFile) { $pos = $filename->getPos(); @@ -441,34 +390,27 @@ public static function verify( ); $filename->reset($pos); return $verified; - } - if (\is_resource($filename) || \is_string($filename)) { + } else { $readOnly = new ReadOnlyFile($filename); try { - $verified = self::verifyData( + return self::verifyData( $readOnly, $publicKey, $signature, $encoding ); - return $verified; } finally { - if (isset($readOnly)) { - $readOnly->close(); - } + $readOnly->close(); } } - throw new InvalidType( - 'Argument 1: Expected a filename or resource' - ); } /** * Calculate the BLAKE2b checksum of the contents of a file * * @param StreamInterface $fileStream - * @param Key $key - * @param mixed $encoding Which encoding scheme to use for the checksum? + * @param ?Key $key + * @param string|bool $encoding Which encoding scheme to use for the checksum? * @return string * * @throws CannotPerformOperation @@ -483,7 +425,7 @@ public static function verify( protected static function checksumData( StreamInterface $fileStream, Key $key = null, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = self::getConfig( Halite::HALITE_VERSION_FILE, @@ -1017,15 +959,15 @@ protected static function getConfig( $major = \ord($header[2]); $minor = \ord($header[3]); if ($mode === 'encrypt') { - return new Config( + return new SymmetricConfig( self::getConfigEncrypt($major, $minor) ); } elseif ($mode === 'seal') { - return new Config( + return new SymmetricConfig( self::getConfigSeal($major, $minor) ); } elseif ($mode === 'checksum') { - return new Config( + return new SymmetricConfig( self::getConfigChecksum($major, $minor) ); } @@ -1046,30 +988,30 @@ protected static function getConfig( */ protected static function getConfigEncrypt(int $major, int $minor): array { - - if ($major === 4) { + if ($major === 5) { return [ 'SHORTEST_CIPHERTEXT_LENGTH' => 92, 'BUFFER' => 1048576, 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } elseif ($major === 4) { + return [ + 'SHORTEST_CIPHERTEXT_LENGTH' => 92, + 'BUFFER' => 1048576, + 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'HKDF_SALT_LEN' => 32, + 'MAC_SIZE' => 32, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; - } elseif ($major === 3) { - switch ($minor) { - case 0: - return [ - 'SHORTEST_CIPHERTEXT_LENGTH' => 92, - 'BUFFER' => 1048576, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, - 'HKDF_SALT_LEN' => 32, - 'MAC_SIZE' => 32, - 'HKDF_SBOX' => 'Halite|EncryptionKey', - 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' - ]; - } } // If we reach here, we've got an invalid version tag: // @codeCoverageIgnoreStart @@ -1089,7 +1031,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array */ protected static function getConfigSeal(int $major, int $minor): array { - if ($major === 4) { + if ($major === 5) { switch ($minor) { case 0: return [ @@ -1098,24 +1040,28 @@ protected static function getConfigSeal(int $major, int $minor): array 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; } - } elseif ($major === 3) { - switch ($minor) { - case 0: - return [ - 'SHORTEST_CIPHERTEXT_LENGTH' => 100, - 'BUFFER' => 1048576, - 'HKDF_SALT_LEN' => 32, - 'MAC_SIZE' => 32, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, - 'HKDF_SBOX' => 'Halite|EncryptionKey', - 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' - ]; + } elseif ($major === 4) { + switch ($minor) { + case 0: + return [ + 'SHORTEST_CIPHERTEXT_LENGTH' => 100, + 'BUFFER' => 1048576, + 'HKDF_SALT_LEN' => 32, + 'MAC_SIZE' => 32, + 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } } - } // @codeCoverageIgnoreStart throw new InvalidMessage( 'Invalid version tag' @@ -1133,7 +1079,7 @@ protected static function getConfigSeal(int $major, int $minor): array */ protected static function getConfigChecksum(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 3 || $major === 4 || $major === 5) { switch ($minor) { case 0: return [ @@ -1192,7 +1138,7 @@ protected static function splitKeys( * @param EncryptionKey $encKey * @param string $nonce * @param string $mac (hash context for BLAKE2b) - * @param Config $config + * @param SymmetricConfig $config * * @return int (number of bytes) * @@ -1222,11 +1168,19 @@ private static function streamEncrypt( : (int) $config->BUFFER ); - $encrypted = \sodium_crypto_stream_xor( - $read, - (string) $nonce, - $encKey->getRawKeyMaterial() - ); + if ($config->ENC_ALGO === 'XChaCha20') { + $encrypted = \sodium_crypto_stream_xchacha20_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } else { + $encrypted = \sodium_crypto_stream_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } \sodium_crypto_generichash_update($mac, $encrypted); $written += $output->writeBytes($encrypted); \sodium_increment($nonce); @@ -1258,7 +1212,7 @@ private static function streamEncrypt( * @param EncryptionKey $encKey * @param string $nonce * @param string $mac (hash context for BLAKE2b) - * @param Config $config + * @param SymmetricConfig $config * @param array &$chunk_macs * * @return bool @@ -1277,7 +1231,7 @@ private static function streamDecrypt( EncryptionKey $encKey, string $nonce, string $mac, - Config $config, + SymmetricConfig $config, array &$chunk_macs ): bool { $start = $input->getPos(); @@ -1327,11 +1281,19 @@ private static function streamDecrypt( } // This is where the decryption actually occurs: - $decrypted = \sodium_crypto_stream_xor( - $read, - (string) $nonce, - $encKey->getRawKeyMaterial() - ); + if ($config->ENC_ALGO === 'XChaCha20') { + $decrypted = \sodium_crypto_stream_xchacha20_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } else { + $decrypted = \sodium_crypto_stream_xor( + $read, + (string)$nonce, + $encKey->getRawKeyMaterial() + ); + } $output->writeBytes($decrypted); \sodium_increment($nonce); } diff --git a/src/Halite.php b/src/Halite.php index 9bb37afe..4495898f 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -36,16 +36,16 @@ */ final class Halite { - const VERSION = '4.4.0'; + const VERSION = '5.0.0'; - const HALITE_VERSION_KEYS = "\x31\x40\x04\x00"; - const HALITE_VERSION_FILE = "\x31\x41\x04\x00"; - const HALITE_VERSION = "\x31\x42\x04\x00"; + const HALITE_VERSION_KEYS = "\x31\x40\x05\x00"; + const HALITE_VERSION_FILE = "\x31\x41\x05\x00"; + const HALITE_VERSION = "\x31\x42\x05\x00"; /* Raw bytes (decoded) of the underlying ciphertext */ const VERSION_TAG_LEN = 4; - const VERSION_PREFIX = 'MUIEA'; - const VERSION_OLD_PREFIX = 'MUIDA'; + const VERSION_PREFIX = 'MUIFA'; + const VERSION_OLD_PREFIX = 'MUIEA'; const ENCODE_HEX = 'hex'; const ENCODE_BASE32 = 'base32'; diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 2952ca66..5af1b32a 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -24,6 +24,18 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * @property string ENCODING + * @property int SHORTEST_CIPHERTEXT_LENGTH + * @property int NONCE_BYTES + * @property int HKDF_SALT_LEN + * @property string ENC_ALGO + * @property string MAC_ALGO + * @property int MAC_SIZE + * @property int PUBLICKEY_BYTES + * @property string HKDF_SBOX + * @property string HKDF_AUTH + * @property bool USE_PAE */ final class Config extends BaseConfig { @@ -76,7 +88,24 @@ public static function getConfig( */ public static function getConfigEncrypt(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 5) { + switch ($minor) { + case 0: + return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, + 'SHORTEST_CIPHERTEXT_LENGTH' => 124, + 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'HKDF_SALT_LEN' => 32, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'MAC_ALGO' => 'BLAKE2b', + 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } + } + if ($major === 4 || $major === 3) { switch ($minor) { case 0: return [ @@ -84,6 +113,8 @@ public static function getConfigEncrypt(int $major, int $minor): array 'SHORTEST_CIPHERTEXT_LENGTH' => 124, 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, 'MAC_ALGO' => 'BLAKE2b', 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, 'HKDF_SBOX' => 'Halite|EncryptionKey', @@ -106,10 +137,11 @@ public static function getConfigEncrypt(int $major, int $minor): array */ public static function getConfigAuth(int $major, int $minor): array { - if ($major === 3 || $major === 4) { + if ($major === 4 || $major === 5) { switch ($minor) { case 0: return [ + 'USE_PAE' => $major >= 5, 'HKDF_SALT_LEN' => 32, 'MAC_ALGO' => 'BLAKE2b', 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 241f3cb0..6f137648 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -10,12 +10,7 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{ - Config as BaseConfig, - Halite, - Symmetric\Config as SymmetricConfig, - Util as CryptoUtil -}; +use ParagonIE\Halite\{Config as BaseConfig, Halite, Symmetric\Config as SymmetricConfig, Util as CryptoUtil, Util}; use ParagonIE\HiddenString\HiddenString; /** @@ -100,9 +95,9 @@ public static function authenticate( public static function decrypt( string $ciphertext, EncryptionKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return static::decryptWithAd( + return self::decryptWithAd( $ciphertext, $secretKey, '', @@ -131,7 +126,7 @@ public static function decryptWithAd( string $ciphertext, EncryptionKey $secretKey, string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { $decoder = Halite::chooseEncoder($encoding, true); if (\is_callable($decoder)) { @@ -178,18 +173,29 @@ public static function decryptWithAd( $authKey = $split[1]; // Check the MAC first - if (!self::verifyMAC( + if ($config->USE_PAE) { + $verified = self::verifyMAC( + (string) $auth, + Util::PAE($version, $salt, $nonce, $additionalData, $encrypted), + $authKey, + $config + ); + } else { + $verified = self::verifyMAC( // @codeCoverageIgnoreStart - (string) $auth, - (string) $version . + (string) $auth, + (string) $version . (string) $salt . (string) $nonce . (string) $additionalData . (string) $encrypted, - // @codeCoverageIgnoreEnd - $authKey, - $config - )) { + // @codeCoverageIgnoreEnd + $authKey, + $config + ); + } + + if (!$verified) { throw new InvalidMessage( 'Invalid message authentication code' ); @@ -198,11 +204,11 @@ public static function decryptWithAd( CryptoUtil::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt - $plaintext = \sodium_crypto_stream_xor( - (string) $encrypted, - (string) $nonce, - (string) $encKey - ); + if ($config->ENC_ALGO === 'XChaCha20') { + $plaintext = \sodium_crypto_stream_xchacha20_xor($encrypted, $nonce, $encKey); + } else { + $plaintext = \sodium_crypto_stream_xor($encrypted, $nonce, $encKey); + } CryptoUtil::memzero($encrypted); CryptoUtil::memzero($nonce); CryptoUtil::memzero($encKey); @@ -230,9 +236,9 @@ public static function decryptWithAd( public static function encrypt( HiddenString $plaintext, EncryptionKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - return static::encryptWithAd( + return self::encryptWithAd( $plaintext, $secretKey, '', @@ -244,7 +250,7 @@ public static function encrypt( * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string $additionalData - * @param string|bool $encoding + * @param bool|string $encoding * @return string * * @throws CannotPerformOperation @@ -258,7 +264,7 @@ public static function encryptWithAd( HiddenString $plaintext, EncryptionKey $secretKey, string $additionalData = '', - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); @@ -281,19 +287,42 @@ public static function encryptWithAd( list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: - $encrypted = \sodium_crypto_stream_xor( - $plaintext->getString(), - $nonce, - $encKey - ); + + if ($config->ENC_ALGO === 'XChaCha20') { + $encrypted = \sodium_crypto_stream_xchacha20_xor( + $plaintext->getString(), + $nonce, + $encKey + ); + } else { + $encrypted = \sodium_crypto_stream_xor( + $plaintext->getString(), + $nonce, + $encKey + ); + } CryptoUtil::memzero($encKey); // Calculate an authentication tag: - $auth = self::calculateMAC( - Halite::HALITE_VERSION . $salt . $nonce . $additionalData . $encrypted, - $authKey, - $config - ); + if ($config->USE_PAE) { + $auth = self::calculateMAC( + Util::PAE( + Halite::HALITE_VERSION, + $salt, + $nonce, + $additionalData, + $encrypted + ), + $authKey, + $config + ); + } else { + $auth = self::calculateMAC( + Halite::HALITE_VERSION . $salt . $nonce . $additionalData . $encrypted, + $authKey, + $config + ); + } CryptoUtil::memzero($authKey); /** @var string $message */ diff --git a/src/Util.php b/src/Util.php index 16edcb91..77790af6 100644 --- a/src/Util.php +++ b/src/Util.php @@ -218,6 +218,22 @@ public static function keyed_hash( ); } + /** + * Pre-authentication encoding + * + * @param string ...$pieces + * @return string + */ + public static function PAE(string ...$pieces): string + { + $out = []; + $out[] = pack('P', count($pieces)); + foreach ($pieces as $piece) { + $out[] = pack('P', Binary::safeStrlen($piece)) . $piece; + } + return implode($out); + } + /** * Wrapper around SODIUM_CRypto_generichash() * diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 2a199088..3a132998 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -156,17 +156,6 @@ public function testEncryptFail() unlink(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png'); } - - try { - File::encrypt(true, false, $key); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::decrypt(true, false, $key); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -208,7 +197,7 @@ public function testEncryptSmallFail() file_put_contents( __DIR__.'/tmp/empty.encrypted.txt', - "\x31\x41\x03\x00\x01" + "\x31\x41\x04\x00\x01" ); try { File::decrypt( @@ -224,7 +213,7 @@ public function testEncryptSmallFail() file_put_contents( __DIR__.'/tmp/empty.encrypted.txt', - "\x31\x41\x03\x00" . \str_repeat("\x00", 87) + "\x31\x41\x04\x00" . \str_repeat("\x00", 87) ); try { File::decrypt( @@ -480,17 +469,6 @@ public function testSealFail() unlink(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.open_fail.png'); } - - try { - File::seal(true, false, $publickey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::unseal(true, false, $secretkey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -529,7 +507,7 @@ public function testSealSmallFail() file_put_contents( __DIR__.'/tmp/empty.sealed.txt', - "\x31\x41\x03\x00" . \str_repeat("\x00", 95) + "\x31\x41\x04\x00" . \str_repeat("\x00", 95) ); try { File::unseal( @@ -634,17 +612,6 @@ public function testSign() $signature ) ); - - try { - File::sign(true, $secretkey); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } - try { - File::verify(false, $publickey, ''); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } } /** @@ -734,11 +701,6 @@ public function testChecksum() File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateAuthenticationKey(), true); File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateSignatureKeyPair()->getPublicKey(), true); - try { - File::checksum(false); - $this->fail('Invalid type was accepted.'); - } catch (CryptoException\InvalidType $ex) { - } try { File::checksum(__DIR__.'/tmp/garbage.dat', KeyFactory::generateEncryptionKey()); $this->fail('Invalid type was accepted.'); @@ -785,7 +747,7 @@ public function testOutputToOutputbuffer() ob_start(); File::decrypt( __DIR__.'/tmp/paragon_avatar.encrypted.png', - $stream, + new MutableFile($stream), $key ); $contents = ob_get_clean(); diff --git a/test/unit/PasswordTest.php b/test/unit/PasswordTest.php index dfc1eb75..8962d0dc 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -169,11 +169,11 @@ public function testRehash() try { // Sorry version 1, you get no love from us anymore. - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyA=='; + $legacyHash = 'MUIEAM8F9xoJSz0yBWtA8_DWq0tJM7RuTYPxehbgJ-CW0e-TnJz3-TrZI1ID8gujH' . + '5pQNzejQZEeMwaWlbIgHbpz0OUrITw5Urlv-_RxI4Ih-80uXieWfq0cOp9QqnX9uCO56OsczuPL' . + '5nDCUcTfnG-GnfvH6FkINGBLMkWfzUzaEBNS1zJVcszqle5GEAp6rm9S-BwnCmbKgdigq2rw-Lu' . + 'N_lfcC4Gijx88EwW4D7L7B3r4zyVh4eFjsaU6Djqv5XIxKvH1gJPUToE_Hukd-5dV4wOI9PKtUL' . + 'ZG0w=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -182,7 +182,7 @@ public function testRehash() ); } try { - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . + $legacyHash = 'MUIEAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . 'oPyB=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { @@ -192,7 +192,7 @@ public function testRehash() ); } try { - $legacyHash = 'MUIEAPH'; + $legacyHash = 'MUIFAPH'; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -202,11 +202,11 @@ public function testRehash() } try { - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyB=='; + $legacyHash = 'MUIFAM8F9xoJSz0yBWtA8_DWq0tJM7RuTYPxehbgJ-CW0e-TnJz3-TrZI1ID8gujH' . + '5pQNzejQZEeMwaWlbIgHbpz0OUrITw5Urlv-_RxI4Ih-80uXieWfq0cOp9QqnX9uCO56OsczuPL' . + '5nDCUcTfnG-GnfvH6FkINGBLMkWfzUzaEBNS1zJVcszqle5GEAp6rm9S-BwnCmbKgdigq2rw-Lu' . + 'N_lfcC4Gijx88EwW4D7L7B3r4zyVh4eFjsaU6Djqv5XIxKvH1gJPUToE_Hukd-5dV4wOI9PKtUL' . + 'ZG0w=='; Password::needsRehash($legacyHash, $key); } catch (InvalidMessage $ex) { $this->assertSame( @@ -215,16 +215,6 @@ public function testRehash() ); } - $legacyHash = 'MUIDAPHyUoOjV7zXTOF7nPRJP5KQTw_xOge4F9ytBnm_nqz-oKQ-yjxMRhrRLdM0X' . - '4HrEop9vppxhM6GPnwws9khtStJaQvrU2M6QDjA4VraKkVLMHRkTbLyYGppCbfNYy9iaxsKHaV4' . - 'u9j5NSo3OTiRqiz8WHKLBrQ2ETMfd8iSIaHi1u7NXgT6zTvA8mwRa3a5SrWtHw8fEfVoSt47xTy' . - 'SLnKtpUTU_YoudA4vchbPh05YqexJKmV9PAEtTORzLN3eRiucIixaEJrm4T6rLRrqjMaaOCbUu8' . - 'oPyA=='; - $this->assertTrue( - Password::verify(new HiddenString('test'), $legacyHash, $key), - 'Legacy password hash calculation.' - ); - $hash = Password::hash(new HiddenString('test password'), $key); $this->assertFalse( Password::needsRehash($hash, $key), From 8931e5e1b871d5b78b68cab1d563a8fda853cd3d Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 11:21:51 -0400 Subject: [PATCH 055/134] Add support for AAD in file encryption --- src/Config.php | 11 +++ src/File.php | 145 ++++++++++++++++++++++++++++++++------- src/Symmetric/Config.php | 12 ---- test/unit/FileTest.php | 105 ++++++++++++++++++++++++++-- 4 files changed, 230 insertions(+), 43 deletions(-) diff --git a/src/Config.php b/src/Config.php index f39f1391..3dbeb2b3 100644 --- a/src/Config.php +++ b/src/Config.php @@ -23,6 +23,17 @@ * @property bool CHECKSUM_PUBKEY * @property int BUFFER * @property int HASH_LEN + * @property string ENCODING + * @property int SHORTEST_CIPHERTEXT_LENGTH + * @property int NONCE_BYTES + * @property int HKDF_SALT_LEN + * @property string ENC_ALGO + * @property string MAC_ALGO + * @property int MAC_SIZE + * @property int PUBLICKEY_BYTES + * @property string HKDF_SBOX + * @property string HKDF_AUTH + * @property bool USE_PAE */ class Config { diff --git a/src/File.php b/src/File.php index b64e801a..43cfe98e 100644 --- a/src/File.php +++ b/src/File.php @@ -26,6 +26,7 @@ Symmetric\Config as SymmetricConfig, Symmetric\EncryptionKey }; +use ParagonIE\ConstantTime\Binary; use ParagonIE\HiddenString\HiddenString; /** @@ -113,8 +114,9 @@ public static function checksum( * * @param string|ReadOnlyFile $input Input file * @param string|MutableFile $output Output file - * @param EncryptionKey $key Symmetric encryption key - * @return int Number of bytes written + * @param EncryptionKey $key Symmetric encryption key + * @param string|null $aad Additional authenticated data + * @return int Number of bytes written * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -129,7 +131,8 @@ public static function checksum( public static function encrypt( string|ReadOnlyFile $input, string|MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): int { try { if ($input instanceof ReadOnlyFile) { @@ -145,7 +148,8 @@ public static function encrypt( return self::encryptData( $readOnly, $mutable, - $key + $key, + $aad ); } finally { if (isset($readOnly)) { @@ -162,8 +166,9 @@ public static function encrypt( * * @param string|ReadOnlyFile $input Input file * @param string|MutableFile $output Output file - * @param EncryptionKey $key Symmetric encryption key - * @return bool TRUE if successful + * @param EncryptionKey $key Symmetric encryption key + * @param string|null $aad Additional authenticated data + * @return bool TRUE if successful * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -178,7 +183,8 @@ public static function encrypt( public static function decrypt( string|ReadOnlyFile $input, string|MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): bool { try { if ($input instanceof ReadOnlyFile) { @@ -194,7 +200,8 @@ public static function decrypt( return self::decryptData( $readOnly, $mutable, - $key + $key, + $aad ); } finally { if (isset($readOnly)) { @@ -210,9 +217,10 @@ public static function decrypt( * Encrypt a file using anonymous public-key encryption (with ciphertext * authentication). * - * @param string|ReadOnlyFile $input Input file - * @param string|MutableFile $output Output file - * @param EncryptionPublicKey $publicKey Recipient's encryption public key + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionPublicKey $publicKey Recipient's encryption public key + * @param string|null $aad Additional authenticated data * @return int * * @throws CannotPerformOperation @@ -227,7 +235,8 @@ public static function decrypt( public static function seal( string|ReadOnlyFile $input, string|MutableFile $output, - EncryptionPublicKey $publicKey + EncryptionPublicKey $publicKey, + ?string $aad = null ): int { try { if ($input instanceof ReadOnlyFile) { @@ -243,7 +252,8 @@ public static function seal( return self::sealData( $readOnly, $mutable, - $publicKey + $publicKey, + $aad ); } finally { if (isset($readOnly)) { @@ -259,10 +269,11 @@ public static function seal( * Decrypt a file using anonymous public-key encryption. Ciphertext * integrity is still assured thanks to the Encrypt-then-MAC construction. * - * @param string|ReadOnlyFile $input Input file - * @param string|MutableFile $output Output file - * @param EncryptionSecretKey $secretKey Recipient's encryption secret key - * @return bool TRUE on success + * @param string|ReadOnlyFile $input Input file + * @param string|MutableFile $output Output file + * @param EncryptionSecretKey $secretKey Recipient's encryption secret key + * @param string|null $aad Additional authenticated data + * @return bool TRUE on success * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -277,7 +288,8 @@ public static function seal( public static function unseal( string|ReadOnlyFile $input, string|MutableFile $output, - EncryptionSecretKey $secretKey + EncryptionSecretKey $secretKey, + ?string $aad = null ): bool { try { if ($input instanceof ReadOnlyFile) { @@ -293,7 +305,8 @@ public static function unseal( return self::unsealData( $readOnly, $mutable, - $secretKey + $secretKey, + $aad ); } finally { if (isset($readOnly)) { @@ -497,6 +510,7 @@ protected static function checksumData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionKey $key + * @param string|null $aad Additional authenticated data * @return int * * @throws CannotPerformOperation @@ -513,8 +527,10 @@ protected static function checksumData( protected static function encryptData( ReadOnlyFile $input, MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): int { + /** @var SymmetricConfig $config */ $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'encrypt'); // Generate a nonce and HKDF salt @@ -546,14 +562,35 @@ protected static function encryptData( // VERSION 2+ uses BMAC $mac = \sodium_crypto_generichash_init($authKey); + // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + + // Length followed by piece + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); \sodium_crypto_generichash_update($mac, $firstNonce); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); \sodium_crypto_generichash_update($mac, $hkdfSalt); + + // Optional: AAD support + if ($config->USE_PAE && !is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } /** @var string $mac */ Util::memzero($authKey); Util::memzero($hkdfSalt); + // Prepend with length: + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + return self::streamEncrypt( $input, $output, @@ -572,6 +609,7 @@ protected static function encryptData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionKey $key + * @param string|null $aad Additional authenticated data * @return bool * * @throws CannotPerformOperation @@ -587,7 +625,8 @@ protected static function encryptData( protected static function decryptData( ReadOnlyFile $input, MutableFile $output, - EncryptionKey $key + EncryptionKey $key, + ?string $aad = null ): bool { // Rewind $input->reset(0); @@ -602,6 +641,7 @@ protected static function decryptData( $header = $input->readBytes(Halite::VERSION_TAG_LEN); // Load the config + /** @var SymmetricConfig $config */ $config = self::getConfig($header, 'encrypt'); // Is this shorter than an encrypted empty string? @@ -620,10 +660,29 @@ protected static function decryptData( // VERSION 2+ uses BMAC $mac = \sodium_crypto_generichash_init($authKey); + // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + // Length followed by piece + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); \sodium_crypto_generichash_update($mac, $header); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); \sodium_crypto_generichash_update($mac, $firstNonce); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); \sodium_crypto_generichash_update($mac, $hkdfSalt); - /** @var string $mac */ + + // Optional: AAD support + if ($config->USE_PAE && !is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + + // Prepend with length: + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes() - $config->MAC_SIZE)); $old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config); @@ -659,6 +718,7 @@ protected static function decryptData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionPublicKey $publicKey + * @param ?string $aad * @return int * * @throws CannotPerformOperation @@ -673,7 +733,8 @@ protected static function decryptData( protected static function sealData( ReadOnlyFile $input, MutableFile $output, - EncryptionPublicKey $publicKey + EncryptionPublicKey $publicKey, + ?string $aad = null ): int { // Generate a new keypair for this encryption $ephemeralKeyPair = KeyFactory::generateEncryptionKeyPair(); @@ -728,13 +789,29 @@ protected static function sealData( // VERSION 2+ $mac = \sodium_crypto_generichash_init($authKey); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); // We no longer need $authKey after we set up the hash context Util::memzero($authKey); + // Length followed by piece + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); \sodium_crypto_generichash_update($mac, $hkdfSalt); + if ($config->USE_PAE && !is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + if ($config->USE_PAE) { + \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + } unset($ephPublic); Util::memzero($hkdfSalt); @@ -761,6 +838,8 @@ protected static function sealData( * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionSecretKey $secretKey + * @param ?string $aad + * @param ?string $aad * @return bool * * @throws CannotPerformOperation @@ -776,7 +855,8 @@ protected static function sealData( protected static function unsealData( ReadOnlyFile $input, MutableFile $output, - EncryptionSecretKey $secretKey + EncryptionSecretKey $secretKey, + ?string $aad = null ): bool { $publicKey = $secretKey ->derivePublicKey(); @@ -836,10 +916,27 @@ protected static function unsealData( unset($key); $mac = \sodium_crypto_generichash_init($authKey); + // Number of pieces: + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + // Length followed by piece + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); \sodium_crypto_generichash_update($mac, $header); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); \sodium_crypto_generichash_update($mac, $ephPublic); + if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); \sodium_crypto_generichash_update($mac, $hkdfSalt); + if ($config->USE_PAE && !is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + if ($config->USE_PAE) { + \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes() - $config->MAC_SIZE)); + } /** @var string $mac */ $oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config); diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 5af1b32a..fee11f30 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -24,18 +24,6 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * @property string ENCODING - * @property int SHORTEST_CIPHERTEXT_LENGTH - * @property int NONCE_BYTES - * @property int HKDF_SALT_LEN - * @property string ENC_ALGO - * @property string MAC_ALGO - * @property int MAC_SIZE - * @property int PUBLICKEY_BYTES - * @property string HKDF_SBOX - * @property string HKDF_AUTH - * @property bool USE_PAE */ final class Config extends BaseConfig { diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 3a132998..b93adb7e 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -66,6 +66,62 @@ public function testEncrypt() unlink(__DIR__.'/tmp/paragon_avatar.decrypted.png'); } + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws TypeError + */ + public function testEncryptWithAAD() + { + touch(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png', 0777); + + $key = new EncryptionKey( + new HiddenString(\str_repeat('B', 32)) + ); + $aad = "Additional associated data"; + + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + $key, + $aad + ); + try { + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.decrypted-aad.png', + $key + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.decrypted-aad.png', + $key, + $aad + ); + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted-aad.png') + ); + + unlink(__DIR__.'/tmp/paragon_avatar.encrypted-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.decrypted-aad.png'); + } + /** * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileAccessDenied @@ -304,30 +360,65 @@ public function testSeal() chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); touch(__DIR__.'/tmp/paragon_avatar.opened.png'); chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777); - + $keypair = KeyFactory::generateEncryptionKeyPair(); - $secretkey = $keypair->getSecretKey(); - $publickey = $keypair->getPublicKey(); - + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + File::seal( __DIR__.'/tmp/paragon_avatar.png', __DIR__.'/tmp/paragon_avatar.sealed.png', $publickey ); - + File::unseal( __DIR__.'/tmp/paragon_avatar.sealed.png', __DIR__.'/tmp/paragon_avatar.opened.png', $secretkey ); - + $this->assertSame( hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png') ); - + + // New: Additional Associated Data tests + $aad = "Additional associated data"; + File::seal( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + $publickey, + $aad + ); + try { + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + __DIR__.'/tmp/paragon_avatar.opened-aad.png', + $secretkey + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed-aad.png', + __DIR__.'/tmp/paragon_avatar.opened-aad.png', + $secretkey, + $aad + ); + + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened-aad.png') + ); + unlink(__DIR__.'/tmp/paragon_avatar.sealed.png'); unlink(__DIR__.'/tmp/paragon_avatar.opened.png'); + unlink(__DIR__.'/tmp/paragon_avatar.sealed-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.opened-aad.png'); } /** From 0cd5f26bcbe5800ed72a0e3ac6103ea492d3420c Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 11:52:55 -0400 Subject: [PATCH 056/134] Add asymmetric encryption, cache public keys See #174 --- src/Asymmetric/EncryptionSecretKey.php | 15 +-- src/Asymmetric/SecretKey.php | 7 +- src/Asymmetric/SignatureSecretKey.php | 23 +++-- src/File.php | 134 ++++++++++++++++++++++++- src/KeyFactory.php | 16 ++- test/unit/FileTest.php | 76 ++++++++++++++ 6 files changed, 251 insertions(+), 20 deletions(-) diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index 07105a9c..678f3eca 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -22,14 +22,14 @@ final class EncryptionSecretKey extends SecretKey * @throws InvalidKey * @throws \TypeError */ - public function __construct(HiddenString $keyMaterial) + public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey( 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long' ); } - parent::__construct($keyMaterial); + parent::__construct($keyMaterial, $pk); } /** @@ -39,14 +39,17 @@ public function __construct(HiddenString $keyMaterial) * * @throws InvalidKey * @throws \TypeError + * @throws \SodiumException */ public function derivePublicKey() { - $publicKey = \sodium_crypto_box_publickey_from_secretkey( - $this->getRawKeyMaterial() - ); + if (is_null($this->cachedPublicKey)) { + $this->cachedPublicKey = \sodium_crypto_box_publickey_from_secretkey( + $this->getRawKeyMaterial() + ); + } return new EncryptionPublicKey( - new HiddenString($publicKey) + new HiddenString($this->cachedPublicKey) ); } } diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index a99703e0..8772efe4 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -15,15 +15,20 @@ */ class SecretKey extends Key { + protected ?string $cachedPublicKey = null; + /** * SecretKey constructor. * @param HiddenString $keyMaterial - The actual key data * * @throws \TypeError */ - public function __construct(HiddenString $keyMaterial) + public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { parent::__construct($keyMaterial); + if (!is_null($pk)) { + $this->cachedPublicKey = $pk->getString(); + } $this->isAsymmetricKey = true; } diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index 4ebd1c19..c6204edc 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -24,14 +24,14 @@ final class SignatureSecretKey extends SecretKey * @throws InvalidKey * @throws \TypeError */ - public function __construct(HiddenString $keyMaterial) + public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey( 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long' ); } - parent::__construct($keyMaterial); + parent::__construct($keyMaterial, $pk); $this->isSigningKey = true; } @@ -44,10 +44,12 @@ public function __construct(HiddenString $keyMaterial) */ public function derivePublicKey() { - $publicKey = \sodium_crypto_sign_publickey_from_secretkey( - $this->getRawKeyMaterial() - ); - return new SignaturePublicKey(new HiddenString($publicKey)); + if (is_null($this->cachedPublicKey)) { + $this->cachedPublicKey = \sodium_crypto_sign_publickey_from_secretkey( + $this->getRawKeyMaterial() + ); + } + return new SignaturePublicKey(new HiddenString($this->cachedPublicKey)); } /** @@ -63,6 +65,15 @@ public function getEncryptionSecretKey(): EncryptionSecretKey $x25519_sk = \sodium_crypto_sign_ed25519_sk_to_curve25519( $ed25519_sk ); + if (!is_null($this->cachedPublicKey)) { + $x25519_pk = \sodium_crypto_sign_ed25519_pk_to_curve25519( + $this->cachedPublicKey + ); + return new EncryptionSecretKey( + new HiddenString($x25519_sk), + new HiddenString($x25519_pk) + ); + } return new EncryptionSecretKey( new HiddenString($x25519_sk) ); diff --git a/src/File.php b/src/File.php index 43cfe98e..4a3eee12 100644 --- a/src/File.php +++ b/src/File.php @@ -109,6 +109,134 @@ public static function checksum( throw new InvalidType('Argument 1: Expected a filename'); } + /** + * @param string|ReadOnlyFile $input + * @param string|MutableFile $output + * @param EncryptionPublicKey $recipientPK + * @param EncryptionSecretKey $senderSK + * @param string|null $aad + * @return int + * + * @throws CannotPerformOperation + * @throws FileAccessDenied + * @throws FileError + * @throws FileModified + * @throws InvalidDigestLength + * @throws InvalidKey + * @throws InvalidMessage + * @throws InvalidType + * @throws \SodiumException + */ + public static function asymmetricEncrypt( + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionPublicKey $recipientPK, + EncryptionSecretKey $senderSK, + ?string $aad = null + ): int { + try { + $key = new EncryptionKey( + new HiddenString( + \sodium_crypto_generichash( + \sodium_crypto_scalarmult( + $senderSK->getRawKeyMaterial(), + $recipientPK->getRawKeyMaterial() + ) . + $senderSK->derivePublicKey()->getRawKeyMaterial() . + $recipientPK->getRawKeyMaterial() + ) + ) + ); + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::encryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); + } + } + } + + /** + * @param string|ReadOnlyFile $input + * @param string|MutableFile $output + * @param EncryptionSecretKey $recipientSK + * @param EncryptionPublicKey $senderPK + * @param string|null $aad + * @return bool + * + * @throws CannotPerformOperation + * @throws FileAccessDenied + * @throws FileError + * @throws FileModified + * @throws InvalidDigestLength + * @throws InvalidKey + * @throws InvalidMessage + * @throws InvalidType + * @throws \SodiumException + */ + public static function asymmetricDecrypt( + string|ReadOnlyFile $input, + string|MutableFile $output, + EncryptionSecretKey $recipientSK, + EncryptionPublicKey $senderPK, + ?string $aad = null + ): bool { + try { + $key = new EncryptionKey( + new HiddenString( + sodium_crypto_generichash( + sodium_crypto_scalarmult( + $recipientSK->getRawKeyMaterial(), + $senderPK->getRawKeyMaterial() + ) . + $senderPK->getRawKeyMaterial() . + $recipientSK->derivePublicKey()->getRawKeyMaterial() + ) + ) + ); + if ($input instanceof ReadOnlyFile) { + $readOnly = $input; + } else { + $readOnly = new ReadOnlyFile($input); + } + if ($output instanceof MutableFile) { + $mutable = $output; + } else { + $mutable = new MutableFile($output); + } + return self::decryptData( + $readOnly, + $mutable, + $key, + $aad + ); + } finally { + if (isset($readOnly)) { + $readOnly->close(); + } + if (isset($mutable)) { + $mutable->close(); + } + } + } + /** * Encrypt a file using symmetric authenticated encryption. * @@ -1235,7 +1363,7 @@ protected static function splitKeys( * @param EncryptionKey $encKey * @param string $nonce * @param string $mac (hash context for BLAKE2b) - * @param SymmetricConfig $config + * @param Config $config * * @return int (number of bytes) * @@ -1309,7 +1437,7 @@ private static function streamEncrypt( * @param EncryptionKey $encKey * @param string $nonce * @param string $mac (hash context for BLAKE2b) - * @param SymmetricConfig $config + * @param Config $config * @param array &$chunk_macs * * @return bool @@ -1328,7 +1456,7 @@ private static function streamDecrypt( EncryptionKey $encKey, string $nonce, string $mac, - SymmetricConfig $config, + Config $config, array &$chunk_macs ): bool { $start = $input->getPos(); diff --git a/src/KeyFactory.php b/src/KeyFactory.php index 9d3d72a6..e8240fb3 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -103,12 +103,14 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair // Encryption keypair $kp = \sodium_crypto_box_keypair(); $secretKey = \sodium_crypto_box_secretkey($kp); + $publicKey = \sodium_crypto_box_publickey($kp); // Let's wipe our $kp variable Util::memzero($kp); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -126,12 +128,14 @@ public static function generateSignatureKeyPair(): SignatureKeyPair // Encryption keypair $kp = \sodium_crypto_sign_keypair(); $secretKey = \sodium_crypto_sign_secretkey($kp); + $publicKey = \sodium_crypto_sign_publickey($kp); // Let's wipe our $kp variable Util::memzero($kp); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -269,12 +273,14 @@ public static function deriveEncryptionKeyPair( ); $keyPair = \sodium_crypto_box_seed_keypair($seed); $secretKey = \sodium_crypto_box_secretkey($keyPair); + $publicKey = \sodium_crypto_box_publickey($keyPair); // Let's wipe our $kp variable Util::memzero($keyPair); return new EncryptionKeyPair( new EncryptionSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } @@ -322,12 +328,14 @@ public static function deriveSignatureKeyPair( ); $keyPair = \sodium_crypto_sign_seed_keypair($seed); $secretKey = \sodium_crypto_sign_secretkey($keyPair); + $publicKey = \sodium_crypto_sign_publickey($keyPair); // Let's wipe our $kp variable Util::memzero($keyPair); return new SignatureKeyPair( new SignatureSecretKey( - new HiddenString($secretKey) + new HiddenString($secretKey), + new HiddenString($publicKey) ) ); } diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index b93adb7e..adf32ee8 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -25,6 +25,82 @@ public function setUp(): void } } + /** + * @throws CryptoException\InvalidKey + * @throws SodiumException + */ + public function testAsymmetricEncrypt() + { + touch(__DIR__.'/tmp/paragon_avatar.a-encrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-encrypted.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-decrypted.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-decrypted.png', 0777); + touch(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png'); + chmod(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', 0777); + + $alice = KeyFactory::generateEncryptionKeyPair(); + $aliceSecret = $alice->getSecretKey(); + $alicePublic = $alice->getPublicKey(); + $bob = KeyFactory::generateEncryptionKeyPair(); + $bobSecret = $bob->getSecretKey(); + $bobPublic = $bob->getPublicKey(); + + File::asymmetricEncrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.a-encrypted.png', + $bobPublic, + $aliceSecret + ); + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted.png', + $bobSecret, + $alicePublic + ); + $this->assertSame( + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + hash_file('sha256', __DIR__.'/tmp/paragon_avatar.a-decrypted.png') + ); + + // Now with AAD: + $aad = 'Halite v5 test'; + File::asymmetricEncrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + $bobPublic, + $aliceSecret, + $aad + ); + + try { + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', + $bobSecret, + $alicePublic + ); + } catch (CryptoException\HaliteAlert $ex) { + $this->assertSame( + 'Invalid message authentication code', + $ex->getMessage() + ); + } + File::asymmetricDecrypt( + __DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png', + __DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png', + $bobSecret, + $alicePublic, + $aad + ); + + unlink(__DIR__.'/tmp/paragon_avatar.a-encrypted.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-decrypted.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-encrypted-aad.png'); + unlink(__DIR__.'/tmp/paragon_avatar.a-decrypted-aad.png'); + } + /** * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileAccessDenied From 85aae26657e574c2a9330250f49f2c66fe217ea5 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 11:53:45 -0400 Subject: [PATCH 057/134] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c275ae0f..5f234bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Encryption now uses XChaCha20 instead of XSalsa20. * The `File` class no longer supports the `resource` type. To migrate code, wrap your `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. +* Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. ## Version 4.8.0 (2021-04-18) From b95dafc56e4fbc14d9d8541946ef20ceba51a07b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 11:56:00 -0400 Subject: [PATCH 058/134] Don't attempt on PHP <8 --- .github/workflows/ci.yml | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2b36363..ebb19e60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,44 +3,13 @@ name: CI on: [push, pull_request] jobs: - old: - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: ['ubuntu-latest'] - php-versions: ['7.2', '7.3'] - phpunit-versions: ['latest'] - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, sodium - ini-values: post_max_size=256M, max_execution_time=180 - tools: psalm, phpunit:${{ matrix.phpunit-versions }} - - - name: Install dependencies - run: composer install - - - name: PHPUnit tests - uses: php-actions/phpunit@v2 - timeout-minutes: 30 - with: - memory_limit: 256M - - name: Static Analysis - run: vendor/bin/psalm - modern: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['7.4', '8.0'] + php-versions: ['8.0', '8.1'] phpunit-versions: ['latest'] steps: - name: Checkout From 8ced082f98e44ae59d577df32f0f6dcaf8392cdf Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 20:42:43 -0500 Subject: [PATCH 059/134] Update File.php --- src/File.php | 155 ++++++++++++++++++++++++++++----------------------- 1 file changed, 86 insertions(+), 69 deletions(-) diff --git a/src/File.php b/src/File.php index 4a3eee12..0a7dbde4 100644 --- a/src/File.php +++ b/src/File.php @@ -691,41 +691,42 @@ protected static function encryptData( // VERSION 2+ uses BMAC $mac = \sodium_crypto_generichash_init($authKey); // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 - if ($config->USE_PAE) + if ($config->USE_PAE) { + // Number of pieces: \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); - // Length followed by piece - if ($config->USE_PAE) + // Length followed by piece: \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); - \sodium_crypto_generichash_update($mac, $firstNonce); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $firstNonce); \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - - // Optional: AAD support - if ($config->USE_PAE && !is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + \sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + } else { + // Legacy version: No PAE + \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + \sodium_crypto_generichash_update($mac, $firstNonce); + \sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); } - /** @var string $mac */ Util::memzero($authKey); Util::memzero($hkdfSalt); - // Prepend with length: - if ($config->USE_PAE) - \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); - return self::streamEncrypt( $input, $output, new EncryptionKey( new HiddenString($encKey) ), - (string) $firstNonce, + $firstNonce, (string) $mac, $config ); @@ -788,30 +789,36 @@ protected static function decryptData( // VERSION 2+ uses BMAC $mac = \sodium_crypto_generichash_init($authKey); - // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 - if ($config->USE_PAE) + if ($config->USE_PAE) { + // Number of pieces: \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); - // Length followed by piece - if ($config->USE_PAE) + + // Length followed by piece: \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, $header); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $header); \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); - \sodium_crypto_generichash_update($mac, $firstNonce); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $firstNonce); \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - - // Optional: AAD support - if ($config->USE_PAE && !is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); - } + \sodium_crypto_generichash_update($mac, $hkdfSalt); - // Prepend with length: - if ($config->USE_PAE) - \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes() - $config->MAC_SIZE)); + if (!is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + \sodium_crypto_generichash_update( + $mac, + \pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + ); + } else { + // Legacy version: No PAE + \sodium_crypto_generichash_update($mac, $header); + \sodium_crypto_generichash_update($mac, $firstNonce); + \sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } $old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config); Util::memzero($authKey); @@ -917,28 +924,31 @@ protected static function sealData( // VERSION 2+ $mac = \sodium_crypto_generichash_init($authKey); - if ($config->USE_PAE) - \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); - - // We no longer need $authKey after we set up the hash context Util::memzero($authKey); + if ($config->USE_PAE) { + // Number of pieces: + \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); - // Length followed by piece - if ($config->USE_PAE) + // Length followed by piece: \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - if ($config->USE_PAE && !is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); - } - if ($config->USE_PAE) { + \sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + } else { + // Legacy version: No PAE + \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + \sodium_crypto_generichash_update($mac, $hkdfSalt); + } + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); } unset($ephPublic); @@ -967,7 +977,6 @@ protected static function sealData( * @param MutableFile $output * @param EncryptionSecretKey $secretKey * @param ?string $aad - * @param ?string $aad * @return bool * * @throws CannotPerformOperation @@ -979,6 +988,7 @@ protected static function sealData( * @throws InvalidMessage * @throws InvalidType * @throws \TypeError + * @throws \SodiumException */ protected static function unsealData( ReadOnlyFile $input, @@ -1044,29 +1054,36 @@ protected static function unsealData( unset($key); $mac = \sodium_crypto_generichash_init($authKey); - // Number of pieces: - if ($config->USE_PAE) + + if ($config->USE_PAE) { + // Number of pieces: \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); - // Length followed by piece - if ($config->USE_PAE) + // Length followed by piece: \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, $header); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $header); \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); - \sodium_crypto_generichash_update($mac, $ephPublic); - if ($config->USE_PAE) + \sodium_crypto_generichash_update($mac, $ephPublic); \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - if ($config->USE_PAE && !is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + \sodium_crypto_generichash_update($mac, $hkdfSalt); + if (!is_null($aad)) { + \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); + \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + } + \sodium_crypto_generichash_update( + $mac, + \pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + ); + } else { + // Legacy version: No PAE + \sodium_crypto_generichash_update($mac, $header); + \sodium_crypto_generichash_update($mac, $ephPublic); + \sodium_crypto_generichash_update($mac, $hkdfSalt); } - if ($config->USE_PAE) { - \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes() - $config->MAC_SIZE)); + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); } - /** @var string $mac */ $oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config); // We no longer need these: From cafe11a91e25c3dfe3e6f3988b055a6676df3065 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 20:48:08 -0500 Subject: [PATCH 060/134] Update CHANGELOG, gitignore --- .gitignore | 1 + CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d4fb8272..a82c535d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /composer.lock /composer.phar /.idea/ +/.phpunit.result.cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f234bd6..37543140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ * The `File` class no longer supports the `resource` type. To migrate code, wrap your `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. * Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. +* **Security:** Halite v5 uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) + strategy from PASETO to prevent canonicalization attacks. ## Version 4.8.0 (2021-04-18) From 22a76fd9486aadb06404d49c0b628ee4441c7e87 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 22:05:14 -0500 Subject: [PATCH 061/134] Update coding style for Halite v5 We now prefer explicit imports over global namespace resolution operators (the \ prefix). --- src/Alerts/CannotCloneKey.php | 8 +- src/Alerts/CannotPerformOperation.php | 8 +- src/Alerts/CannotSerializeKey.php | 8 +- src/Alerts/ConfigDirectiveNotFound.php | 8 +- src/Alerts/FileAccessDenied.php | 8 +- src/Alerts/FileError.php | 8 +- src/Alerts/FileModified.php | 8 +- src/Alerts/HaliteAlert.php | 12 +- src/Alerts/HaliteAlertInterface.php | 12 +- src/Alerts/InvalidDigestLength.php | 8 +- src/Alerts/InvalidFlags.php | 8 +- src/Alerts/InvalidKey.php | 8 +- src/Alerts/InvalidMessage.php | 8 +- src/Alerts/InvalidSalt.php | 8 +- src/Alerts/InvalidSignature.php | 8 +- src/Alerts/InvalidType.php | 8 +- src/Asymmetric/Crypto.php | 103 ++++---- src/Asymmetric/EncryptionPublicKey.php | 3 +- src/Asymmetric/EncryptionSecretKey.php | 14 +- src/Asymmetric/PublicKey.php | 3 +- src/Asymmetric/SecretKey.php | 3 +- src/Asymmetric/SignaturePublicKey.php | 14 +- src/Asymmetric/SignatureSecretKey.php | 24 +- src/Config.php | 4 +- src/Contract/StreamInterface.php | 4 +- src/Cookie.php | 34 ++- src/EncryptionKeyPair.php | 4 +- src/File.php | 310 ++++++++++++++----------- src/Halite.php | 19 +- src/KeyFactory.php | 243 ++++++++++--------- src/KeyPair.php | 10 +- src/Password.php | 34 +-- src/SignatureKeyPair.php | 17 +- src/Stream/MutableFile.php | 86 ++++--- src/Stream/ReadOnlyFile.php | 114 ++++----- src/Structure/MerkleTree.php | 54 ++--- src/Structure/Node.php | 10 +- src/Structure/TrimmedMerkleTree.php | 12 +- src/Symmetric/AuthenticationKey.php | 6 +- src/Symmetric/Config.php | 25 +- src/Symmetric/Crypto.php | 140 ++++++----- src/Symmetric/EncryptionKey.php | 6 +- src/Util.php | 102 ++++---- 43 files changed, 830 insertions(+), 704 deletions(-) diff --git a/src/Alerts/CannotCloneKey.php b/src/Alerts/CannotCloneKey.php index 78e73b69..a76d08db 100644 --- a/src/Alerts/CannotCloneKey.php +++ b/src/Alerts/CannotCloneKey.php @@ -1,5 +1,7 @@ getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ) @@ -230,7 +243,7 @@ public static function getSharedSecret( ); } return new HiddenString( - \sodium_crypto_scalarmult( + sodium_crypto_scalarmult( $privateKey->getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ) @@ -242,7 +255,7 @@ public static function getSharedSecret( * * @param HiddenString $plaintext Message to encrypt * @param EncryptionPublicKey $publicKey Public encryption key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? * @return string Ciphertext * * @throws InvalidType @@ -252,9 +265,9 @@ public static function getSharedSecret( public static function seal( HiddenString $plaintext, EncryptionPublicKey $publicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - $sealed = \sodium_crypto_box_seal( + $sealed = sodium_crypto_box_seal( $plaintext->getString(), $publicKey->getRawKeyMaterial() ); @@ -270,19 +283,19 @@ public static function seal( * * @param string $message Message to sign * @param SignatureSecretKey $privateKey Private signing key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? * @return string Signature (detached) * * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function sign( string $message, SignatureSecretKey $privateKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - $signed = \sodium_crypto_sign_detached( + $signed = sodium_crypto_sign_detached( $message, $privateKey->getRawKeyMaterial() ); @@ -307,14 +320,14 @@ public static function sign( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function signAndEncrypt( HiddenString $message, SignatureSecretKey $secretKey, PublicKey $recipientPublicKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($recipientPublicKey instanceof SignaturePublicKey) { $publicKey = $recipientPublicKey->getEncryptionPublicKey(); @@ -344,13 +357,13 @@ public static function signAndEncrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function unseal( string $ciphertext, EncryptionSecretKey $privateKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -358,7 +371,7 @@ public static function unseal( try { /** @var string $ciphertext */ $ciphertext = $decoder($ciphertext); - } catch (\RangeException $ex) { + } catch (RangeException $ex) { throw new InvalidMessage( 'Invalid character encoding' ); @@ -367,8 +380,8 @@ public static function unseal( // Get a box keypair (needed by crypto_box_seal_open) $secret_key = $privateKey->getRawKeyMaterial(); - $public_key = \sodium_crypto_box_publickey_from_secretkey($secret_key); - $key_pair = \sodium_crypto_box_keypair_from_secretkey_and_publickey( + $public_key = sodium_crypto_box_publickey_from_secretkey($secret_key); + $key_pair = sodium_crypto_box_keypair_from_secretkey_and_publickey( $secret_key, $public_key ); @@ -378,14 +391,14 @@ public static function unseal( Util::memzero($public_key); // Now let's open that sealed box - $message = \sodium_crypto_box_seal_open( + $message = sodium_crypto_box_seal_open( $ciphertext, $key_pair ); // Always memzero after retrieving a value Util::memzero($key_pair); - if (!\is_string($message)) { + if (!is_string($message)) { // @codeCoverageIgnoreStart throw new InvalidKey( 'Incorrect secret key for this sealed message' @@ -403,19 +416,19 @@ public static function unseal( * @param string $message Message to verify * @param SignaturePublicKey $publicKey Public key * @param string $signature Signature - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? * @return bool * * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( string $message, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -431,7 +444,7 @@ public static function verify( // @codeCoverageIgnoreEnd } - return (bool) \sodium_crypto_sign_verify_detached( + return sodium_crypto_sign_verify_detached( $signature, $message, $publicKey->getRawKeyMaterial() @@ -460,7 +473,7 @@ public static function verifyAndDecrypt( string $ciphertext, SignaturePublicKey $senderPublicKey, SecretKey $givenSecretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { if ($givenSecretKey instanceof SignatureSecretKey) { $secretKey = $givenSecretKey->getEncryptionSecretKey(); diff --git a/src/Asymmetric/EncryptionPublicKey.php b/src/Asymmetric/EncryptionPublicKey.php index ab1a6b52..2f9ce8c8 100644 --- a/src/Asymmetric/EncryptionPublicKey.php +++ b/src/Asymmetric/EncryptionPublicKey.php @@ -5,6 +5,7 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use const SODIUM_CRYPTO_BOX_PUBLICKEYBYTES; /** * Class EncryptionPublicKey @@ -26,7 +27,7 @@ final class EncryptionPublicKey extends PublicKey */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { throw new InvalidKey( 'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long' ); diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index 678f3eca..4ddc344f 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -5,6 +5,10 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_BOX_SECRETKEYBYTES; +use function sodium_crypto_box_publickey_from_secretkey; /** * Class EncryptionSecretKey @@ -20,11 +24,11 @@ final class EncryptionSecretKey extends SecretKey * EncryptionSecretKey constructor. * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey( 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long' ); @@ -38,13 +42,13 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) * @return EncryptionPublicKey * * @throws InvalidKey - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public function derivePublicKey() { if (is_null($this->cachedPublicKey)) { - $this->cachedPublicKey = \sodium_crypto_box_publickey_from_secretkey( + $this->cachedPublicKey = sodium_crypto_box_publickey_from_secretkey( $this->getRawKeyMaterial() ); } diff --git a/src/Asymmetric/PublicKey.php b/src/Asymmetric/PublicKey.php index 61bba20d..0d9ee986 100644 --- a/src/Asymmetric/PublicKey.php +++ b/src/Asymmetric/PublicKey.php @@ -4,6 +4,7 @@ use ParagonIE\Halite\Key; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class PublicKey @@ -19,7 +20,7 @@ class PublicKey extends Key * PublicKey constructor. * @param HiddenString $keyMaterial - The actual key data * - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index 8772efe4..f85e2f8c 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -4,6 +4,7 @@ use ParagonIE\Halite\Alerts\CannotPerformOperation; use ParagonIE\Halite\Key; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class SecretKey @@ -21,7 +22,7 @@ class SecretKey extends Key * SecretKey constructor. * @param HiddenString $keyMaterial - The actual key data * - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { diff --git a/src/Asymmetric/SignaturePublicKey.php b/src/Asymmetric/SignaturePublicKey.php index d5366781..141a2936 100644 --- a/src/Asymmetric/SignaturePublicKey.php +++ b/src/Asymmetric/SignaturePublicKey.php @@ -5,6 +5,10 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use function sodium_crypto_sign_ed25519_pk_to_curve25519; +use const SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES; /** * Class SignaturePublicKey @@ -22,11 +26,11 @@ final class SignaturePublicKey extends PublicKey * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { throw new InvalidKey( 'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long' ); @@ -39,13 +43,15 @@ public function __construct(HiddenString $keyMaterial) * Get an encryption public key from a signing public key. * * @return EncryptionPublicKey - * @throws \TypeError + * + * @throws SodiumException + * @throws TypeError * @throws InvalidKey */ public function getEncryptionPublicKey(): EncryptionPublicKey { $ed25519_pk = $this->getRawKeyMaterial(); - $x25519_pk = \sodium_crypto_sign_ed25519_pk_to_curve25519( + $x25519_pk = sodium_crypto_sign_ed25519_pk_to_curve25519( $ed25519_pk ); return new EncryptionPublicKey( diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index c6204edc..eee0917f 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -5,6 +5,14 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const + SODIUM_CRYPTO_SIGN_SECRETKEYBYTES; +use function + sodium_crypto_sign_ed25519_sk_to_curve25519, + sodium_crypto_sign_ed25519_pk_to_curve25519, + sodium_crypto_sign_publickey_from_secretkey; /** * Class SignatureSecretKey @@ -22,11 +30,11 @@ final class SignatureSecretKey extends SecretKey * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey( 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long' ); @@ -40,12 +48,13 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) * * @return SignaturePublicKey * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public function derivePublicKey() { if (is_null($this->cachedPublicKey)) { - $this->cachedPublicKey = \sodium_crypto_sign_publickey_from_secretkey( + $this->cachedPublicKey = sodium_crypto_sign_publickey_from_secretkey( $this->getRawKeyMaterial() ); } @@ -57,16 +66,17 @@ public function derivePublicKey() * * @return EncryptionSecretKey * @throws InvalidKey - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public function getEncryptionSecretKey(): EncryptionSecretKey { $ed25519_sk = $this->getRawKeyMaterial(); - $x25519_sk = \sodium_crypto_sign_ed25519_sk_to_curve25519( + $x25519_sk = sodium_crypto_sign_ed25519_sk_to_curve25519( $ed25519_sk ); if (!is_null($this->cachedPublicKey)) { - $x25519_pk = \sodium_crypto_sign_ed25519_pk_to_curve25519( + $x25519_pk = sodium_crypto_sign_ed25519_pk_to_curve25519( $this->cachedPublicKey ); return new EncryptionSecretKey( diff --git a/src/Config.php b/src/Config.php index 3dbeb2b3..4ff81705 100644 --- a/src/Config.php +++ b/src/Config.php @@ -40,7 +40,7 @@ class Config /** * @var array */ - private $config; + private array $config; /** * Config constructor. @@ -74,7 +74,7 @@ public function __get(string $key) * @return bool * @codeCoverageIgnore */ - public function __set(string $key, $value = null) + public function __set(string $key, mixed $value = null) { return false; } diff --git a/src/Contract/StreamInterface.php b/src/Contract/StreamInterface.php index 082b5011..15f39f77 100644 --- a/src/Contract/StreamInterface.php +++ b/src/Contract/StreamInterface.php @@ -68,9 +68,9 @@ public function remainingBytes(): int; * Write to a stream; prevent partial writes * * @param string $buf - * @param int $num (number of bytes) + * @param ?int $num (number of bytes) * @return int * @throws FileAccessDenied */ - public function writeBytes(string $buf, int $num = null): int; + public function writeBytes(string $buf, ?int $num = null): int; } diff --git a/src/Cookie.php b/src/Cookie.php index f3a0dbce..eeaa6279 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -20,6 +20,16 @@ EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use const PHP_VERSION; +use function + hash_equals, + is_string, + json_decode, + json_encode, + setcookie, + version_compare; /** * Class Cookie @@ -75,7 +85,8 @@ public function __debugInfo() * @throws InvalidSignature * @throws CannotPerformOperation * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public function fetch(string $name) { @@ -85,7 +96,7 @@ public function fetch(string $name) try { /** @var string|array|int|float|bool $stored */ $stored = $_COOKIE[$name]; - if (!\is_string($stored)) { + if (!is_string($stored)) { throw new InvalidType('Cookie value is not a string'); } $config = self::getConfig($stored); @@ -94,7 +105,7 @@ public function fetch(string $name) $this->key, $config->ENCODING ); - return \json_decode($decrypted->getString(), true); + return json_decode($decrypted->getString(), true); } catch (InvalidMessage $e) { return null; } @@ -107,7 +118,7 @@ public function fetch(string $name) * @return SymmetricConfig * * @throws InvalidMessage - * @throws \TypeError + * @throws TypeError */ protected static function getConfig(string $stored): SymmetricConfig { @@ -118,7 +129,7 @@ protected static function getConfig(string $stored): SymmetricConfig 'Encrypted password hash is way too short.' ); } - if (\hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { + if (hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( $decoded, @@ -139,14 +150,15 @@ protected static function getConfig(string $stored): SymmetricConfig * @param string $domain (defaults to NULL) * @param bool $secure (defaults to TRUE) * @param bool $httpOnly (defaults to TRUE) - * @param string $samesite (defaults to ''; PHP >= 7.3.0) + * @param string $sameSite (defaults to ''; PHP >= 7.3.0) * @return bool * * @throws InvalidDigestLength * @throws CannotPerformOperation * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * * @psalm-suppress InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument @@ -163,11 +175,11 @@ public function store( ): bool { $val = Crypto::encrypt( new HiddenString( - (string) \json_encode($value) + (string) json_encode($value) ), $this->key ); - if (\version_compare(PHP_VERSION, '7.3.0') >= 0) { + if (version_compare(PHP_VERSION, '7.3.0') >= 0) { $options = [ 'expires' => (int) $expire, 'path' => (string) $path, @@ -178,12 +190,12 @@ public function store( if ($sameSite !== '') { $options['samesite'] = (string) $sameSite; } - return \setcookie( + return setcookie( $name, $val, $options); } - return \setcookie( + return setcookie( $name, $val, (int) $expire, diff --git a/src/EncryptionKeyPair.php b/src/EncryptionKeyPair.php index 6004f5d5..4b675d56 100644 --- a/src/EncryptionKeyPair.php +++ b/src/EncryptionKeyPair.php @@ -30,12 +30,12 @@ final class EncryptionKeyPair extends KeyPair /** * @var EncryptionSecretKey */ - protected $secretKey; + protected Asymmetric\SecretKey $secretKey; /** * @var EncryptionPublicKey */ - protected $publicKey; + protected Asymmetric\PublicKey $publicKey; /** * Pass it a secret key, it will automatically generate a public key diff --git a/src/File.php b/src/File.php index 0a7dbde4..97ad23ca 100644 --- a/src/File.php +++ b/src/File.php @@ -28,6 +28,28 @@ }; use ParagonIE\ConstantTime\Binary; use ParagonIE\HiddenString\HiddenString; +use Exception; +use Error; +use Throwable; +use TypeError; +use SodiumException; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES; +use function + array_shift, + hash_equals, + is_string, + pack, + random_bytes, + sodium_crypto_generichash, + sodium_crypto_generichash_init, + sodium_crypto_generichash_update, + sodium_crypto_generichash_final, + sodium_crypto_scalarmult; /** * Class File @@ -50,12 +72,12 @@ final class File /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -75,7 +97,8 @@ private function __construct() * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function checksum( string|ReadonlyFile $filePath, @@ -125,7 +148,7 @@ public static function checksum( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ public static function asymmetricEncrypt( string|ReadOnlyFile $input, @@ -137,8 +160,8 @@ public static function asymmetricEncrypt( try { $key = new EncryptionKey( new HiddenString( - \sodium_crypto_generichash( - \sodium_crypto_scalarmult( + sodium_crypto_generichash( + sodium_crypto_scalarmult( $senderSK->getRawKeyMaterial(), $recipientPK->getRawKeyMaterial() ) . @@ -189,7 +212,7 @@ public static function asymmetricEncrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ public static function asymmetricDecrypt( string|ReadOnlyFile $input, @@ -254,7 +277,7 @@ public static function asymmetricDecrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ public static function encrypt( string|ReadOnlyFile $input, @@ -306,7 +329,7 @@ public static function encrypt( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ public static function decrypt( string|ReadOnlyFile $input, @@ -357,8 +380,8 @@ public static function decrypt( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \Exception - * @throws \TypeError + * @throws Exception + * @throws TypeError */ public static function seal( string|ReadOnlyFile $input, @@ -411,7 +434,8 @@ public static function seal( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function unseal( string|ReadOnlyFile $input, @@ -465,7 +489,7 @@ public static function unseal( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws TypeError */ public static function sign( string|ReadOnlyFile $filename, @@ -512,7 +536,8 @@ public static function sign( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( string|ReadOnlyFile $filename, @@ -560,8 +585,8 @@ public static function verify( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected static function checksumData( StreamInterface $fileStream, @@ -576,13 +601,13 @@ protected static function checksumData( // 1. Initialize the hash context if ($key instanceof AuthenticationKey) { // AuthenticationKey is for HMAC, but we can use it for keyed hashes too - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( $key->getRawKeyMaterial(), (int) $config->HASH_LEN ); } elseif($config->CHECKSUM_PUBKEY && ($key instanceof SignaturePublicKey)) { // In version 2, we use the public key as a hash key - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( $key->getRawKeyMaterial(), (int) $config->HASH_LEN ); @@ -593,7 +618,7 @@ protected static function checksumData( 'Argument 2: Expected an instance of AuthenticationKey or SignaturePublicKey' ); } else { - $state = \sodium_crypto_generichash_init( + $state = sodium_crypto_generichash_init( '', (int) $config->HASH_LEN ); @@ -611,14 +636,14 @@ protected static function checksumData( // @codeCoverageIgnoreEnd } $read = $fileStream->readBytes($amount_to_read); - \sodium_crypto_generichash_update($state, $read); + sodium_crypto_generichash_update($state, $read); } // 3. Do we want a raw checksum? $encoder = Halite::chooseEncoder($encoding); if ($encoder) { return (string) $encoder( - \sodium_crypto_generichash_final( + sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $state, // @codeCoverageIgnoreEnd @@ -626,7 +651,7 @@ protected static function checksumData( ) ); } - return (string) \sodium_crypto_generichash_final( + return (string) sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $state, // @codeCoverageIgnoreEnd @@ -649,8 +674,8 @@ protected static function checksumData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected static function encryptData( ReadOnlyFile $input, @@ -664,9 +689,9 @@ protected static function encryptData( // Generate a nonce and HKDF salt // @codeCoverageIgnoreStart try { - $firstNonce = \random_bytes((int) $config->NONCE_BYTES); - $hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN); - } catch (\Throwable $ex) { + $firstNonce = random_bytes((int) $config->NONCE_BYTES); + $hkdfSalt = random_bytes((int) $config->HKDF_SALT_LEN); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -681,7 +706,7 @@ protected static function encryptData( ); $output->writeBytes( $firstNonce, - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); $output->writeBytes( $hkdfSalt, @@ -689,29 +714,29 @@ protected static function encryptData( ); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); + $mac = sodium_crypto_generichash_init($authKey); // Number of pieces that go into MAC (header, first nonce, salt, ciphertext) -> 4 if ($config->USE_PAE) { // Number of pieces: - \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); // Length followed by piece: - \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_STREAM_NONCEBYTES)); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); if (!is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); } - \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + sodium_crypto_generichash_update($mac, pack('P', $input->remainingBytes())); } else { // Legacy version: No PAE - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, $hkdfSalt); } if (!is_string($mac)) { throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); @@ -749,7 +774,7 @@ protected static function encryptData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException + * @throws SodiumException */ protected static function decryptData( ReadOnlyFile $input, @@ -788,33 +813,32 @@ protected static function decryptData( list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); + $mac = sodium_crypto_generichash_init($authKey); if ($config->USE_PAE) { // Number of pieces: - \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); // Length followed by piece: - \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_STREAM_NONCEBYTES)); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); - + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_STREAM_NONCEBYTES)); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); if (!is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); } - \sodium_crypto_generichash_update( + sodium_crypto_generichash_update( $mac, - \pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) ); } else { // Legacy version: No PAE - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, $firstNonce); + sodium_crypto_generichash_update($mac, $hkdfSalt); } if (!is_string($mac)) { throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); @@ -862,8 +886,8 @@ protected static function decryptData( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \Exception - * @throws \TypeError + * @throws Exception + * @throws TypeError */ protected static function sealData( ReadOnlyFile $input, @@ -881,7 +905,7 @@ protected static function sealData( $sharedSecretKey = AsymmetricCrypto::getSharedSecret($ephSecret, $publicKey, true); // @codeCoverageIgnoreStart if (!($sharedSecretKey instanceof EncryptionKey)) { - throw new \TypeError('Shared secret is the wrong key type.'); + throw new TypeError('Shared secret is the wrong key type.'); } // @codeCoverageIgnoreEnd @@ -892,14 +916,14 @@ protected static function sealData( $config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal'); // Generate a nonce as per crypto_box_seal - $nonce = \sodium_crypto_generichash( + $nonce = sodium_crypto_generichash( $ephPublic->getRawKeyMaterial() . $publicKey->getRawKeyMaterial(), '', - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // Generate a random HKDF salt - $hkdfSalt = \random_bytes((int) $config->HKDF_SALT_LEN); + $hkdfSalt = random_bytes((int) $config->HKDF_SALT_LEN); // Split the keys /** @@ -915,7 +939,7 @@ protected static function sealData( ); $output->writeBytes( $ephPublic->getRawKeyMaterial(), - \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES ); $output->writeBytes( $hkdfSalt, @@ -923,29 +947,29 @@ protected static function sealData( ); // VERSION 2+ - $mac = \sodium_crypto_generichash_init($authKey); + $mac = sodium_crypto_generichash_init($authKey); Util::memzero($authKey); if ($config->USE_PAE) { // Number of pieces: - \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); // Length followed by piece: - \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); + sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); if (!is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); } - \sodium_crypto_generichash_update($mac, \pack('P', $input->remainingBytes())); + sodium_crypto_generichash_update($mac, pack('P', $input->remainingBytes())); } else { // Legacy version: No PAE - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); + sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); + sodium_crypto_generichash_update($mac, $hkdfSalt); } if (!is_string($mac)) { throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); @@ -987,8 +1011,8 @@ protected static function sealData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected static function unsealData( ReadOnlyFile $input, @@ -1022,10 +1046,10 @@ protected static function unsealData( $hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN); // Generate the same nonce, as per sealData() - $nonce = \sodium_crypto_generichash( + $nonce = sodium_crypto_generichash( $ephPublic . $publicKey->getRawKeyMaterial(), '', - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // Create a key object out of the public key: @@ -1040,7 +1064,7 @@ protected static function unsealData( ); // @codeCoverageIgnoreStart if (!($key instanceof EncryptionKey)) { - throw new \TypeError(); + throw new TypeError(); } // @codeCoverageIgnoreEnd unset($ephemeral); @@ -1053,32 +1077,32 @@ protected static function unsealData( // We no longer need the original key after we split it unset($key); - $mac = \sodium_crypto_generichash_init($authKey); + $mac = sodium_crypto_generichash_init($authKey); if ($config->USE_PAE) { // Number of pieces: - \sodium_crypto_generichash_update($mac, \pack('P', is_null($aad) ? 4 : 5)); + sodium_crypto_generichash_update($mac, pack('P', is_null($aad) ? 4 : 5)); // Length followed by piece: - \sodium_crypto_generichash_update($mac, \pack('P', Halite::VERSION_TAG_LEN)); - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, \pack('P', \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); - \sodium_crypto_generichash_update($mac, $ephPublic); - \sodium_crypto_generichash_update($mac, \pack('P', $config->HKDF_SALT_LEN)); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, pack('P', Halite::VERSION_TAG_LEN)); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, pack('P', SODIUM_CRYPTO_BOX_PUBLICKEYBYTES)); + sodium_crypto_generichash_update($mac, $ephPublic); + sodium_crypto_generichash_update($mac, pack('P', $config->HKDF_SALT_LEN)); + sodium_crypto_generichash_update($mac, $hkdfSalt); if (!is_null($aad)) { - \sodium_crypto_generichash_update($mac, \pack('P', Binary::safeStrlen($aad))); - \sodium_crypto_generichash_update($mac, \pack('P', $aad)); + sodium_crypto_generichash_update($mac, pack('P', Binary::safeStrlen($aad))); + sodium_crypto_generichash_update($mac, pack('P', $aad)); } - \sodium_crypto_generichash_update( + sodium_crypto_generichash_update( $mac, - \pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) + pack('P', $input->remainingBytes() - ((int) $config->MAC_SIZE)) ); } else { // Legacy version: No PAE - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $ephPublic); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + sodium_crypto_generichash_update($mac, $header); + sodium_crypto_generichash_update($mac, $ephPublic); + sodium_crypto_generichash_update($mac, $hkdfSalt); } if (!is_string($mac)) { throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); @@ -1116,7 +1140,7 @@ protected static function unsealData( * * @param ReadOnlyFile $input * @param SignatureSecretKey $secretKey - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * @return string * * @throws CannotPerformOperation @@ -1125,12 +1149,12 @@ protected static function unsealData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws TypeError */ protected static function signData( ReadOnlyFile $input, SignatureSecretKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $checksum = self::checksumData( $input, @@ -1150,7 +1174,7 @@ protected static function signData( * @param $input (file handle) * @param SignaturePublicKey $publicKey * @param string $signature - * @param mixed $encoding Which encoding scheme to use for the signature? + * @param string|bool $encoding Which encoding scheme to use for the signature? * * @return bool * @@ -1161,13 +1185,14 @@ protected static function signData( * @throws InvalidKey * @throws InvalidMessage * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function verifyData( ReadOnlyFile $input, SignaturePublicKey $publicKey, string $signature, - $encoding = Halite::ENCODE_BASE64URLSAFE + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): bool { $checksum = self::checksumData($input, $publicKey, true); return AsymmetricCrypto::verify( @@ -1191,15 +1216,15 @@ protected static function getConfig( string $header, string $mode = 'encrypt' ): Config { - if (\ord($header[0]) !== 49 || \ord($header[1]) !== 65) { + if (Util::chrToInt($header[0]) !== 49 || Util::chrToInt($header[1]) !== 65) { // @codeCoverageIgnoreStart throw new InvalidMessage( 'Invalid version tag' ); // @codeCoverageIgnoreEnd } - $major = \ord($header[2]); - $minor = \ord($header[3]); + $major = Util::chrToInt($header[2]); + $minor = Util::chrToInt($header[3]); if ($mode === 'encrypt') { return new SymmetricConfig( self::getConfigEncrypt($major, $minor) @@ -1234,7 +1259,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array return [ 'SHORTEST_CIPHERTEXT_LENGTH' => 92, 'BUFFER' => 1048576, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, 'ENC_ALGO' => 'XChaCha20', @@ -1246,7 +1271,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array return [ 'SHORTEST_CIPHERTEXT_LENGTH' => 92, 'BUFFER' => 1048576, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, 'ENC_ALGO' => 'XSalsa20', @@ -1281,7 +1306,7 @@ protected static function getConfigSeal(int $major, int $minor): array 'BUFFER' => 1048576, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, 'ENC_ALGO' => 'XChaCha20', 'USE_PAE' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', @@ -1296,7 +1321,7 @@ protected static function getConfigSeal(int $major, int $minor): array 'BUFFER' => 1048576, 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, 'ENC_ALGO' => 'XSalsa20', 'USE_PAE' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', @@ -1327,7 +1352,7 @@ protected static function getConfigChecksum(int $major, int $minor): array return [ 'CHECKSUM_PUBKEY' => true, 'BUFFER' => 1048576, - 'HASH_LEN' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + 'HASH_LEN' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ]; } } @@ -1348,7 +1373,8 @@ protected static function getConfigChecksum(int $major, int $minor): array * * @throws InvalidDigestLength * @throws CannotPerformOperation - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function splitKeys( Key $master, @@ -1359,13 +1385,13 @@ protected static function splitKeys( return [ Util::hkdfBlake2b( $binary, - \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, (string) $config->HKDF_SBOX, $salt ), Util::hkdfBlake2b( $binary, - \SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_AUTH_KEYBYTES, (string) $config->HKDF_AUTH, $salt ) @@ -1388,8 +1414,8 @@ protected static function splitKeys( * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ private static function streamEncrypt( ReadOnlyFile $input, @@ -1411,28 +1437,28 @@ private static function streamEncrypt( ); if ($config->ENC_ALGO === 'XChaCha20') { - $encrypted = \sodium_crypto_stream_xchacha20_xor( + $encrypted = sodium_crypto_stream_xchacha20_xor( $read, (string)$nonce, $encKey->getRawKeyMaterial() ); } else { - $encrypted = \sodium_crypto_stream_xor( + $encrypted = sodium_crypto_stream_xor( $read, (string)$nonce, $encKey->getRawKeyMaterial() ); } - \sodium_crypto_generichash_update($mac, $encrypted); + sodium_crypto_generichash_update($mac, $encrypted); $written += $output->writeBytes($encrypted); - \sodium_increment($nonce); + sodium_increment($nonce); } - if (\is_string($nonce)) { + if (is_string($nonce)) { Util::memzero($nonce); } // Check that our input file was not modified before we MAC it - if (!\hash_equals($input->getHash(), $initHash)) { + if (!hash_equals($input->getHash(), $initHash)) { // @codeCoverageIgnoreStart throw new FileModified( 'Read-only file has been modified since it was opened for reading' @@ -1440,7 +1466,7 @@ private static function streamEncrypt( // @codeCoverageIgnoreEnd } $written += $output->writeBytes( - \sodium_crypto_generichash_final($mac, (int) $config->MAC_SIZE), + sodium_crypto_generichash_final($mac, (int) $config->MAC_SIZE), (int) $config->MAC_SIZE ); return $written; @@ -1464,8 +1490,8 @@ private static function streamEncrypt( * @throws FileAccessDenied * @throws FileModified * @throws InvalidMessage - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ private static function streamDecrypt( ReadOnlyFile $input, @@ -1497,10 +1523,10 @@ private static function streamDecrypt( } // Version 2+ uses a keyed BLAKE2b hash instead of HMAC - \sodium_crypto_generichash_update($mac, $read); + sodium_crypto_generichash_update($mac, $read); /** @var string $mac */ $calcMAC = Util::safeStrcpy($mac); - $calc = \sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE); + $calc = sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE); if (empty($chunk_macs)) { // @codeCoverageIgnoreStart @@ -1511,8 +1537,8 @@ private static function streamDecrypt( // @codeCoverageIgnoreEnd } else { /** @var string $chunkMAC */ - $chunkMAC = \array_shift($chunk_macs); - if (!\hash_equals($chunkMAC, $calc)) { + $chunkMAC = array_shift($chunk_macs); + if (!hash_equals($chunkMAC, $calc)) { // This chunk was altered after the original MAC was verified // @codeCoverageIgnoreStart throw new InvalidMessage( @@ -1524,22 +1550,22 @@ private static function streamDecrypt( // This is where the decryption actually occurs: if ($config->ENC_ALGO === 'XChaCha20') { - $decrypted = \sodium_crypto_stream_xchacha20_xor( + $decrypted = sodium_crypto_stream_xchacha20_xor( $read, (string)$nonce, $encKey->getRawKeyMaterial() ); } else { - $decrypted = \sodium_crypto_stream_xor( + $decrypted = sodium_crypto_stream_xor( $read, (string)$nonce, $encKey->getRawKeyMaterial() ); } $output->writeBytes($decrypted); - \sodium_increment($nonce); + sodium_increment($nonce); } - if (\is_string($nonce)) { + if (is_string($nonce)) { Util::memzero($nonce); } return true; @@ -1558,8 +1584,8 @@ private static function streamDecrypt( * @throws FileAccessDenied * @throws FileModified * @throws InvalidMessage - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ private static function streamVerify( ReadOnlyFile $input, @@ -1595,11 +1621,11 @@ private static function streamVerify( /** * We're updating our HMAC and nothing else */ - \sodium_crypto_generichash_update($mac, $read); + sodium_crypto_generichash_update($mac, $read); $mac = (string) $mac; // Copy the hash state then store the MAC of this chunk $chunkMAC = Util::safeStrcpy($mac); - $chunkMACs []= \sodium_crypto_generichash_final( + $chunkMACs []= sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $chunkMAC, // @codeCoverageIgnoreEnd @@ -1610,7 +1636,7 @@ private static function streamVerify( /** * We should now have enough data to generate an identical MAC */ - $finalHMAC = \sodium_crypto_generichash_final( + $finalHMAC = sodium_crypto_generichash_final( // @codeCoverageIgnoreStart $mac, // @codeCoverageIgnoreEnd @@ -1620,7 +1646,7 @@ private static function streamVerify( /** * Use hash_equals() to be timing-invariant */ - if (!\hash_equals($finalHMAC, $stored_mac)) { + if (!hash_equals($finalHMAC, $stored_mac)) { throw new InvalidMessage( 'Invalid message authentication code' ); diff --git a/src/Halite.php b/src/Halite.php index 4495898f..a55efa3b 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -10,6 +10,7 @@ Hex }; use ParagonIE\Halite\Alerts\InvalidType; +use function extension_loaded, implode; /** * Class Halite @@ -80,7 +81,7 @@ public static function chooseEncoder($chosen, bool $decode = false) if ($chosen === true) { return null; } elseif ($chosen === false) { - return \implode( + return implode( '::', [ Hex::class, @@ -88,7 +89,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32) { - return \implode( + return implode( '::', [ Base32::class, @@ -96,7 +97,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE32HEX) { - return \implode( + return implode( '::', [ Base32Hex::class, @@ -104,7 +105,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64) { - return \implode( + return implode( '::', [ Base64::class, @@ -112,7 +113,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_BASE64URLSAFE) { - return \implode( + return implode( '::', [ Base64UrlSafe::class, @@ -120,7 +121,7 @@ public static function chooseEncoder($chosen, bool $decode = false) ] ); } elseif ($chosen === self::ENCODE_HEX) { - return \implode( + return implode( '::', [ Hex::class, @@ -143,7 +144,7 @@ public static function chooseEncoder($chosen, bool $decode = false) */ public static function isLibsodiumSetupCorrectly(bool $echo = false): bool { - if (!\extension_loaded('sodium')) { + if (!extension_loaded('sodium')) { if ($echo) { echo "You do not have the sodium extension enabled.\n"; } @@ -151,11 +152,11 @@ public static function isLibsodiumSetupCorrectly(bool $echo = false): bool } // Require libsodium 1.0.15 - $major = \SODIUM_LIBRARY_MAJOR_VERSION; + $major = SODIUM_LIBRARY_MAJOR_VERSION; if ($major < 10) { if ($echo) { echo 'Halite needs libsodium 1.0.15 or higher. You have: ', - \SODIUM_LIBRARY_VERSION, "\n"; + SODIUM_LIBRARY_VERSION, "\n"; } return false; } diff --git a/src/KeyFactory.php b/src/KeyFactory.php index e8240fb3..25916450 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -21,6 +21,39 @@ Symmetric\EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_BOX_SEEDBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13, + SODIUM_CRYPTO_PWHASH_SALTBYTES, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_SIGN_SEEDBYTES, + SODIUM_CRYPTO_STREAM_KEYBYTES; +use function + file_get_contents, + file_put_contents, + hash_equals, + is_readable, + random_bytes, + sodium_crypto_box_keypair, + sodium_crypto_box_publickey, + sodium_crypto_box_secretkey, + sodium_crypto_box_seed_keypair, + sodium_crypto_generichash, + sodium_crypto_pwhash, + sodium_crypto_sign_keypair, + sodium_crypto_sign_publickey, + sodium_crypto_sign_secretkey, + sodium_crypto_sign_seed_keypair; /** * Class KeyFactory @@ -46,7 +79,7 @@ final class KeyFactory const SENSITIVE = 'sensitive'; /** - * Generate an an authentication key (symmetric-key cryptography) + * Generate an authentication key (symmetric-key cryptography) * * @return AuthenticationKey * @throws CannotPerformOperation @@ -57,8 +90,8 @@ public static function generateAuthenticationKey(): AuthenticationKey { // @codeCoverageIgnoreStart try { - $secretKey = \random_bytes(\SODIUM_CRYPTO_AUTH_KEYBYTES); - } catch (\Throwable $ex) { + $secretKey = random_bytes(SODIUM_CRYPTO_AUTH_KEYBYTES); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -79,8 +112,8 @@ public static function generateEncryptionKey(): EncryptionKey { // @codeCoverageIgnoreStart try { - $secretKey = \random_bytes(\SODIUM_CRYPTO_STREAM_KEYBYTES); - } catch (\Throwable $ex) { + $secretKey = random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -92,18 +125,18 @@ public static function generateEncryptionKey(): EncryptionKey /** * Generate a key pair for public key encryption * - * @return \ParagonIE\Halite\EncryptionKeyPair + * @return EncryptionKeyPair * * @throws InvalidKey - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public static function generateEncryptionKeyPair(): EncryptionKeyPair { // Encryption keypair - $kp = \sodium_crypto_box_keypair(); - $secretKey = \sodium_crypto_box_secretkey($kp); - $publicKey = \sodium_crypto_box_publickey($kp); + $kp = sodium_crypto_box_keypair(); + $secretKey = sodium_crypto_box_secretkey($kp); + $publicKey = sodium_crypto_box_publickey($kp); // Let's wipe our $kp variable Util::memzero($kp); @@ -120,15 +153,15 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair * * @return SignatureKeyPair * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function generateSignatureKeyPair(): SignatureKeyPair { // Encryption keypair - $kp = \sodium_crypto_sign_keypair(); - $secretKey = \sodium_crypto_sign_secretkey($kp); - $publicKey = \sodium_crypto_sign_publickey($kp); + $kp = sodium_crypto_sign_keypair(); + $secretKey = sodium_crypto_sign_secretkey($kp); + $publicKey = sodium_crypto_sign_publickey($kp); // Let's wipe our $kp variable Util::memzero($kp); @@ -154,8 +187,8 @@ public static function generateSignatureKeyPair(): SignatureKeyPair * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveAuthenticationKey( HiddenString $password, @@ -165,15 +198,15 @@ public static function deriveAuthenticationKey( ): AuthenticationKey { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } - $secretKey = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_AUTH_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_AUTH_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -199,8 +232,8 @@ public static function deriveAuthenticationKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKey( HiddenString $password, @@ -210,15 +243,15 @@ public static function deriveEncryptionKey( ): EncryptionKey { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } - $secretKey = \sodium_crypto_pwhash( - \SODIUM_CRYPTO_STREAM_KEYBYTES, + $secretKey = sodium_crypto_pwhash( + SODIUM_CRYPTO_STREAM_KEYBYTES, $password->getString(), $salt, $kdfLimits[0], @@ -244,8 +277,8 @@ public static function deriveEncryptionKey( * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function deriveEncryptionKeyPair( HiddenString $password, @@ -255,25 +288,25 @@ public static function deriveEncryptionKeyPair( ): EncryptionKeyPair { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } // Diffie Hellman key exchange key pair - $seed = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_BOX_SEEDBYTES, + $seed = sodium_crypto_pwhash( + SODIUM_CRYPTO_BOX_SEEDBYTES, $password->getString(), $salt, $kdfLimits[0], $kdfLimits[1], $alg ); - $keyPair = \sodium_crypto_box_seed_keypair($seed); - $secretKey = \sodium_crypto_box_secretkey($keyPair); - $publicKey = \sodium_crypto_box_publickey($keyPair); + $keyPair = sodium_crypto_box_seed_keypair($seed); + $secretKey = sodium_crypto_box_secretkey($keyPair); + $publicKey = sodium_crypto_box_publickey($keyPair); // Let's wipe our $kp variable Util::memzero($keyPair); @@ -318,17 +351,17 @@ public static function deriveSignatureKeyPair( // @codeCoverageIgnoreEnd } // Digital signature keypair - $seed = @\sodium_crypto_pwhash( - \SODIUM_CRYPTO_SIGN_SEEDBYTES, + $seed = sodium_crypto_pwhash( + SODIUM_CRYPTO_SIGN_SEEDBYTES, $password->getString(), $salt, $kdfLimits[0], $kdfLimits[1], $alg ); - $keyPair = \sodium_crypto_sign_seed_keypair($seed); - $secretKey = \sodium_crypto_sign_secretkey($keyPair); - $publicKey = \sodium_crypto_sign_publickey($keyPair); + $keyPair = sodium_crypto_sign_seed_keypair($seed); + $secretKey = sodium_crypto_sign_secretkey($keyPair); + $publicKey = sodium_crypto_sign_publickey($keyPair); // Let's wipe our $kp variable Util::memzero($keyPair); @@ -360,8 +393,8 @@ public static function getSecurityLevels( return [4, 33554432]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE ]; case self::MODERATE: if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) { @@ -369,8 +402,8 @@ public static function getSecurityLevels( return [6, 134217728]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE ]; case self::SENSITIVE: if ($alg === SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13) { @@ -378,8 +411,8 @@ public static function getSecurityLevels( return [8, 536870912]; } return [ - \SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, - \SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE + SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE ]; default: throw new InvalidType( @@ -395,8 +428,8 @@ public static function getSecurityLevels( * @return AuthenticationKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importAuthenticationKey(HiddenString $keyData): AuthenticationKey { @@ -416,8 +449,8 @@ public static function importAuthenticationKey(HiddenString $keyData): Authentic * @return EncryptionKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey { @@ -437,8 +470,8 @@ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey * @return EncryptionPublicKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionPublicKey(HiddenString $keyData): EncryptionPublicKey { @@ -458,8 +491,8 @@ public static function importEncryptionPublicKey(HiddenString $keyData): Encrypt * @return EncryptionSecretKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionSecretKey(HiddenString $keyData): EncryptionSecretKey { @@ -479,8 +512,8 @@ public static function importEncryptionSecretKey(HiddenString $keyData): Encrypt * @return SignaturePublicKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignaturePublicKey(HiddenString $keyData): SignaturePublicKey { @@ -500,8 +533,8 @@ public static function importSignaturePublicKey(HiddenString $keyData): Signatur * @return SignatureSecretKey * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureSecretKey(HiddenString $keyData): SignatureSecretKey { @@ -521,8 +554,8 @@ public static function importSignatureSecretKey(HiddenString $keyData): Signatur * @return EncryptionKeyPair * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importEncryptionKeyPair(HiddenString $keyData): EncryptionKeyPair { @@ -544,8 +577,8 @@ public static function importEncryptionKeyPair(HiddenString $keyData): Encryptio * @return SignatureKeyPair * * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function importSignatureKeyPair(HiddenString $keyData): SignatureKeyPair { @@ -568,13 +601,13 @@ public static function importSignatureKeyPair(HiddenString $keyData): SignatureK * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadAuthenticationKey(string $filePath): AuthenticationKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -592,13 +625,13 @@ public static function loadAuthenticationKey(string $filePath): AuthenticationKe * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionKey(string $filePath): EncryptionKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -616,13 +649,13 @@ public static function loadEncryptionKey(string $filePath): EncryptionKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionPublicKey(string $filePath): EncryptionPublicKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -664,13 +697,13 @@ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecr * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignaturePublicKey(string $filePath): SignaturePublicKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -688,13 +721,13 @@ public static function loadSignaturePublicKey(string $filePath): SignaturePublic * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignatureSecretKey(string $filePath): SignatureSecretKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -712,13 +745,13 @@ public static function loadSignatureSecretKey(string $filePath): SignatureSecret * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPair { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -738,13 +771,13 @@ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPai * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -764,8 +797,8 @@ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair * * @throws CannotPerformOperation * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function export($key): HiddenString { @@ -777,15 +810,15 @@ public static function export($key): HiddenString return new HiddenString( Hex::encode( Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() . - \sodium_crypto_generichash( + sodium_crypto_generichash( Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(), '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ) ); } - throw new \TypeError('Expected a Key.'); + throw new TypeError('Expected a Key.'); } /** @@ -815,12 +848,12 @@ public static function save($key, string $filename = ''): bool * @return HiddenString * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function loadKeyFile(string $filePath): HiddenString { - $fileData = \file_get_contents($filePath); + $fileData = file_get_contents($filePath); if ($fileData === false) { // @codeCoverageIgnoreStart throw new CannotPerformOperation( @@ -842,8 +875,8 @@ protected static function loadKeyFile(string $filePath): HiddenString * @param string $data * @return string * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function getKeyDataFromString(string $data): string { @@ -851,19 +884,19 @@ public static function getKeyDataFromString(string $data): string $keyData = Binary::safeSubstr( $data, Halite::VERSION_TAG_LEN, - -\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + -SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); $checksum = Binary::safeSubstr( $data, - -\SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + -SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); $calc = \sodium_crypto_generichash( $versionTag . $keyData, '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); - if (!\hash_equals($calc, $checksum)) { + if (!hash_equals($calc, $checksum)) { // @codeCoverageIgnoreStart throw new InvalidKey( 'Checksum validation fail' @@ -884,21 +917,21 @@ public static function getKeyDataFromString(string $data): string * @param string $keyData * @return bool * - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ protected static function saveKeyFile( string $filePath, string $keyData ): bool { - $saved = \file_put_contents( + $saved = file_put_contents( $filePath, Hex::encode( Halite::HALITE_VERSION_KEYS . $keyData . - \sodium_crypto_generichash( + sodium_crypto_generichash( Halite::HALITE_VERSION_KEYS . $keyData, '', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ) ); diff --git a/src/KeyPair.php b/src/KeyPair.php index 1cd462a9..9a62c336 100644 --- a/src/KeyPair.php +++ b/src/KeyPair.php @@ -25,15 +25,9 @@ */ class KeyPair { - /** - * @var SecretKey - */ - protected $secretKey; + protected SecretKey $secretKey; - /** - * @var PublicKey - */ - protected $publicKey; + protected PublicKey $publicKey; /** * Hide this from var_dump(), etc. diff --git a/src/Password.php b/src/Password.php index 6c0b0b70..a9455962 100644 --- a/src/Password.php +++ b/src/Password.php @@ -19,6 +19,12 @@ EncryptionKey }; use ParagonIE\HiddenString\HiddenString; +use SodiumException; +use TypeError; +use function + hash_equals, + sodium_crypto_pwhash_str, + sodium_crypto_pwhash_str_verify; /** * Class Password @@ -51,8 +57,8 @@ final class Password * @throws CannotPerformOperation * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function hash( HiddenString $password, @@ -62,7 +68,7 @@ public static function hash( ): string { $kdfLimits = KeyFactory::getSecurityLevels($level); // First, let's calculate the hash - $hashed = \sodium_crypto_pwhash_str( + $hashed = sodium_crypto_pwhash_str( $password->getString(), $kdfLimits[0], $kdfLimits[1] @@ -91,8 +97,8 @@ public static function hash( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function needsRehash( string $stored, @@ -114,7 +120,7 @@ public static function needsRehash( )->getString(); // Upon successful decryption, verify that we're using Argon2id - if (!\hash_equals( + if (!hash_equals( Binary::safeSubstr($hash_str, 0, 10), \SODIUM_CRYPTO_PWHASH_STRPREFIX )) { @@ -124,17 +130,17 @@ public static function needsRehash( // Parse the cost parameters: switch ($level) { case KeyFactory::INTERACTIVE: - return !\hash_equals( + return !hash_equals( '$argon2id$v=19$m=65536,t=2,p=1$', Binary::safeSubstr($hash_str, 0, 31) ); case KeyFactory::MODERATE: - return !\hash_equals( + return !hash_equals( '$argon2id$v=19$m=262144,t=3,p=1$', Binary::safeSubstr($hash_str, 0, 32) ); case KeyFactory::SENSITIVE: - return !\hash_equals( + return !hash_equals( '$argon2id$v=19$m=1048576,t=4,p=1$', Binary::safeSubstr($hash_str, 0, 33) ); @@ -161,9 +167,9 @@ protected static function getConfig(string $stored): SymmetricConfig ); } if ( - \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX) + hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX) || - \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) + hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) ) { $decoded = Base64UrlSafe::decode($stored); return SymmetricConfig::getConfig( @@ -191,8 +197,8 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( HiddenString $password, @@ -210,7 +216,7 @@ public static function verify( // First let's decrypt the hash $hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $config->ENCODING); // Upon successful decryption, verify the password is correct - return \sodium_crypto_pwhash_str_verify( + return sodium_crypto_pwhash_str_verify( $hash_str->getString(), $password->getString() ); diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index d866b159..4dc94036 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -8,6 +8,9 @@ SignatureSecretKey }; use ParagonIE\HiddenString\HiddenString; +use InvalidArgumentException; +use TypeError; +use function count; /** * Class SignatureKeyPair @@ -30,12 +33,12 @@ final class SignatureKeyPair extends KeyPair /** * @var SignatureSecretKey */ - protected $secretKey; + protected Asymmetric\SecretKey $secretKey; /** * @var SignaturePublicKey */ - protected $publicKey; + protected Asymmetric\PublicKey $publicKey; /** * Pass it a secret key, it will automatically generate a public key @@ -43,11 +46,11 @@ final class SignatureKeyPair extends KeyPair * @param array $keys * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(Key ...$keys) { - switch (\count($keys)) { + switch (count($keys)) { /** * If we received two keys, it must be an asymmetric secret key and * an asymmetric public key, in either order. @@ -115,7 +118,7 @@ public function __construct(Key ...$keys) ); break; default: - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Halite\\EncryptionKeyPair expects 1 or 2 keys' ); } @@ -124,7 +127,7 @@ public function __construct(Key ...$keys) /** * @return EncryptionKeyPair * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function getEncryptionKeyPair(): EncryptionKeyPair { @@ -141,7 +144,7 @@ public function getEncryptionKeyPair(): EncryptionKeyPair * @return void * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ protected function setupKeyPair(SignatureSecretKey $secret): void { diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index acdcb9b6..ba6299e7 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -9,6 +9,26 @@ FileAccessDenied, InvalidType }; +use TypeError; +use function + clearstatcache, + file_exists, + fclose, + fopen, + fread, + fseek, + fstat, + ftell, + fwrite, + in_array, + is_int, + is_readable, + is_resource, + is_string, + is_writable, + min, + stream_get_meta_data, + touch; /** * Class MutableFile @@ -34,7 +54,7 @@ class MutableFile implements StreamInterface /** * @var bool */ - private $closeAfter = false; + private bool $closeAfter = false; /** * @var resource @@ -44,7 +64,7 @@ class MutableFile implements StreamInterface /** * @var int */ - private $pos; + private int $pos; /** * @var array @@ -60,28 +80,28 @@ class MutableFile implements StreamInterface */ public function __construct($file) { - if (\is_string($file)) { - if (!\file_exists($file)) { - if (!\is_writable(\dirname($file))) { + if (is_string($file)) { + if (!file_exists($file)) { + if (!is_writable(dirname($file))) { throw new FileAccessDenied( 'Could not write to directory that contains file' ); } - \touch($file); // Make the file exist + touch($file); // Make the file exist } - if (!\is_readable($file)) { + if (!is_readable($file)) { throw new FileAccessDenied( 'Could not open file for reading' ); } - if (!\is_writable($file)) { + if (!is_writable($file)) { throw new FileAccessDenied( 'Could not open file for writing' ); } - $fp = \fopen($file, 'w+b'); + $fp = fopen($file, 'w+b'); // @codeCoverageIgnoreStart - if (!\is_resource($fp)) { + if (!is_resource($fp)) { throw new FileAccessDenied( 'Could not open file for reading' ); @@ -90,18 +110,18 @@ public function __construct($file) $this->fp = $fp; $this->closeAfter = true; $this->pos = 0; - $this->stat = \fstat($this->fp); - } elseif (\is_resource($file)) { + $this->stat = fstat($this->fp); + } elseif (is_resource($file)) { /** @var array $metadata */ - $metadata = \stream_get_meta_data($file); - if (!\in_array($metadata['mode'], self::ALLOWED_MODES, true)) { + $metadata = stream_get_meta_data($file); + if (!in_array($metadata['mode'], self::ALLOWED_MODES, true)) { throw new FileAccessDenied( 'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.' ); } $this->fp = $file; - $this->pos = \ftell($this->fp); - $this->stat = \fstat($this->fp); + $this->pos = ftell($this->fp); + $this->stat = fstat($this->fp); } else { throw new InvalidType( 'Argument 1: Expected a filename or resource' @@ -118,8 +138,8 @@ public function close(): void { if ($this->closeAfter) { $this->closeAfter = false; - \fclose($this->fp); - \clearstatcache(); + fclose($this->fp); + clearstatcache(); } } @@ -138,7 +158,7 @@ public function __destruct() */ public function getPos(): int { - return \ftell($this->fp); + return ftell($this->fp); } /** @@ -148,7 +168,7 @@ public function getPos(): int */ public function getSize(): int { - $stat = \fstat($this->fp); + $stat = fstat($this->fp); return (int) $stat['size']; } @@ -159,7 +179,7 @@ public function getSize(): int */ public function getStreamMetadata(): array { - return \stream_get_meta_data($this->fp); + return stream_get_meta_data($this->fp); } /** @@ -191,10 +211,10 @@ public function readBytes(int $num, bool $skipTests = false): string break; // @codeCoverageIgnoreEnd } - $bufSize = \min($remaining, self::CHUNK); + $bufSize = min($remaining, self::CHUNK); /** @var string|bool $read */ - $read = \fread($this->fp, $bufSize); - if (!\is_string($read)) { + $read = fread($this->fp, $bufSize); + if (!is_string($read)) { // @codeCoverageIgnoreStart throw new FileAccessDenied( 'Could not read from the file' @@ -217,9 +237,9 @@ public function readBytes(int $num, bool $skipTests = false): string public function remainingBytes(): int { /** @var array $stat */ - $stat = \fstat($this->fp); + $stat = fstat($this->fp); /** @var int $pos */ - $pos = \ftell($this->fp); + $pos = ftell($this->fp); return (int) ( PHP_INT_MAX & ( (int) $stat['size'] - $pos @@ -238,7 +258,7 @@ public function remainingBytes(): int public function reset(int $i = 0): bool { $this->pos = $i; - if (\fseek($this->fp, $i, SEEK_SET) === 0) { + if (fseek($this->fp, $i, SEEK_SET) === 0) { return true; } throw new CannotPerformOperation( @@ -250,17 +270,17 @@ public function reset(int $i = 0): bool * Write to a stream; prevent partial writes * * @param string $buf - * @param int|null $num (number of bytes) + * @param ?int $num (number of bytes) * @return int * * @throws CannotPerformOperation * @throws FileAccessDenied - * @throws \TypeError + * @throws TypeError */ - public function writeBytes(string $buf, int $num = null): int + public function writeBytes(string $buf, ?int $num = null): int { $bufSize = Binary::safeStrlen($buf); - if (!\is_int($num) || $num > $bufSize) { + if (!is_int($num) || $num > $bufSize) { $num = $bufSize; } // @codeCoverageIgnoreStart @@ -275,7 +295,7 @@ public function writeBytes(string $buf, int $num = null): int break; } // @codeCoverageIgnoreEnd - $written = \fwrite($this->fp, $buf, $remaining); + $written = fwrite($this->fp, $buf, $remaining); if ($written === false) { // @codeCoverageIgnoreStart throw new FileAccessDenied( @@ -285,7 +305,7 @@ public function writeBytes(string $buf, int $num = null): int } $buf = Binary::safeSubstr($buf, $written, null); $this->pos += $written; - $this->stat = \fstat($this->fp); + $this->stat = fstat($this->fp); $remaining -= $written; } while ($remaining > 0); return $num; diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 40e48f89..5506f3cd 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -12,6 +12,27 @@ InvalidType, }; use ParagonIE\Halite\Key; +use SodiumException; +use TypeError; +use const + SEEK_SET, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX; +use function + clearstatcache, + fclose, + fopen, + fread, + fseek, + fstat, + ftell, + in_array, + is_readable, + is_resource, + is_string, + sodium_crypto_generichash_init, + sodium_crypto_generichash_update, + sodium_crypto_generichash_final, + stream_get_meta_data; /** * Class ReadOnlyFile @@ -32,35 +53,17 @@ class ReadOnlyFile implements StreamInterface const ALLOWED_MODES = ['rb']; const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default - /** - * @var bool - */ - private $closeAfter = false; + private bool $closeAfter = false; /** * @var resource */ private $fp; - /** - * @var string - */ - private $hash; - - /** - * @var int - */ - private $pos = 0; - - /** - * @var null|string - */ - private $hashKey = null; - - /** - * @var array - */ - private $stat = []; + private string $hash = ''; + private int $pos = 0; + private ?string $hashKey = null; + private array $stat = []; /** * ReadOnlyFile constructor. @@ -71,21 +74,22 @@ class ReadOnlyFile implements StreamInterface * @throws FileAccessDenied * @throws FileError * @throws InvalidType - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @psalm-suppress RedundantConditionGivenDocblockType */ public function __construct($file, Key $key = null) { - if (\is_string($file)) { - if (!\is_readable($file)) { + if (is_string($file)) { + if (!is_readable($file)) { throw new FileAccessDenied( 'Could not open file for reading' ); } /** @var resource|bool $fp */ - $fp = \fopen($file, 'rb'); + $fp = fopen($file, 'rb'); // @codeCoverageIgnoreStart - if (!\is_resource($fp)) { + if (!is_resource($fp)) { throw new FileAccessDenied( 'Could not open file for reading' ); @@ -96,16 +100,16 @@ public function __construct($file, Key $key = null) $this->closeAfter = true; $this->pos = 0; $this->stat = $this->fstat(); - } elseif (\is_resource($file)) { + } elseif (is_resource($file)) { /** @var array $metadata */ - $metadata = \stream_get_meta_data($file); - if (!\in_array($metadata['mode'], (array) static::ALLOWED_MODES, true)) { + $metadata = stream_get_meta_data($file); + if (!in_array($metadata['mode'], (array) static::ALLOWED_MODES, true)) { throw new FileAccessDenied( 'Resource is in ' . $metadata['mode'] . ' mode, which is not allowed.' ); } $this->fp = $file; - $this->pos = \ftell($this->fp); + $this->pos = ftell($this->fp); $this->stat = $this->fstat(); } else { throw new InvalidType( @@ -138,8 +142,8 @@ public function close(): void { if ($this->closeAfter) { $this->closeAfter = false; - \fclose($this->fp); - \clearstatcache(); + fclose($this->fp); + clearstatcache(); } } @@ -158,29 +162,29 @@ public function getHash(): string return $this->hash; } $init = $this->pos; - \fseek($this->fp, 0, SEEK_SET); + fseek($this->fp, 0, SEEK_SET); // Create a hash context: - $h = \sodium_crypto_generichash_init( + $h = sodium_crypto_generichash_init( $this->hashKey, - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); for ($i = 0; $i < $this->stat['size']; $i += self::CHUNK) { if (($i + self::CHUNK) > $this->stat['size']) { - $c = \fread($this->fp, ((int) $this->stat['size'] - $i)); + $c = fread($this->fp, ((int) $this->stat['size'] - $i)); } else { - $c = \fread($this->fp, self::CHUNK); + $c = fread($this->fp, self::CHUNK); } - if (!\is_string($c)) { + if (!is_string($c)) { // @codeCoverageIgnoreStart throw new FileError('Could not read file'); // @codeCoverageIgnoreEnd } - \sodium_crypto_generichash_update($h, $c); + sodium_crypto_generichash_update($h, $c); } // Reset the file pointer's internal cursor to where it was: - \fseek($this->fp, $init, SEEK_SET); - return \sodium_crypto_generichash_final($h); + fseek($this->fp, $init, SEEK_SET); + return sodium_crypto_generichash_final($h); } /** @@ -210,7 +214,7 @@ public function getSize(): int */ public function getStreamMetadata(): array { - return \stream_get_meta_data($this->fp); + return stream_get_meta_data($this->fp); } /** @@ -252,8 +256,8 @@ public function readBytes(int $num, bool $skipTests = false): string } // @codeCoverageIgnoreEnd /** @var string|bool $read */ - $read = \fread($this->fp, $remaining); - if (!\is_string($read)) { + $read = fread($this->fp, $remaining); + if (!is_string($read)) { // @codeCoverageIgnoreStart throw new FileAccessDenied( 'Could not read from the file' @@ -292,7 +296,7 @@ public function remainingBytes(): int public function reset(int $position = 0): bool { $this->pos = $position; - if (\fseek($this->fp, $position, SEEK_SET) === 0) { + if (fseek($this->fp, $position, SEEK_SET) === 0) { return true; } // @codeCoverageIgnoreStart @@ -310,9 +314,9 @@ public function reset(int $position = 0): bool * @throws FileModified * @return void */ - public function toctouTest() + public function toctouTest(): void { - if (\ftell($this->fp) !== $this->pos) { + if (ftell($this->fp) !== $this->pos) { // @codeCoverageIgnoreStart throw new FileModified( 'Read-only file has been modified since it was opened for reading' @@ -331,11 +335,11 @@ public function toctouTest() * This is a meaningless operation for a Read-Only File! * * @param string $buf - * @param int $num (number of bytes) + * @param ?int $num (number of bytes) * @return int * @throws FileAccessDenied */ - public function writeBytes(string $buf, int $num = null): int + public function writeBytes(string $buf, ?int $num = null): int { unset($buf); unset($num); @@ -350,7 +354,7 @@ public function writeBytes(string $buf, int $num = null): int * @return array */ private function fstat() : array { - $stat = \fstat($this->fp); + $stat = fstat($this->fp); if ($stat) { return $stat; } @@ -358,11 +362,11 @@ private function fstat() : array { $stat = [ 'size' => 0, ]; - \fseek($this->fp, 0); + fseek($this->fp, 0); while (!feof($this->fp)) { - $stat['size'] += \strlen(\fread($this->fp, 8192)); + $stat['size'] += Binary::safeStrlen(fread($this->fp, self::CHUNK)); } - \fseek($this->fp, $this->pos); + fseek($this->fp, $this->pos); return $stat; } } diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 65b3e7a3..502eef2a 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -8,6 +8,15 @@ InvalidDigestLength }; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_GENERICHASH_BYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN; +use function + array_shift, + count, + sprintf; /** * Class MerkleTree @@ -31,30 +40,23 @@ class MerkleTree const MERKLE_LEAF = "\x01"; const MERKLE_BRANCH = "\x00"; - /** - * @var bool - */ - protected $rootCalculated = false; - - /** - * @var string - */ - protected $root = ''; + protected bool $rootCalculated = false; + protected string $root = ''; /** * @var Node[] */ - protected $nodes = []; + protected array $nodes = []; /** * @var string */ - protected $personalization = ''; + protected string $personalization = ''; /** * @var int */ - protected $outputSize = \SODIUM_CRYPTO_GENERICHASH_BYTES; + protected int $outputSize = SODIUM_CRYPTO_GENERICHASH_BYTES; /** * Instantiate a Merkle tree @@ -73,8 +75,8 @@ public function __construct(Node ...$nodes) * * @return string * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public function getRoot(bool $raw = false): string { @@ -113,19 +115,19 @@ public function getExpandedTree(Node ...$nodes): MerkleTree */ public function setHashSize(int $size): self { - if ($size < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { + if ($size < SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { throw new InvalidDigestLength( - \sprintf( + sprintf( 'Merkle roots must be at least %d long.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN ) ); } - if ($size > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { + if ($size > SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { throw new InvalidDigestLength( - \sprintf( + sprintf( 'Merkle roots must be at most %d long.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ); } @@ -173,12 +175,12 @@ public function triggerRootCalculation(): self * * @return string * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ protected function calculateRoot(): string { - $size = \count($this->nodes); + $size = count($this->nodes); if ($size < 1) { return ''; } @@ -239,11 +241,9 @@ protected function calculateRoot(): string $hash = $tmp; $order >>= 1; } while ($order > 1); - // We should only have one value left:t + // We should only have one value left: $this->rootCalculated = true; - /** @var string $first */ - $first = \array_shift($hash); - return $first; + return (string) array_shift($hash); } /** diff --git a/src/Structure/Node.php b/src/Structure/Node.php index 034044d6..622cd1d8 100644 --- a/src/Structure/Node.php +++ b/src/Structure/Node.php @@ -4,6 +4,9 @@ use ParagonIE\Halite\Alerts\CannotPerformOperation; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use const SODIUM_CRYPTO_GENERICHASH_BYTES; /** * Class Node @@ -24,7 +27,7 @@ class Node /** * @var string */ - private $data; + private string $data; /** * Node constructor. @@ -56,11 +59,12 @@ public function getData(): string * * @return string * @throws CannotPerformOperation - * @throws \TypeError + * @throws TypeError + * @throws SodiumException */ public function getHash( bool $raw = false, - int $outputSize = \SODIUM_CRYPTO_GENERICHASH_BYTES, + int $outputSize = SODIUM_CRYPTO_GENERICHASH_BYTES, string $personalization = '' ): string { if ($raw) { diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index a3944505..398c4dee 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -7,6 +7,9 @@ InvalidDigestLength }; use ParagonIE\Halite\Util; +use SodiumException; +use TypeError; +use function count; /** * Class TrimmedMerkleTree @@ -37,12 +40,13 @@ class TrimmedMerkleTree extends MerkleTree * * @return string * @throws CannotPerformOperation - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @psalm-suppress EmptyArrayAccess Psalm is misreading array elements */ protected function calculateRoot(): string { - $size = \count($this->nodes); + $size = count($this->nodes); if ($size < 1) { return ''; } @@ -84,9 +88,7 @@ protected function calculateRoot(): string // We should only have one value left: $this->rootCalculated = true; - /** @var string $first */ - $first = \array_shift($hash); - return $first; + return (string) array_shift($hash); } /** diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index ff990a16..b4b932fe 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -5,6 +5,8 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use const SODIUM_CRYPTO_AUTH_KEYBYTES; +use TypeError; /** * Class AuthenticationKey @@ -21,11 +23,11 @@ final class AuthenticationKey extends SecretKey * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_AUTH_KEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey( 'Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long' ); diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index fee11f30..5685cbb5 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -6,8 +6,13 @@ use ParagonIE\Halite\Alerts\InvalidMessage; use ParagonIE\Halite\{ Config as BaseConfig, - Halite + Halite, + Util }; +use const + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_STREAM_NONCEBYTES; /** * Class Config @@ -45,13 +50,13 @@ public static function getConfig( 'Invalid version tag' ); } - if (\ord($header[0]) !== 49 || \ord($header[1]) !== 66) { + if (Util::chrToInt($header[0]) !== 49 || Util::chrToInt($header[1]) !== 66) { throw new InvalidMessage( 'Invalid version tag' ); } - $major = \ord($header[2]); - $minor = \ord($header[3]); + $major = Util::chrToInt($header[2]); + $minor = Util::chrToInt($header[3]); if ($mode === 'encrypt') { return new Config( self::getConfigEncrypt($major, $minor) @@ -82,12 +87,12 @@ public static function getConfigEncrypt(int $major, int $minor): array return [ 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, 'SHORTEST_CIPHERTEXT_LENGTH' => 124, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'ENC_ALGO' => 'XChaCha20', 'USE_PAE' => true, 'MAC_ALGO' => 'BLAKE2b', - 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -99,12 +104,12 @@ public static function getConfigEncrypt(int $major, int $minor): array return [ 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, 'SHORTEST_CIPHERTEXT_LENGTH' => 124, - 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, + 'NONCE_BYTES' => SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, 'ENC_ALGO' => 'XSalsa20', 'USE_PAE' => false, 'MAC_ALGO' => 'BLAKE2b', - 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -132,8 +137,8 @@ public static function getConfigAuth(int $major, int $minor): array 'USE_PAE' => $major >= 5, 'HKDF_SALT_LEN' => 32, 'MAC_ALGO' => 'BLAKE2b', - 'MAC_SIZE' => \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, - 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 6f137648..a696871b 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -10,8 +10,30 @@ InvalidSignature, InvalidType }; -use ParagonIE\Halite\{Config as BaseConfig, Halite, Symmetric\Config as SymmetricConfig, Util as CryptoUtil, Util}; +use ParagonIE\Halite\{ + Config as BaseConfig, + Halite, + Symmetric\Config as SymmetricConfig, + Util +}; use ParagonIE\HiddenString\HiddenString; +use Error; +use RangeException; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES; +use function + hash_equals, + is_callable, + is_null, + random_bytes, + sodium_crypto_generichash, + sodium_crypto_stream_xchacha20_xor, + sodium_crypto_stream_xor; /** * Class Crypto @@ -34,12 +56,12 @@ final class Crypto /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ final private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -47,18 +69,18 @@ final private function __construct() * * @param string $message * @param AuthenticationKey $secretKey - * @param mixed $encoding + * @param string|bool $encoding * @return string * * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function authenticate( string $message, AuthenticationKey $secretKey, - $encoding = Halite::ENCODE_BASE64URLSAFE + bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = SymmetricConfig::getConfig( Halite::HALITE_VERSION, @@ -89,8 +111,8 @@ public static function authenticate( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function decrypt( string $ciphertext, @@ -119,8 +141,8 @@ public static function decrypt( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function decryptWithAd( string $ciphertext, @@ -129,13 +151,13 @@ public static function decryptWithAd( bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { $decoder = Halite::chooseEncoder($encoding, true); - if (\is_callable($decoder)) { + if (is_callable($decoder)) { // We were given encoded data: // @codeCoverageIgnoreStart try { /** @var string $ciphertext */ $ciphertext = $decoder($ciphertext); - } catch (\RangeException $ex) { + } catch (RangeException $ex) { throw new InvalidMessage( 'Invalid character encoding' ); @@ -200,18 +222,18 @@ public static function decryptWithAd( 'Invalid message authentication code' ); } - CryptoUtil::memzero($salt); - CryptoUtil::memzero($authKey); + Util::memzero($salt); + Util::memzero($authKey); // crypto_stream_xor() can be used to encrypt and decrypt if ($config->ENC_ALGO === 'XChaCha20') { - $plaintext = \sodium_crypto_stream_xchacha20_xor($encrypted, $nonce, $encKey); + $plaintext = sodium_crypto_stream_xchacha20_xor($encrypted, $nonce, $encKey); } else { - $plaintext = \sodium_crypto_stream_xor($encrypted, $nonce, $encKey); + $plaintext = sodium_crypto_stream_xor($encrypted, $nonce, $encKey); } - CryptoUtil::memzero($encrypted); - CryptoUtil::memzero($nonce); - CryptoUtil::memzero($encKey); + Util::memzero($encrypted); + Util::memzero($nonce); + Util::memzero($encKey); return new HiddenString($plaintext); } @@ -230,8 +252,8 @@ public static function decryptWithAd( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function encrypt( HiddenString $plaintext, @@ -257,8 +279,8 @@ public static function encrypt( * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function encryptWithAd( HiddenString $plaintext, @@ -271,9 +293,9 @@ public static function encryptWithAd( // Generate a nonce and HKDF salt: // @codeCoverageIgnoreStart try { - $nonce = \random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); - $salt = \random_bytes((int) $config->HKDF_SALT_LEN); - } catch (\Throwable $ex) { + $nonce = random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $salt = random_bytes((int) $config->HKDF_SALT_LEN); + } catch (Throwable $ex) { throw new CannotPerformOperation($ex->getMessage()); } // @codeCoverageIgnoreEnd @@ -289,19 +311,19 @@ public static function encryptWithAd( // Encrypt our message with the encryption key: if ($config->ENC_ALGO === 'XChaCha20') { - $encrypted = \sodium_crypto_stream_xchacha20_xor( + $encrypted = sodium_crypto_stream_xchacha20_xor( $plaintext->getString(), $nonce, $encKey ); } else { - $encrypted = \sodium_crypto_stream_xor( + $encrypted = sodium_crypto_stream_xor( $plaintext->getString(), $nonce, $encKey ); } - CryptoUtil::memzero($encKey); + Util::memzero($encKey); // Calculate an authentication tag: if ($config->USE_PAE) { @@ -323,23 +345,21 @@ public static function encryptWithAd( $config ); } - CryptoUtil::memzero($authKey); + Util::memzero($authKey); - /** @var string $message */ $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory - CryptoUtil::memzero($nonce); - CryptoUtil::memzero($salt); - CryptoUtil::memzero($encrypted); - CryptoUtil::memzero($auth); + Util::memzero($nonce); + Util::memzero($salt); + Util::memzero($encrypted); + Util::memzero($auth); $encoder = Halite::chooseEncoder($encoding); if ($encoder) { return (string) $encoder($message); } return (string) $message; - } /** @@ -352,8 +372,8 @@ public static function encryptWithAd( * * @throws CannotPerformOperation * @throws InvalidDigestLength - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function splitKeys( EncryptionKey $master, @@ -362,15 +382,15 @@ public static function splitKeys( ): array { $binary = $master->getRawKeyMaterial(); return [ - CryptoUtil::hkdfBlake2b( + Util::hkdfBlake2b( $binary, - \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, (string) $config->HKDF_SBOX, $salt ), - CryptoUtil::hkdfBlake2b( + Util::hkdfBlake2b( $binary, - \SODIUM_CRYPTO_AUTH_KEYBYTES, + SODIUM_CRYPTO_AUTH_KEYBYTES, (string) $config->HKDF_AUTH, $salt ) @@ -386,7 +406,7 @@ public static function splitKeys( * @return array * * @throws InvalidMessage - * @throws \TypeError + * @throws TypeError * @codeCoverageIgnore */ public static function unpackMessageForDecryption(string $ciphertext): array @@ -427,7 +447,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array // 36: Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN, // 24: - \SODIUM_CRYPTO_STREAM_NONCEBYTES + SODIUM_CRYPTO_STREAM_NONCEBYTES ); // This is the crypto_stream_xor()ed ciphertext @@ -436,12 +456,12 @@ public static function unpackMessageForDecryption(string $ciphertext): array // 60: Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN + - \SODIUM_CRYPTO_STREAM_NONCEBYTES, + SODIUM_CRYPTO_STREAM_NONCEBYTES, // $length - 124 $length - ( Halite::VERSION_TAG_LEN + (int) $config->HKDF_SALT_LEN + - \SODIUM_CRYPTO_STREAM_NONCEBYTES + + SODIUM_CRYPTO_STREAM_NONCEBYTES + (int) $config->MAC_SIZE ) ); @@ -453,7 +473,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array ); // We don't need this anymore. - CryptoUtil::memzero($ciphertext); + Util::memzero($ciphertext); // Now we return the pieces in a specific order: return [$version, $config, $salt, $nonce, $encrypted, $auth]; @@ -465,22 +485,22 @@ public static function unpackMessageForDecryption(string $ciphertext): array * @param string $message * @param AuthenticationKey $secretKey * @param string $mac - * @param mixed $encoding - * @param SymmetricConfig $config + * @param string|bool $encoding + * @param ?SymmetricConfig $config * @return bool * * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verify( string $message, AuthenticationKey $secretKey, string $mac, - $encoding = Halite::ENCODE_BASE64URLSAFE, - SymmetricConfig $config = null + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE, + ?SymmetricConfig $config = null ): bool { $decoder = Halite::chooseEncoder($encoding, true); if ($decoder) { @@ -488,7 +508,7 @@ public static function verify( /** @var string $mac */ $mac = $decoder($mac); } - if ($config === null) { + if (is_null($config)) { // Default to the current version $config = SymmetricConfig::getConfig( Halite::HALITE_VERSION, @@ -525,7 +545,7 @@ protected static function calculateMAC( SymmetricConfig $config ): string { if ($config->MAC_ALGO === 'BLAKE2b') { - return \sodium_crypto_generichash( + return sodium_crypto_generichash( $message, $authKey, (int) $config->MAC_SIZE @@ -550,7 +570,7 @@ protected static function calculateMAC( * * @throws InvalidMessage * @throws InvalidSignature - * @throws \SodiumException + * @throws SodiumException */ protected static function verifyMAC( string $mac, @@ -566,13 +586,13 @@ protected static function verifyMAC( // @codeCoverageIgnoreEnd } if ($config->MAC_ALGO === 'BLAKE2b') { - $calc = \sodium_crypto_generichash( + $calc = sodium_crypto_generichash( $message, $authKey, (int) $config->MAC_SIZE ); - $res = \hash_equals($mac, $calc); - CryptoUtil::memzero($calc); + $res = hash_equals($mac, $calc); + Util::memzero($calc); return $res; } // @codeCoverageIgnoreStart diff --git a/src/Symmetric/EncryptionKey.php b/src/Symmetric/EncryptionKey.php index 61160b43..c6d00c4e 100644 --- a/src/Symmetric/EncryptionKey.php +++ b/src/Symmetric/EncryptionKey.php @@ -5,6 +5,8 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; +use TypeError; +use const SODIUM_CRYPTO_STREAM_KEYBYTES; /** * Class EncryptionKey @@ -20,11 +22,11 @@ final class EncryptionKey extends SecretKey * EncryptionKey constructor. * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public function __construct(HiddenString $keyMaterial) { - if (Binary::safeStrlen($keyMaterial->getString()) !== \SODIUM_CRYPTO_STREAM_KEYBYTES) { + if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_STREAM_KEYBYTES) { throw new InvalidKey( 'Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long' ); diff --git a/src/Util.php b/src/Util.php index 77790af6..c4efba4b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -11,6 +11,26 @@ InvalidDigestLength, InvalidType }; +use Error; +use RangeException; +use SodiumException; +use Throwable; +use TypeError; +use const + SODIUM_CRYPTO_GENERICHASH_BYTES, + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN, + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + SODIUM_CRYPTO_GENERICHASH_KEYBYTES; +use function + array_values, + count, + implode, + pack, + sodium_crypto_generichash, + sodium_memzero, + sprintf, + str_repeat, + unpack; /** * Class Util @@ -32,12 +52,12 @@ final class Util { /** * Don't allow this to be instantiated. - * @throws \Error + * @throws Error * @codeCoverageIgnore */ final private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** @@ -45,19 +65,19 @@ final private function __construct() * * @param string $chr * @return int - * @throws \RangeException + * @throws RangeException */ public static function chrToInt(string $chr): int { if (Binary::safeStrlen($chr) !== 1) { - throw new \RangeException('Must be a string with a length of 1'); + throw new RangeException('Must be a string with a length of 1'); } - $result = \unpack('C', $chr); + $result = unpack('C', $chr); return (int) $result[1]; } /** - * Wrapper around SODIUM_CRypto_generichash() + * Wrapper around sodium_crypto_generichash() * * Returns hexadecimal characters. * @@ -65,12 +85,12 @@ public static function chrToInt(string $chr): int * @param int $length * @return string * @throws CannotPerformOperation - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function hash( string $input, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return Hex::encode( self::raw_keyed_hash($input, '', $length) @@ -78,7 +98,7 @@ public static function hash( } /** - * Wrapper around SODIUM_CRypto_generichash() + * Wrapper around sodium_crypto_generichash() * * Returns raw binary. * @@ -86,11 +106,11 @@ public static function hash( * @param int $length * @return string * @throws CannotPerformOperation - * @throws \SodiumException + * @throws SodiumException */ public static function raw_hash( string $input, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return self::raw_keyed_hash($input, '', $length); } @@ -112,8 +132,8 @@ public static function raw_hash( * @return string * @throws CannotPerformOperation * @throws InvalidDigestLength - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public static function hkdfBlake2b( string $ikm, @@ -122,7 +142,7 @@ public static function hkdfBlake2b( string $salt = '' ): string { // Sanity-check the desired output length. - if ($length < 0 || $length > (255 * \SODIUM_CRYPTO_GENERICHASH_KEYBYTES)) { + if ($length < 0 || $length > (255 * SODIUM_CRYPTO_GENERICHASH_KEYBYTES)) { throw new InvalidDigestLength( 'Argument 2: Bad HKDF Digest Length' ); @@ -130,7 +150,7 @@ public static function hkdfBlake2b( // "If [salt] not provided, is set to a string of HashLen zeroes." if (empty($salt)) { // @codeCoverageIgnoreStart - $salt = \str_repeat("\x00", \SODIUM_CRYPTO_GENERICHASH_KEYBYTES); + $salt = str_repeat("\x00", SODIUM_CRYPTO_GENERICHASH_KEYBYTES); // @codeCoverageIgnoreEnd } @@ -142,7 +162,7 @@ public static function hkdfBlake2b( // HKDF-Expand: // This check is useless, but it serves as a reminder to the spec. // @codeCoverageIgnoreStart - if (Binary::safeStrlen($prk) < \SODIUM_CRYPTO_GENERICHASH_KEYBYTES) { + if (Binary::safeStrlen($prk) < SODIUM_CRYPTO_GENERICHASH_KEYBYTES) { throw new CannotPerformOperation( 'An unknown error has occurred' ); @@ -154,15 +174,14 @@ public static function hkdfBlake2b( for ($block_index = 1; Binary::safeStrlen($t) < $length; ++$block_index) { // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) $last_block = self::raw_keyed_hash( - $last_block . $info . \chr($block_index), + $last_block . $info . pack('C', $block_index), $prk ); // T = T(1) | T(2) | T(3) | ... | T(N) $t .= $last_block; } // ORM = first L octets of T - $orm = Binary::safeSubstr($t, 0, $length); - return $orm; + return Binary::safeSubstr($t, 0, $length); } /** @@ -177,8 +196,8 @@ public static function intArrayToString(array $integers): string foreach ($args as $i => $v) { $args[$i] = (int) ($v & 0xff); } - return \pack( - \str_repeat('C', \count($args)), + return pack( + str_repeat('C', count($args)), ...$args ); } @@ -191,7 +210,7 @@ public static function intArrayToString(array $integers): string */ public static function intToChr(int $int): string { - return \pack('C', $int); + return pack('C', $int); } /** @@ -205,13 +224,13 @@ public static function intToChr(int $int): string * @param int $length * @return string * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException */ public static function keyed_hash( string $input, string $key, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { return Hex::encode( self::raw_keyed_hash($input, $key, $length) @@ -245,30 +264,30 @@ public static function PAE(string ...$pieces): string * @param int $length * @return string * @throws CannotPerformOperation - * @throws \SodiumException + * @throws SodiumException */ public static function raw_keyed_hash( string $input, string $key, - int $length = \SODIUM_CRYPTO_GENERICHASH_BYTES + int $length = SODIUM_CRYPTO_GENERICHASH_BYTES ): string { - if ($length < \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { + if ($length < SODIUM_CRYPTO_GENERICHASH_BYTES_MIN) { throw new CannotPerformOperation( - \sprintf( + sprintf( 'Output length must be at least %d bytes.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MIN + SODIUM_CRYPTO_GENERICHASH_BYTES_MIN ) ); } - if ($length > \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { + if ($length > SODIUM_CRYPTO_GENERICHASH_BYTES_MAX) { throw new CannotPerformOperation( - \sprintf( + sprintf( 'Output length must be at most %d bytes.', - \SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ) ); } - return \sodium_crypto_generichash($input, $key, $length); + return sodium_crypto_generichash($input, $key, $length); } /** @@ -277,7 +296,7 @@ public static function raw_keyed_hash( * * @param string $string * @return string - * @throws \TypeError + * @throws TypeError */ public static function safeStrcpy(string $string): string { @@ -298,23 +317,20 @@ public static function safeStrcpy(string $string): string * * @param string $string * @return array - * @throws \TypeError + * @throws TypeError */ public static function stringToIntArray(string $string): array { /** * @var array */ - $values = \array_values(\unpack('C*', $string)); + $values = array_values(unpack('C*', $string)); return $values; } /** * Calculate A xor B, given two binary strings of the same length. * - * Uses pack() and unpack() to avoid cache-timing leaks caused by - * chr(). - * * @param string $left * @param string $right * @return string @@ -345,8 +361,8 @@ public static function xorStrings(string $left, string $right): string public static function memzero(string &$var): void { try { - \sodium_memzero($var); - } catch (\Throwable $ex) { + sodium_memzero($var); + } catch (Throwable $ex) { // Best-effort: $var ^= $var; } From 29a56a98bcb121d85374c09d3346c95008cf4b27 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 22:20:04 -0500 Subject: [PATCH 062/134] Update README.md --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c935fc00..aabad76b 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,16 @@ Before you can use Halite, you must choose a version that fits the requirements of your project. The differences between the requirements for the available versions of Halite are briefly highlighted below. -| | PHP | libsodium | PECL libsodium | Support | -|-------------------------------------------------------------|-------|-----------|----------------|---------------------------| -| Halite 4.1 and newer | 7.2.0 | 1.0.15 | N/A (standard) | :heavy_check_mark: Active | -| [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :heavy_check_mark: Active | -| [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | -| [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | -| [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported | - -If you need a version of Halite before 4.0, see the documentation relevant to that +| | PHP | libsodium | PECL libsodium | Support | +|--------------------------------------------------------------|-------|-----------|----------------|---------------------------| +| Halite 5.0 and newer | 8.0.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | +| [Halite 4.1+](https://github.com/paragonie/halite/tree/v4.x) | 7.2.0 | 1.0.15 | N/A (standard) | :heavy_check_mark: Active | +| [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :x: Not Supported | +| [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | +| [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | +| [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported | + +If you need a version of Halite before 5.0, see the documentation relevant to that particular branch. **To install Halite, you first need to [install libsodium](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium).** @@ -56,7 +57,7 @@ If you're stuck, [this step-by-step guide contributed by @aolko](doc/Install-Gui Once you have the prerequisites installed, install Halite through [Composer](https://getcomposer.org/doc/00-intro.md): - composer require paragonie/halite:^4 + composer require paragonie/halite:^5 ### Commercial Support for Older Halite Versions From 46266bfad19bb5bb2ed29844d911d83e62b763ec Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 22:27:39 -0500 Subject: [PATCH 063/134] Usability: Show value next to constant name See #98 --- src/Asymmetric/EncryptionPublicKey.php | 6 +++++- src/Asymmetric/EncryptionSecretKey.php | 9 +++++++-- src/Asymmetric/SignaturePublicKey.php | 9 +++++++-- src/Asymmetric/SignatureSecretKey.php | 8 ++++++-- src/Symmetric/AuthenticationKey.php | 8 ++++++-- src/Symmetric/EncryptionKey.php | 6 +++++- test/unit/KeyTest.php | 12 ++++++------ 7 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/Asymmetric/EncryptionPublicKey.php b/src/Asymmetric/EncryptionPublicKey.php index 2f9ce8c8..ce44766c 100644 --- a/src/Asymmetric/EncryptionPublicKey.php +++ b/src/Asymmetric/EncryptionPublicKey.php @@ -6,6 +6,7 @@ use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; use const SODIUM_CRYPTO_BOX_PUBLICKEYBYTES; +use function sprintf; /** * Class EncryptionPublicKey @@ -29,7 +30,10 @@ public function __construct(HiddenString $keyMaterial) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) { throw new InvalidKey( - 'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long' + sprintf( + 'Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_BOX_PUBLICKEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index 4ddc344f..20c1b630 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -8,7 +8,9 @@ use SodiumException; use TypeError; use const SODIUM_CRYPTO_BOX_SECRETKEYBYTES; -use function sodium_crypto_box_publickey_from_secretkey; +use function + sodium_crypto_box_publickey_from_secretkey, + sprintf; /** * Class EncryptionSecretKey @@ -30,7 +32,10 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey( - 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long' + sprintf( + 'Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_BOX_SECRETKEYBYTES + ) ); } parent::__construct($keyMaterial, $pk); diff --git a/src/Asymmetric/SignaturePublicKey.php b/src/Asymmetric/SignaturePublicKey.php index 141a2936..db782937 100644 --- a/src/Asymmetric/SignaturePublicKey.php +++ b/src/Asymmetric/SignaturePublicKey.php @@ -7,8 +7,10 @@ use ParagonIE\HiddenString\HiddenString; use SodiumException; use TypeError; -use function sodium_crypto_sign_ed25519_pk_to_curve25519; use const SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES; +use function + sodium_crypto_sign_ed25519_pk_to_curve25519, + sprintf; /** * Class SignaturePublicKey @@ -32,7 +34,10 @@ public function __construct(HiddenString $keyMaterial) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) { throw new InvalidKey( - 'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long' + sprintf( + 'Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index eee0917f..38d858e6 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -12,7 +12,8 @@ use function sodium_crypto_sign_ed25519_sk_to_curve25519, sodium_crypto_sign_ed25519_pk_to_curve25519, - sodium_crypto_sign_publickey_from_secretkey; + sodium_crypto_sign_publickey_from_secretkey, + sprintf; /** * Class SignatureSecretKey @@ -36,7 +37,10 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey( - 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long' + sprintf( + 'Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES (%d) bytes long', + SODIUM_CRYPTO_SIGN_SECRETKEYBYTES + ) ); } parent::__construct($keyMaterial, $pk); diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index b4b932fe..afef733b 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -5,8 +5,9 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\InvalidKey; use ParagonIE\HiddenString\HiddenString; -use const SODIUM_CRYPTO_AUTH_KEYBYTES; use TypeError; +use const SODIUM_CRYPTO_AUTH_KEYBYTES; +use function sprintf; /** * Class AuthenticationKey @@ -29,7 +30,10 @@ public function __construct(HiddenString $keyMaterial) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey( - 'Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long' + sprintf( + 'Authentication key must be CRYPTO_AUTH_KEYBYTES (%d) bytes long', + SODIUM_CRYPTO_AUTH_KEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/src/Symmetric/EncryptionKey.php b/src/Symmetric/EncryptionKey.php index c6d00c4e..24439803 100644 --- a/src/Symmetric/EncryptionKey.php +++ b/src/Symmetric/EncryptionKey.php @@ -7,6 +7,7 @@ use ParagonIE\HiddenString\HiddenString; use TypeError; use const SODIUM_CRYPTO_STREAM_KEYBYTES; +use function sprintf; /** * Class EncryptionKey @@ -28,7 +29,10 @@ public function __construct(HiddenString $keyMaterial) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_STREAM_KEYBYTES) { throw new InvalidKey( - 'Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long' + sprintf( + 'Encryption key must be CRYPTO_STREAM_KEYBYTES (%d) bytes long', + SODIUM_CRYPTO_STREAM_KEYBYTES + ) ); } parent::__construct($keyMaterial); diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index 3cfeea70..c7275eae 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -522,37 +522,37 @@ public function testInvalidSizes() new \ParagonIE\Halite\Symmetric\AuthenticationKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Authentication key must be CRYPTO_AUTH_KEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Symmetric\EncryptionKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption key must be CRYPTO_STREAM_KEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption key must be CRYPTO_STREAM_KEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\EncryptionSecretKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES (32) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\SignatureSecretKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES (64) bytes long', $ex->getMessage()); } try { new \ParagonIE\Halite\Asymmetric\SignaturePublicKey(new HiddenString('')); $this->fail('Invalid key size accepted'); } catch (\ParagonIE\Halite\Alerts\InvalidKey $ex) { - $this->assertSame('Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES bytes long', $ex->getMessage()); + $this->assertSame('Signature public key must be CRYPTO_SIGN_PUBLICKEYBYTES (32) bytes long', $ex->getMessage()); } } } From a361ad21e2e2be43015369e9599d59b9f50ce195 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 22:31:06 -0500 Subject: [PATCH 064/134] Remove .travis.yml --- .travis.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b0f83d82..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: php -sudo: required -dist: trusty -php: - - "7.2" - - "7.3" - - "7.4" - - "8.0" - - "master" - - "nightly" -matrix: - fast_finish: true - allow_failures: - - php: "8.0" - - php: "master" - - php: "nightly" - -install: - - travis_retry composer install --no-interaction - - wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar - - chmod +x php-coveralls.phar - - php php-coveralls.phar --version -script: - - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml - - ./vendor/bin/psalm -after_success: - - travis_retry php php-coveralls.phar -v -before_script: - - mkdir -p build/logs - - ls -al -cache: - directories: - - vendor - - $HOME/.cache/composer From a6b8a741773f65fc9849351edd5618b1dcac0c79 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 22:32:41 -0500 Subject: [PATCH 065/134] Clarify HKDF --- src/Util.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Util.php b/src/Util.php index c4efba4b..f31a8f0e 100644 --- a/src/Util.php +++ b/src/Util.php @@ -130,6 +130,7 @@ public static function raw_hash( * @param string $info What sort of key are we deriving? * @param string $salt * @return string + * * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws TypeError @@ -157,6 +158,9 @@ public static function hkdfBlake2b( // HKDF-Extract: // PRK = HMAC-Hash(salt, IKM) // The salt is the HMAC key. + // + // Note: The notation used by the RFC is backwards from what we're doing here. + // They use (Key, Msg) while our API is (Msg, Key). $prk = self::raw_keyed_hash($ikm, $salt); // HKDF-Expand: From 0c10d03249006cbcd0be77bc15693d789bb8f554 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 18 Jan 2022 23:34:31 -0500 Subject: [PATCH 066/134] Boyscouting --- src/Asymmetric/Crypto.php | 6 ++- src/Config.php | 5 ++- src/Cookie.php | 49 ++++++++++------------- src/File.php | 24 ++++++----- src/Halite.php | 20 +++++++--- src/Key.php | 26 +++--------- src/KeyFactory.php | 84 ++++++++++++++++++++++++++------------- src/KeyPair.php | 1 - src/Password.php | 65 ++++++++++++++++-------------- src/SignatureKeyPair.php | 23 ++++++++--- src/Symmetric/Crypto.php | 79 +++++++++++++++++++++--------------- src/Util.php | 26 +++++++++++- 12 files changed, 240 insertions(+), 168 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index fe38d3f3..82269564 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -103,6 +103,7 @@ public static function encrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return string * * @throws CannotPerformOperation @@ -161,7 +162,7 @@ public static function decrypt( EncryptionPublicKey $theirPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return static::decryptWithAd( + return self::decryptWithAd( $ciphertext, $ourPrivateKey, $theirPublicKey, @@ -178,6 +179,7 @@ public static function decrypt( * @param EncryptionPublicKey $theirPublicKey * @param string $additionalData * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -351,7 +353,7 @@ public static function signAndEncrypt( * * @param string $ciphertext Encrypted message * @param EncryptionSecretKey $privateKey Private decryption key - * @param mixed $encoding Which encoding scheme to use? + * @param string|bool $encoding Which encoding scheme to use? * @return HiddenString * * @throws InvalidKey diff --git a/src/Config.php b/src/Config.php index 4ff81705..5402f7a2 100644 --- a/src/Config.php +++ b/src/Config.php @@ -3,6 +3,7 @@ namespace ParagonIE\Halite; use ParagonIE\Halite\Alerts\ConfigDirectiveNotFound; +use function array_key_exists; /** * Class Config @@ -23,7 +24,7 @@ * @property bool CHECKSUM_PUBKEY * @property int BUFFER * @property int HASH_LEN - * @property string ENCODING + * @property string|bool ENCODING * @property int SHORTEST_CIPHERTEXT_LENGTH * @property int NONCE_BYTES * @property int HKDF_SALT_LEN @@ -60,7 +61,7 @@ public function __construct(array $set = []) */ public function __get(string $key) { - if (\array_key_exists($key, $this->config)) { + if (array_key_exists($key, $this->config)) { return $this->config[$key]; } throw new ConfigDirectiveNotFound($key); diff --git a/src/Cookie.php b/src/Cookie.php index eeaa6279..c905f151 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -22,14 +22,12 @@ use ParagonIE\HiddenString\HiddenString; use SodiumException; use TypeError; -use const PHP_VERSION; use function hash_equals, is_string, json_decode, json_encode, - setcookie, - version_compare; + setcookie; /** * Class Cookie @@ -51,10 +49,7 @@ */ final class Cookie { - /** - * @var EncryptionKey - */ - protected $key; + protected EncryptionKey $key; /** * Cookie constructor. @@ -64,6 +59,7 @@ public function __construct(EncryptionKey $key) { $this->key = $key; } + /** * Hide this from var_dump(), etc. * @@ -80,7 +76,9 @@ public function __debugInfo() * Fetch a value from an encrypted cookie * * @param string $name + * * @return mixed|null (typically an array) + * * @throws InvalidDigestLength * @throws InvalidSignature * @throws CannotPerformOperation @@ -100,10 +98,12 @@ public function fetch(string $name) throw new InvalidType('Cookie value is not a string'); } $config = self::getConfig($stored); + /** @var string|bool $encoding */ + $encoding = $config->ENCODING; $decrypted = Crypto::decrypt( $stored, $this->key, - $config->ENCODING + $encoding ); return json_decode($decrypted->getString(), true); } catch (InvalidMessage $e) { @@ -151,6 +151,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @param bool $secure (defaults to TRUE) * @param bool $httpOnly (defaults to TRUE) * @param string $sameSite (defaults to ''; PHP >= 7.3.0) + * * @return bool * * @throws InvalidDigestLength @@ -160,7 +161,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws SodiumException * @throws TypeError * - * @psalm-suppress InvalidArgument PHP version incompatibilities + * @psalm-suppress InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument */ public function store( @@ -179,30 +180,20 @@ public function store( ), $this->key ); - if (version_compare(PHP_VERSION, '7.3.0') >= 0) { - $options = [ - 'expires' => (int) $expire, - 'path' => (string) $path, - 'domain' => (string) $domain, - 'secure' => (bool) $secure, - 'httponly' => (bool) $httpOnly, - ]; - if ($sameSite !== '') { - $options['samesite'] = (string) $sameSite; - } - return setcookie( - $name, - $val, - $options); + $options = [ + 'expires' => (int) $expire, + 'path' => (string) $path, + 'domain' => (string) $domain, + 'secure' => (bool) $secure, + 'httponly' => (bool) $httpOnly, + ]; + if ($sameSite !== '') { + $options['samesite'] = (string) $sameSite; } return setcookie( $name, $val, - (int) $expire, - (string) $path, - (string) $domain, - (bool) $secure, - (bool) $httpOnly + $options ); } } diff --git a/src/File.php b/src/File.php index 97ad23ca..1f3fd7d5 100644 --- a/src/File.php +++ b/src/File.php @@ -49,7 +49,8 @@ sodium_crypto_generichash_init, sodium_crypto_generichash_update, sodium_crypto_generichash_final, - sodium_crypto_scalarmult; + sodium_crypto_scalarmult, + sodium_increment; /** * Class File @@ -529,6 +530,7 @@ public static function sign( * @param string|bool $encoding Which encoding scheme to use for the signature? * * @return bool + * * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileError @@ -577,6 +579,7 @@ public static function verify( * @param StreamInterface $fileStream * @param ?Key $key * @param string|bool $encoding Which encoding scheme to use for the checksum? + * * @return string * * @throws CannotPerformOperation @@ -664,6 +667,7 @@ protected static function checksumData( * @param MutableFile $output * @param EncryptionKey $key * @param string|null $aad Additional authenticated data + * * @return int * * @throws CannotPerformOperation @@ -1481,7 +1485,7 @@ private static function streamEncrypt( * @param string $nonce * @param string $mac (hash context for BLAKE2b) * @param Config $config - * @param array &$chunk_macs + * @param string[] &$chunk_macs * * @return bool * @@ -1524,7 +1528,9 @@ private static function streamDecrypt( // Version 2+ uses a keyed BLAKE2b hash instead of HMAC sodium_crypto_generichash_update($mac, $read); - /** @var string $mac */ + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } $calcMAC = Util::safeStrcpy($mac); $calc = sodium_crypto_generichash_final($calcMAC, (int) $config->MAC_SIZE); @@ -1536,7 +1542,6 @@ private static function streamDecrypt( ); // @codeCoverageIgnoreEnd } else { - /** @var string $chunkMAC */ $chunkMAC = array_shift($chunk_macs); if (!hash_equals($chunkMAC, $calc)) { // This chunk was altered after the original MAC was verified @@ -1575,10 +1580,10 @@ private static function streamDecrypt( * Recalculate and verify the HMAC of the input file * * @param ReadOnlyFile $input The file we are verifying - * @param string $mac (hash context) + * @param string $mac (hash context) * @param Config $config Version-specific settings * - * @return array Hashes of various chunks + * @return string[] Hashes of various chunks * * @throws CannotPerformOperation * @throws FileAccessDenied @@ -1589,7 +1594,7 @@ private static function streamDecrypt( */ private static function streamVerify( ReadOnlyFile $input, - $mac, + string $mac, Config $config ): array { $start = $input->getPos(); @@ -1604,7 +1609,6 @@ private static function streamVerify( $break = false; while (!$break && $input->getPos() < $cipher_end) { - /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. @@ -1622,7 +1626,9 @@ private static function streamVerify( * We're updating our HMAC and nothing else */ sodium_crypto_generichash_update($mac, $read); - $mac = (string) $mac; + if (!is_string($mac)) { + throw new CannotPerformOperation('Internal error with BLAKE2b implementation'); + } // Copy the hash state then store the MAC of this chunk $chunkMAC = Util::safeStrcpy($mac); $chunkMACs []= sodium_crypto_generichash_final( diff --git a/src/Halite.php b/src/Halite.php index a55efa3b..f2b8e3d1 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite; +use Error; use ParagonIE\ConstantTime\{ Base32, Base32Hex, @@ -10,7 +11,12 @@ Hex }; use ParagonIE\Halite\Alerts\InvalidType; -use function extension_loaded, implode; +use const + SODIUM_LIBRARY_MAJOR_VERSION, + SODIUM_LIBRARY_VERSION; +use function + extension_loaded, + implode; /** * Class Halite @@ -57,26 +63,28 @@ final class Halite /** * Don't allow this to be instantiated. * - * @throws \Error + * @throws Error * @codeCoverageIgnore */ final private function __construct() { - throw new \Error('Do not instantiate'); + throw new Error('Do not instantiate'); } /** * Select which encoding/decoding function to use. * * @internal - * @param mixed $chosen + * @param string|bool $chosen * @param bool $decode - * @return callable|null + * @return ?callable + * * @throws InvalidType + * * @psalm-suppress InvalidReturnStatement * @psalm-suppress InvalidReturnType */ - public static function chooseEncoder($chosen, bool $decode = false) + public static function chooseEncoder(string|bool $chosen, bool $decode = false) { if ($chosen === true) { return null; diff --git a/src/Key.php b/src/Key.php index dbe62ce3..4900bc99 100644 --- a/src/Key.php +++ b/src/Key.php @@ -7,6 +7,7 @@ CannotSerializeKey }; use ParagonIE\HiddenString\HiddenString; +use TypeError; /** * Class Key @@ -26,25 +27,10 @@ */ class Key { - /** - * @var bool - */ - protected $isPublicKey = false; - - /** - * @var bool - */ - protected $isSigningKey = false; - - /** - * @var bool - */ - protected $isAsymmetricKey = false; - - /** - * @var string - */ - private $keyMaterial = ''; + protected bool $isPublicKey = false; + protected bool $isSigningKey = false; + protected bool $isAsymmetricKey = false; + private string $keyMaterial = ''; /** * Don't let this ever succeed @@ -133,7 +119,7 @@ public function __toString() * Get the actual key material * * @return string - * @throws \TypeError + * @throws TypeError */ public function getRawKeyMaterial(): string { diff --git a/src/KeyFactory.php b/src/KeyFactory.php index 25916450..e32728bf 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -42,6 +42,7 @@ file_get_contents, file_put_contents, hash_equals, + is_int, is_readable, random_bytes, sodium_crypto_box_keypair, @@ -101,12 +102,13 @@ public static function generateAuthenticationKey(): AuthenticationKey } /** - * Generate an an encryption key (symmetric-key cryptography) + * Generate an encryption key (symmetric-key cryptography) * * @return EncryptionKey + * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \TypeError + * @throws TypeError */ public static function generateEncryptionKey(): EncryptionKey { @@ -152,6 +154,8 @@ public static function generateEncryptionKeyPair(): EncryptionKeyPair * Generate a key pair for public key digital signatures * * @return SignatureKeyPair + * + * @throws CannotPerformOperation * @throws InvalidKey * @throws SodiumException * @throws TypeError @@ -229,6 +233,7 @@ public static function deriveAuthenticationKey( * (You can safely use the default) * * @return EncryptionKey + * * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType @@ -329,11 +334,11 @@ public static function deriveEncryptionKeyPair( * * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey * @throws InvalidSalt * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException */ public static function deriveSignatureKeyPair( HiddenString $password, @@ -343,10 +348,10 @@ public static function deriveSignatureKeyPair( ): SignatureKeyPair { $kdfLimits = self::getSecurityLevels($level, $alg); // VERSION 2+ (argon2) - if (Binary::safeStrlen($salt) !== \SODIUM_CRYPTO_PWHASH_SALTBYTES) { + if (Binary::safeStrlen($salt) !== SODIUM_CRYPTO_PWHASH_SALTBYTES) { // @codeCoverageIgnoreStart throw new InvalidSalt( - 'Expected ' . \SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) + 'Expected ' . SODIUM_CRYPTO_PWHASH_SALTBYTES . ' bytes, got ' . Binary::safeStrlen($salt) ); // @codeCoverageIgnoreEnd } @@ -378,7 +383,9 @@ public static function deriveSignatureKeyPair( * * @param string $level * @param int $alg + * * @return int[] + * * @throws InvalidType * @codeCoverageIgnore */ @@ -425,6 +432,7 @@ public static function getSecurityLevels( * Load a symmetric authentication key from a string * * @param HiddenString $keyData + * * @return AuthenticationKey * * @throws InvalidKey @@ -446,6 +454,7 @@ public static function importAuthenticationKey(HiddenString $keyData): Authentic * Load a symmetric encryption key from a string * * @param HiddenString $keyData + * * @return EncryptionKey * * @throws InvalidKey @@ -467,6 +476,7 @@ public static function importEncryptionKey(HiddenString $keyData): EncryptionKey * Load, specifically, an encryption public key from a string * * @param HiddenString $keyData + * * @return EncryptionPublicKey * * @throws InvalidKey @@ -488,6 +498,7 @@ public static function importEncryptionPublicKey(HiddenString $keyData): Encrypt * Load, specifically, an encryption secret key from a string * * @param HiddenString $keyData + * * @return EncryptionSecretKey * * @throws InvalidKey @@ -509,6 +520,7 @@ public static function importEncryptionSecretKey(HiddenString $keyData): Encrypt * Load, specifically, a signature public key from a string * * @param HiddenString $keyData + * * @return SignaturePublicKey * * @throws InvalidKey @@ -530,6 +542,7 @@ public static function importSignaturePublicKey(HiddenString $keyData): Signatur * Load, specifically, a signature secret key from a string * * @param HiddenString $keyData + * * @return SignatureSecretKey * * @throws InvalidKey @@ -551,6 +564,7 @@ public static function importSignatureSecretKey(HiddenString $keyData): Signatur * Load an asymmetric encryption key pair from a string * * @param HiddenString $keyData + * * @return EncryptionKeyPair * * @throws InvalidKey @@ -576,6 +590,7 @@ public static function importEncryptionKeyPair(HiddenString $keyData): Encryptio * @param HiddenString $keyData * @return SignatureKeyPair * + * @throws CannotPerformOperation * @throws InvalidKey * @throws SodiumException * @throws TypeError @@ -597,6 +612,7 @@ public static function importSignatureKeyPair(HiddenString $keyData): SignatureK * Load a symmetric authentication key from a file * * @param string $filePath + * * @return AuthenticationKey * * @throws CannotPerformOperation @@ -621,6 +637,7 @@ public static function loadAuthenticationKey(string $filePath): AuthenticationKe * Load a symmetric encryption key from a file * * @param string $filePath + * * @return EncryptionKey * * @throws CannotPerformOperation @@ -645,6 +662,7 @@ public static function loadEncryptionKey(string $filePath): EncryptionKey * Load, specifically, an encryption public key from a file * * @param string $filePath + * * @return EncryptionPublicKey * * @throws CannotPerformOperation @@ -669,17 +687,18 @@ public static function loadEncryptionPublicKey(string $filePath): EncryptionPubl * Load, specifically, an encryption public key from a file * * @param string $filePath + * * @return EncryptionSecretKey * * @throws CannotPerformOperation * @throws InvalidKey - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError * @codeCoverageIgnore */ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecretKey { - if (!\is_readable($filePath)) { + if (!is_readable($filePath)) { throw new CannotPerformOperation( 'Cannot read keyfile: '. $filePath ); @@ -693,6 +712,7 @@ public static function loadEncryptionSecretKey(string $filePath): EncryptionSecr * Load, specifically, a signature public key from a file * * @param string $filePath + * * @return SignaturePublicKey * * @throws CannotPerformOperation @@ -717,6 +737,7 @@ public static function loadSignaturePublicKey(string $filePath): SignaturePublic * Load, specifically, a signature secret key from a file * * @param string $filePath + * * @return SignatureSecretKey * * @throws CannotPerformOperation @@ -741,6 +762,7 @@ public static function loadSignatureSecretKey(string $filePath): SignatureSecret * Load an asymmetric encryption key pair from a file * * @param string $filePath + * * @return EncryptionKeyPair * * @throws CannotPerformOperation @@ -767,6 +789,7 @@ public static function loadEncryptionKeyPair(string $filePath): EncryptionKeyPai * Load an asymmetric signature key pair from a file * * @param string $filePath + * * @return SignatureKeyPair * * @throws CannotPerformOperation @@ -792,7 +815,8 @@ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair /** * Export a cryptography key to a string (with a checksum) * - * @param object $key + * @param Key|KeyPair $key + * * @return HiddenString * * @throws CannotPerformOperation @@ -800,25 +824,23 @@ public static function loadSignatureKeyPair(string $filePath): SignatureKeyPair * @throws SodiumException * @throws TypeError */ - public static function export($key): HiddenString + public static function export(Key|KeyPair $key): HiddenString { if ($key instanceof KeyPair) { return self::export( $key->getSecretKey() ); - } elseif ($key instanceof Key) { - return new HiddenString( - Hex::encode( - Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() . - sodium_crypto_generichash( - Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(), - '', - SODIUM_CRYPTO_GENERICHASH_BYTES_MAX - ) - ) - ); } - throw new TypeError('Expected a Key.'); + return new HiddenString( + Hex::encode( + Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial() . + sodium_crypto_generichash( + Halite::HALITE_VERSION_KEYS . $key->getRawKeyMaterial(), + '', + SODIUM_CRYPTO_GENERICHASH_BYTES_MAX + ) + ) + ); } /** @@ -826,11 +848,13 @@ public static function export($key): HiddenString * * @param Key|KeyPair $key * @param string $filename + * * @return bool - * @throws \SodiumException - * @throws \TypeError + * + * @throws SodiumException + * @throws TypeError */ - public static function save($key, string $filename = ''): bool + public static function save(Key|KeyPair $key, string $filename = ''): bool { if ($key instanceof KeyPair) { return self::saveKeyFile( @@ -845,7 +869,9 @@ public static function save($key, string $filename = ''): bool * Read a key from a file, verify its checksum * * @param string $filePath + * * @return HiddenString + * * @throws CannotPerformOperation * @throws InvalidKey * @throws SodiumException @@ -873,7 +899,9 @@ protected static function loadKeyFile(string $filePath): HiddenString * checksum) * * @param string $data + * * @return string + * * @throws InvalidKey * @throws SodiumException * @throws TypeError @@ -891,7 +919,7 @@ public static function getKeyDataFromString(string $data): string -SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, SODIUM_CRYPTO_GENERICHASH_BYTES_MAX ); - $calc = \sodium_crypto_generichash( + $calc = sodium_crypto_generichash( $versionTag . $keyData, '', SODIUM_CRYPTO_GENERICHASH_BYTES_MAX @@ -935,6 +963,6 @@ protected static function saveKeyFile( ) ) ); - return $saved !== false; + return is_int($saved ); } } diff --git a/src/KeyPair.php b/src/KeyPair.php index 9a62c336..72bdd0ca 100644 --- a/src/KeyPair.php +++ b/src/KeyPair.php @@ -26,7 +26,6 @@ class KeyPair { protected SecretKey $secretKey; - protected PublicKey $publicKey; /** diff --git a/src/Password.php b/src/Password.php index a9455962..8acd6d5f 100644 --- a/src/Password.php +++ b/src/Password.php @@ -11,6 +11,7 @@ CannotPerformOperation, InvalidDigestLength, InvalidMessage, + InvalidSignature, InvalidType }; use ParagonIE\Halite\Symmetric\{ @@ -21,6 +22,7 @@ use ParagonIE\HiddenString\HiddenString; use SodiumException; use TypeError; +use const SODIUM_CRYPTO_PWHASH_STRPREFIX; use function hash_equals, sodium_crypto_pwhash_str, @@ -51,10 +53,11 @@ final class Password * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @param string $additionalData Additional authenticated data + * * @return string An encrypted hash to store * - * @throws InvalidDigestLength * @throws CannotPerformOperation + * @throws InvalidDigestLength * @throws InvalidMessage * @throws InvalidType * @throws SodiumException @@ -89,13 +92,14 @@ public static function hash( * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @param string $additionalData Additional authenticated data (if used to encrypt, mandatory) + * * @return bool Do we need to regenerate the hash or * ciphertext? * - * @throws Alerts\InvalidSignature * @throws CannotPerformOperation * @throws InvalidDigestLength * @throws InvalidMessage + * @throws InvalidSignature * @throws InvalidType * @throws SodiumException * @throws TypeError @@ -110,43 +114,41 @@ public static function needsRehash( if (Binary::safeStrlen($stored) < ((int) $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3)) { throw new InvalidMessage('Encrypted password hash is too short.'); } + /** @var string|bool $encoding */ + $encoding = $config->ENCODING; // First let's decrypt the hash $hash_str = Crypto::decryptWithAd( $stored, $secretKey, $additionalData, - $config->ENCODING + $encoding )->getString(); // Upon successful decryption, verify that we're using Argon2id if (!hash_equals( Binary::safeSubstr($hash_str, 0, 10), - \SODIUM_CRYPTO_PWHASH_STRPREFIX + SODIUM_CRYPTO_PWHASH_STRPREFIX )) { return true; } // Parse the cost parameters: - switch ($level) { - case KeyFactory::INTERACTIVE: - return !hash_equals( - '$argon2id$v=19$m=65536,t=2,p=1$', - Binary::safeSubstr($hash_str, 0, 31) - ); - case KeyFactory::MODERATE: - return !hash_equals( - '$argon2id$v=19$m=262144,t=3,p=1$', - Binary::safeSubstr($hash_str, 0, 32) - ); - case KeyFactory::SENSITIVE: - return !hash_equals( - '$argon2id$v=19$m=1048576,t=4,p=1$', - Binary::safeSubstr($hash_str, 0, 33) - ); - default: - return true; - } + return match ($level) { + KeyFactory::INTERACTIVE => !hash_equals( + '$argon2id$v=19$m=65536,t=2,p=1$', + Binary::safeSubstr($hash_str, 0, 31) + ), + KeyFactory::MODERATE => !hash_equals( + '$argon2id$v=19$m=262144,t=3,p=1$', + Binary::safeSubstr($hash_str, 0, 32) + ), + KeyFactory::SENSITIVE => !hash_equals( + '$argon2id$v=19$m=1048576,t=4,p=1$', + Binary::safeSubstr($hash_str, 0, 33) + ), + default => true, + }; } /** @@ -166,17 +168,16 @@ protected static function getConfig(string $stored): SymmetricConfig 'Encrypted password hash is way too short.' ); } + $prefix = Binary::safeSubstr($stored, 0, 5); if ( - hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX) + hash_equals($prefix, Halite::VERSION_PREFIX) || - hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) + hash_equals($prefix, Halite::VERSION_OLD_PREFIX) ) { $decoded = Base64UrlSafe::decode($stored); - return SymmetricConfig::getConfig( - $decoded, - 'encrypt' - ); + return SymmetricConfig::getConfig($decoded, 'encrypt'); } + // @codeCoverageIgnoreStart $v = Hex::decode(Binary::safeSubstr($stored, 0, 8)); return SymmetricConfig::getConfig($v, 'encrypt'); @@ -190,6 +191,7 @@ protected static function getConfig(string $stored): SymmetricConfig * @param string $stored The encrypted password hash * @param EncryptionKey $secretKey The master key for all passwords * @param string $additionalData Additional authenticated data (needed to decrypt) + * * @return bool Is this password valid? * * @throws Alerts\InvalidSignature @@ -213,8 +215,11 @@ public static function verify( 'Encrypted password hash is too short.' ); } + /** @var string|bool $encoding */ + $encoding = $config->ENCODING; + // First let's decrypt the hash - $hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $config->ENCODING); + $hash_str = Crypto::decryptWithAd($stored, $secretKey, $additionalData, $encoding); // Upon successful decryption, verify the password is correct return sodium_crypto_pwhash_str_verify( $hash_str->getString(), diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index 4dc94036..d36386d5 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -2,13 +2,19 @@ declare(strict_types=1); namespace ParagonIE\Halite; -use ParagonIE\Halite\Alerts\InvalidKey; +use InvalidArgumentException; +use ParagonIE\Halite\Alerts\{ + CannotPerformOperation, + InvalidKey +}; use ParagonIE\Halite\Asymmetric\{ + PublicKey, + SecretKey, SignaturePublicKey, SignatureSecretKey }; use ParagonIE\HiddenString\HiddenString; -use InvalidArgumentException; +use SodiumException; use TypeError; use function count; @@ -33,19 +39,21 @@ final class SignatureKeyPair extends KeyPair /** * @var SignatureSecretKey */ - protected Asymmetric\SecretKey $secretKey; + protected SecretKey $secretKey; /** * @var SignaturePublicKey */ - protected Asymmetric\PublicKey $publicKey; + protected PublicKey $publicKey; /** * Pass it a secret key, it will automatically generate a public key * * @param array $keys * + * @throws CannotPerformOperation * @throws InvalidKey + * @throws SodiumException * @throws TypeError */ public function __construct(Key ...$keys) @@ -119,14 +127,16 @@ public function __construct(Key ...$keys) break; default: throw new InvalidArgumentException( - 'Halite\\EncryptionKeyPair expects 1 or 2 keys' + 'EncryptionKeyPair expects 1 or 2 keys' ); } } /** * @return EncryptionKeyPair + * * @throws InvalidKey + * @throws SodiumException * @throws TypeError */ public function getEncryptionKeyPair(): EncryptionKeyPair @@ -143,8 +153,9 @@ public function getEncryptionKeyPair(): EncryptionKeyPair * @param SignatureSecretKey $secret * @return void * + * @throws CannotPerformOperation * @throws InvalidKey - * @throws TypeError + * @throws SodiumException */ protected function setupKeyPair(SignatureSecretKey $secret): void { diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index a696871b..2e3617ef 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite\Symmetric; +use Error; use ParagonIE\ConstantTime\Binary; use ParagonIE\Halite\Alerts\{ CannotPerformOperation, @@ -17,7 +18,6 @@ Util }; use ParagonIE\HiddenString\HiddenString; -use Error; use RangeException; use SodiumException; use Throwable; @@ -70,6 +70,7 @@ final private function __construct() * @param string $message * @param AuthenticationKey $secretKey * @param string|bool $encoding + * * @return string * * @throws InvalidMessage @@ -95,7 +96,7 @@ public static function authenticate( if ($encoder) { return (string) $encoder($mac); } - return (string) $mac; + return $mac; } /** @@ -103,7 +104,8 @@ public static function authenticate( * * @param string $ciphertext * @param EncryptionKey $secretKey - * @param mixed $encoding + * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -133,7 +135,8 @@ public static function decrypt( * @param string $ciphertext * @param EncryptionKey $secretKey * @param string $additionalData - * @param mixed $encoding + * @param string|bool $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -164,20 +167,22 @@ public static function decryptWithAd( } // @codeCoverageIgnoreEnd } - /** @var array $pieces */ - $pieces = self::unpackMessageForDecryption($ciphertext); - /** @var string $version */ - $version = $pieces[0]; - /** @var Config $config */ - $config = $pieces[1]; - /** @var string $salt */ - $salt = $pieces[2]; - /** @var string $nonce */ - $nonce = $pieces[3]; - /** @var string $encrypted */ - $encrypted = $pieces[4]; - /** @var string $auth */ - $auth = $pieces[5]; + /** + * @var string $version + * @var Config $config + * @var string $salt + * @var string $nonce + * @var string $encrypted + * @var string $auth + */ + [ + $version, + $config, + $salt, + $nonce, + $encrypted, + $auth + ] = self::unpackMessageForDecryption($ciphertext); /* Split our key into two keys: One for encryption, the other for authentication. By using separate keys, we can reasonably dismiss @@ -190,14 +195,14 @@ public static function decryptWithAd( * @var string $encKey * @var string $authKey */ - $split = self::splitKeys($secretKey, (string) $salt, $config); + $split = self::splitKeys($secretKey, $salt, $config); $encKey = $split[0]; $authKey = $split[1]; // Check the MAC first if ($config->USE_PAE) { $verified = self::verifyMAC( - (string) $auth, + $auth, Util::PAE($version, $salt, $nonce, $additionalData, $encrypted), $authKey, $config @@ -205,12 +210,12 @@ public static function decryptWithAd( } else { $verified = self::verifyMAC( // @codeCoverageIgnoreStart - (string) $auth, - (string) $version . - (string) $salt . - (string) $nonce . - (string) $additionalData . - (string) $encrypted, + $auth, + $version . + $salt . + $nonce . + $additionalData . + $encrypted, // @codeCoverageIgnoreEnd $authKey, $config @@ -246,6 +251,7 @@ public static function decryptWithAd( * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string|bool $encoding + * * @return string * * @throws CannotPerformOperation @@ -273,6 +279,7 @@ public static function encrypt( * @param EncryptionKey $secretKey * @param string $additionalData * @param bool|string $encoding + * * @return string * * @throws CannotPerformOperation @@ -306,7 +313,7 @@ public static function encryptWithAd( This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ - list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); + [$encKey, $authKey] = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: @@ -359,7 +366,7 @@ public static function encryptWithAd( if ($encoder) { return (string) $encoder($message); } - return (string) $message; + return $message; } /** @@ -368,6 +375,7 @@ public static function encryptWithAd( * @param EncryptionKey $master * @param string $salt * @param BaseConfig $config + * * @return string[] * * @throws CannotPerformOperation @@ -403,6 +411,7 @@ public static function splitKeys( * Should return exactly 6 elements. * * @param string $ciphertext + * * @return array * * @throws InvalidMessage @@ -487,6 +496,7 @@ public static function unpackMessageForDecryption(string $ciphertext): array * @param string $mac * @param string|bool $encoding * @param ?SymmetricConfig $config + * * @return bool * * @throws InvalidMessage @@ -535,9 +545,11 @@ public static function verify( * @param string $message * @param string $authKey * @param SymmetricConfig $config + * * @return string + * * @throws InvalidMessage - * @throws \SodiumException + * @throws SodiumException */ protected static function calculateMAC( string $message, @@ -562,10 +574,11 @@ protected static function calculateMAC( * Verify a Message Authentication Code (MAC) of a message, with a shared * key. * - * @param string $mac Message Authentication Code - * @param string $message The message to verify - * @param string $authKey Authentication key (symmetric) - * @param SymmetricConfig $config Configuration object + * @param string $mac Message Authentication Code + * @param string $message The message to verify + * @param string $authKey Authentication key (symmetric) + * @param SymmetricConfig $config Configuration object + * * @return bool * * @throws InvalidMessage diff --git a/src/Util.php b/src/Util.php index f31a8f0e..b8ec0915 100644 --- a/src/Util.php +++ b/src/Util.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Halite; +use Error; use ParagonIE\ConstantTime\{ Binary, Hex @@ -11,7 +12,6 @@ InvalidDigestLength, InvalidType }; -use Error; use RangeException; use SodiumException; use Throwable; @@ -64,7 +64,9 @@ final private function __construct() * Convert a character to an integer (without cache-timing side-channels) * * @param string $chr + * * @return int + * * @throws RangeException */ public static function chrToInt(string $chr): int @@ -83,7 +85,9 @@ public static function chrToInt(string $chr): int * * @param string $input * @param int $length + * * @return string + * * @throws CannotPerformOperation * @throws SodiumException * @throws TypeError @@ -104,7 +108,9 @@ public static function hash( * * @param string $input * @param int $length + * * @return string + * * @throws CannotPerformOperation * @throws SodiumException */ @@ -129,6 +135,7 @@ public static function raw_hash( * @param int $length How many bytes? * @param string $info What sort of key are we deriving? * @param string $salt + * * @return string * * @throws CannotPerformOperation @@ -192,6 +199,7 @@ public static function hkdfBlake2b( * Convert an array of integers to a string * * @param array $integers + * * @return string */ public static function intArrayToString(array $integers): string @@ -226,7 +234,9 @@ public static function intToChr(int $int): string * @param string $input * @param string $key * @param int $length + * * @return string + * * @throws CannotPerformOperation * @throws TypeError * @throws SodiumException @@ -245,6 +255,7 @@ public static function keyed_hash( * Pre-authentication encoding * * @param string ...$pieces + * * @return string */ public static function PAE(string ...$pieces): string @@ -266,7 +277,9 @@ public static function PAE(string ...$pieces): string * @param string $input * @param string $key * @param int $length + * * @return string + * * @throws CannotPerformOperation * @throws SodiumException */ @@ -299,7 +312,9 @@ public static function raw_keyed_hash( * the original string. * * @param string $string + * * @return string + * * @throws TypeError */ public static function safeStrcpy(string $string): string @@ -320,7 +335,9 @@ public static function safeStrcpy(string $string): string * Turn a string into an array of integers * * @param string $string + * * @return array + * * @throws TypeError */ public static function stringToIntArray(string $string): array @@ -337,7 +354,9 @@ public static function stringToIntArray(string $string): array * * @param string $left * @param string $right + * * @return string + * * @throws InvalidType */ public static function xorStrings(string $left, string $right): string @@ -358,6 +377,9 @@ public static function xorStrings(string $left, string $right): string * Wrap memzero() without breaking on sodium_compat * * @param string &$var + * + * @return void + * * @psalm-param-out null $var * @psalm-suppress UnnecessaryVarAnnotation * @psalm-suppress InvalidOperand @@ -370,6 +392,6 @@ public static function memzero(string &$var): void // Best-effort: $var ^= $var; } - $var = null; + unset($var); } } From 1876f26704aea08b552f31c69edde81a368f6496 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 01:05:31 -0500 Subject: [PATCH 067/134] Security: Use HKDF to extract key from shared secret --- CHANGELOG.md | 6 +++ src/Asymmetric/Config.php | 102 ++++++++++++++++++++++++++++++++++++++ src/Asymmetric/Crypto.php | 93 ++++++++++++++++++++++++++++------ src/File.php | 10 +++- src/Symmetric/Config.php | 2 - 5 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 src/Asymmetric/Config.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 37543140..3e992124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ * The `File` class no longer supports the `resource` type. To migrate code, wrap your `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. * Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. +* **Security:** Asymmetric encryption now uses HKDF-BLAKE2b to extract a 256-bit + uniformly random bit string for the encryption key, rather than using the raw + X25519 output directly as an encryption key. + + This is important because Elliptic Curve Diffie-Hellman results in a random + group element, but that isn't necessarily a uniformly random bit string. * **Security:** Halite v5 uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) strategy from PASETO to prevent canonicalization attacks. diff --git a/src/Asymmetric/Config.php b/src/Asymmetric/Config.php new file mode 100644 index 00000000..a03dd497 --- /dev/null +++ b/src/Asymmetric/Config.php @@ -0,0 +1,102 @@ + Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => 'HaliteVersion5X25519SharedSecret', + 'HASH_SCALARMULT' => true, + ]; + } + } + if ($major === 4 || $major === 3) { + switch ($minor) { + case 0: + return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => '', + 'HASH_SCALARMULT' => false, + ]; + } + } + throw new InvalidMessage( + 'Invalid version tag' + ); + } +} diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 82269564..382b916c 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -124,7 +124,9 @@ public static function encryptWithAd( /** @var HiddenString $ss */ $ss = self::getSharedSecret( $ourPrivateKey, - $theirPublicKey + $theirPublicKey, + false, + self::getAsymmetricConfig(Halite::HALITE_VERSION, true) ); $sharedSecretKey = new EncryptionKey($ss); $ciphertext = SymmetricCrypto::encryptWithAd( @@ -201,7 +203,9 @@ public static function decryptWithAd( /** @var HiddenString $ss */ $ss = self::getSharedSecret( $ourPrivateKey, - $theirPublicKey + $theirPublicKey, + false, + self::getAsymmetricConfig($ciphertext, $encoding) ); $sharedSecretKey = new EncryptionKey($ss); $plaintext = SymmetricCrypto::decryptWithAd( @@ -223,6 +227,7 @@ public static function decryptWithAd( * @param EncryptionSecretKey $privateKey Private key (yours) * @param EncryptionPublicKey $publicKey Public key (theirs) * @param bool $get_as_object Get as a Key object? + * @param ?Config $config Asymmetric Config * @return HiddenString|Key * * @throws InvalidKey @@ -232,24 +237,38 @@ public static function decryptWithAd( public static function getSharedSecret( EncryptionSecretKey $privateKey, EncryptionPublicKey $publicKey, - bool $get_as_object = false + bool $get_as_object = false, + ?Config $config = null ): HiddenString|Key { - if ($get_as_object) { - return new EncryptionKey( - new HiddenString( - sodium_crypto_scalarmult( - $privateKey->getRawKeyMaterial(), - $publicKey->getRawKeyMaterial() + if (!is_null($config)) { + if ($config->HASH_SCALARMULT) { + $hiddenString = new HiddenString( + Util::hkdfBlake2b( + sodium_crypto_scalarmult( + $privateKey->getRawKeyMaterial(), + $publicKey->getRawKeyMaterial() + ), + 32, + (string) $config->HASH_DOMAIN_SEPARATION ) - ) - ); + ); + if ($get_as_object) { + return new EncryptionKey($hiddenString); + } + return $hiddenString; + } } - return new HiddenString( + + $hiddenString = new HiddenString( sodium_crypto_scalarmult( $privateKey->getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ) ); + if ($get_as_object) { + return new EncryptionKey($hiddenString); + } + return $hiddenString; } /** @@ -258,11 +277,12 @@ public static function getSharedSecret( * @param HiddenString $plaintext Message to encrypt * @param EncryptionPublicKey $publicKey Public encryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return string Ciphertext * * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function seal( HiddenString $plaintext, @@ -286,6 +306,7 @@ public static function seal( * @param string $message Message to sign * @param SignatureSecretKey $privateKey Private signing key * @param string|bool $encoding Which encoding scheme to use? + * * @return string Signature (detached) * * @throws InvalidType @@ -315,6 +336,7 @@ public static function sign( * @param SignatureSecretKey $secretKey Private signing key * @param PublicKey $recipientPublicKey Public encryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return string * * @throws CannotPerformOperation @@ -354,6 +376,7 @@ public static function signAndEncrypt( * @param string $ciphertext Encrypted message * @param EncryptionSecretKey $privateKey Private decryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return HiddenString * * @throws InvalidKey @@ -419,6 +442,7 @@ public static function unseal( * @param SignaturePublicKey $publicKey Public key * @param string $signature Signature * @param string|bool $encoding Which encoding scheme to use? + * * @return bool * * @throws InvalidSignature @@ -468,8 +492,8 @@ public static function verify( * @throws InvalidMessage * @throws InvalidSignature * @throws InvalidType - * @throws \SodiumException - * @throws \TypeError + * @throws SodiumException + * @throws TypeError */ public static function verifyAndDecrypt( string $ciphertext, @@ -493,4 +517,41 @@ public static function verifyAndDecrypt( } return new HiddenString($message); } + + /** + * Get the Asymmetric configuration expected for this Halite version + * + * @param string $ciphertext + * @param string|bool $encoding + * + * @return Config + * + * @throws InvalidMessage + * @throws InvalidType + */ + public static function getAsymmetricConfig( + string $ciphertext, + string|bool $encoding = Halite::ENCODE_BASE64URLSAFE + ): Config { + $decoder = Halite::chooseEncoder($encoding, true); + if (is_callable($decoder)) { + // We were given encoded data: + // @codeCoverageIgnoreStart + try { + /** @var string $ciphertext */ + $ciphertext = $decoder($ciphertext); + } catch (RangeException $ex) { + throw new InvalidMessage( + 'Invalid character encoding' + ); + } + // @codeCoverageIgnoreEnd + } + $version = Binary::safeSubstr( + $ciphertext, + 0, + Halite::VERSION_TAG_LEN + ); + return Config::getConfig($version, 'encrypt'); + } } diff --git a/src/File.php b/src/File.php index 1f3fd7d5..0b9a6d01 100644 --- a/src/File.php +++ b/src/File.php @@ -906,7 +906,12 @@ protected static function sealData( unset($ephemeralKeyPair); // Calculate the shared secret key - $sharedSecretKey = AsymmetricCrypto::getSharedSecret($ephSecret, $publicKey, true); + $sharedSecretKey = AsymmetricCrypto::getSharedSecret( + $ephSecret, + $publicKey, + true, + AsymmetricCrypto::getAsymmetricConfig(Halite::HALITE_VERSION_FILE, true) + ); // @codeCoverageIgnoreStart if (!($sharedSecretKey instanceof EncryptionKey)) { throw new TypeError('Shared secret is the wrong key type.'); @@ -1064,7 +1069,8 @@ protected static function unsealData( $key = AsymmetricCrypto::getSharedSecret( $secretKey, $ephemeral, - true + true, + AsymmetricCrypto::getAsymmetricConfig($header, true) ); // @codeCoverageIgnoreStart if (!($key instanceof EncryptionKey)) { diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 5685cbb5..db55d9c3 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -17,8 +17,6 @@ /** * Class Config * - * Secure encrypted cookies - * * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * From 4f7074ea6336c6aa2621c31fb5cf4f9705943c0c Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 01:24:14 -0500 Subject: [PATCH 068/134] Boyscouting --- src/Asymmetric/Crypto.php | 23 +++++++++++++-------- src/Asymmetric/EncryptionSecretKey.php | 2 +- src/Asymmetric/SecretKey.php | 4 ++-- src/Asymmetric/SignatureSecretKey.php | 2 +- src/Stream/MutableFile.php | 16 +++++++++++---- src/Stream/ReadOnlyFile.php | 19 +++++++++++------ src/Structure/MerkleTree.php | 21 ++++++++++--------- src/Structure/Node.php | 6 +++--- src/Structure/TrimmedMerkleTree.php | 5 ++++- src/Symmetric/AuthenticationKey.php | 1 + src/Symmetric/Config.php | 5 +++++ src/Symmetric/Crypto.php | 28 +++++++++++++++++++------- 12 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 382b916c..3f9362c9 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -23,6 +23,9 @@ use RangeException; use SodiumException; use TypeError; +use const + SODIUM_CRYPTO_STREAM_KEYBYTES, + SODIUM_CRYPTO_SIGN_BYTES; use function is_string, sodium_crypto_box_keypair_from_secretkey_and_publickey, @@ -86,7 +89,7 @@ public static function encrypt( EncryptionPublicKey $theirPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - return self::encryptWithAd( + return self::encryptWithAD( $plaintext, $ourPrivateKey, $theirPublicKey, @@ -114,7 +117,7 @@ public static function encrypt( * @throws SodiumException * @throws TypeError */ - public static function encryptWithAd( + public static function encryptWithAD( HiddenString $plaintext, EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, @@ -129,7 +132,7 @@ public static function encryptWithAd( self::getAsymmetricConfig(Halite::HALITE_VERSION, true) ); $sharedSecretKey = new EncryptionKey($ss); - $ciphertext = SymmetricCrypto::encryptWithAd( + $ciphertext = SymmetricCrypto::encryptWithAD( $plaintext, $sharedSecretKey, $additionalData, @@ -164,7 +167,7 @@ public static function decrypt( EncryptionPublicKey $theirPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return self::decryptWithAd( + return self::decryptWithAD( $ciphertext, $ourPrivateKey, $theirPublicKey, @@ -193,7 +196,7 @@ public static function decrypt( * @throws SodiumException * @throws TypeError */ - public static function decryptWithAd( + public static function decryptWithAD( string $ciphertext, EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, @@ -208,7 +211,7 @@ public static function decryptWithAd( self::getAsymmetricConfig($ciphertext, $encoding) ); $sharedSecretKey = new EncryptionKey($ss); - $plaintext = SymmetricCrypto::decryptWithAd( + $plaintext = SymmetricCrypto::decryptWithAD( $ciphertext, $sharedSecretKey, $additionalData, @@ -228,8 +231,11 @@ public static function decryptWithAd( * @param EncryptionPublicKey $publicKey Public key (theirs) * @param bool $get_as_object Get as a Key object? * @param ?Config $config Asymmetric Config + * * @return HiddenString|Key * + * @throws CannotPerformOperation + * @throws InvalidDigestLength * @throws InvalidKey * @throws SodiumException * @throws TypeError @@ -248,7 +254,7 @@ public static function getSharedSecret( $privateKey->getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ), - 32, + SODIUM_CRYPTO_STREAM_KEYBYTES, (string) $config->HASH_DOMAIN_SEPARATION ) ); @@ -484,6 +490,7 @@ public static function verify( * @param SignaturePublicKey $senderPublicKey Private signing key * @param SecretKey $givenSecretKey Public encryption key * @param string|bool $encoding Which encoding scheme to use? + * * @return HiddenString * * @throws CannotPerformOperation @@ -552,6 +559,6 @@ public static function getAsymmetricConfig( 0, Halite::VERSION_TAG_LEN ); - return Config::getConfig($version, 'encrypt'); + return Config::getConfig($version); } } diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index 20c1b630..ede9f8b2 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -50,7 +50,7 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) * @throws TypeError * @throws SodiumException */ - public function derivePublicKey() + public function derivePublicKey(): EncryptionPublicKey { if (is_null($this->cachedPublicKey)) { $this->cachedPublicKey = sodium_crypto_box_publickey_from_secretkey( diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index f85e2f8c..6da48fa3 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -36,10 +36,10 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) /** * See the appropriate derived class. * @throws CannotPerformOperation - * @return mixed + * @return PublicKey * @codeCoverageIgnore */ - public function derivePublicKey() + public function derivePublicKey(): PublicKey { throw new CannotPerformOperation( 'This is not implemented in the base class' diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index 38d858e6..f834f6c0 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -55,7 +55,7 @@ public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) * @throws SodiumException * @throws TypeError */ - public function derivePublicKey() + public function derivePublicKey(): SignaturePublicKey { if (is_null($this->cachedPublicKey)) { $this->cachedPublicKey = sodium_crypto_sign_publickey_from_secretkey( diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index ba6299e7..d1cb7a55 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -74,6 +74,7 @@ class MutableFile implements StreamInterface /** * MutableFile constructor. * @param string|resource $file + * * @throws InvalidType * @throws FileAccessDenied * @psalm-suppress RedundantConditionGivenDocblockType @@ -132,6 +133,8 @@ public function __construct($file) /** * Close the file handle. * + * @return void + * * @psalm-suppress InvalidPropertyAssignmentValue */ public function close(): void @@ -187,7 +190,9 @@ public function getStreamMetadata(): array * * @param int $num * @param bool $skipTests + * * @return string + * * @throws CannotPerformOperation * @throws FileAccessDenied */ @@ -250,15 +255,17 @@ public function remainingBytes(): int /** * Set the current cursor position to the desired location * - * @param int $i + * @param int $position + * * @return bool + * * @throws CannotPerformOperation * @codeCoverageIgnore */ - public function reset(int $i = 0): bool + public function reset(int $position = 0): bool { - $this->pos = $i; - if (fseek($this->fp, $i, SEEK_SET) === 0) { + $this->pos = $position; + if (fseek($this->fp, $position, SEEK_SET) === 0) { return true; } throw new CannotPerformOperation( @@ -271,6 +278,7 @@ public function reset(int $i = 0): bool * * @param string $buf * @param ?int $num (number of bytes) + * * @return int * * @throws CannotPerformOperation diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 5506f3cd..a2bac8dc 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -151,7 +151,8 @@ public function close(): void * Calculate a BLAKE2b hash of a file * * @return string - * @throws \SodiumException + * + * @throws SodiumException * @throws FileModified * @throws FileError */ @@ -224,10 +225,12 @@ public function getStreamMetadata(): array * decision to make lightly!) * * @param int $num - * @param bool $skipTests Only set this to TRUE if you're absolutely sure - * that you don't want to defend against TOCTOU / - * race condition attacks on the filesystem! + * @param bool $skipTests Only set this to TRUE if you're absolutely sure + * that you don't want to defend against TOCTOU / + * race condition attacks on the filesystem! + * * @return string + * * @throws CannotPerformOperation * @throws FileAccessDenied * @throws FileModified @@ -255,7 +258,6 @@ public function readBytes(int $num, bool $skipTests = false): string break; } // @codeCoverageIgnoreEnd - /** @var string|bool $read */ $read = fread($this->fp, $remaining); if (!is_string($read)) { // @codeCoverageIgnoreStart @@ -290,7 +292,9 @@ public function remainingBytes(): int * Set the current cursor position to the desired location * * @param int $position + * * @return bool + * * @throws CannotPerformOperation */ public function reset(int $position = 0): bool @@ -311,8 +315,9 @@ public function reset(int $position = 0): bool * verifying that the hash matches and the current cursor position/file * size matches their values when the file was first opened. * - * @throws FileModified * @return void + * + * @throws FileModified */ public function toctouTest(): void { @@ -336,7 +341,9 @@ public function toctouTest(): void * * @param string $buf * @param ?int $num (number of bytes) + * * @return int + * * @throws FileAccessDenied */ public function writeBytes(string $buf, ?int $num = null): int diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 502eef2a..2a59fbb1 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -47,15 +47,7 @@ class MerkleTree * @var Node[] */ protected array $nodes = []; - - /** - * @var string - */ protected string $personalization = ''; - - /** - * @var int - */ protected int $outputSize = SODIUM_CRYPTO_GENERICHASH_BYTES; /** @@ -74,6 +66,7 @@ public function __construct(Node ...$nodes) * @param bool $raw - Do we want a raw string instead of a hex string? * * @return string + * * @throws CannotPerformOperation * @throws TypeError * @throws SodiumException @@ -92,7 +85,9 @@ public function getRoot(bool $raw = false): string * Merkle Trees are immutable. Return a replacement with extra nodes. * * @param array $nodes + * * @return MerkleTree + * * @throws InvalidDigestLength */ public function getExpandedTree(Node ...$nodes): MerkleTree @@ -110,7 +105,9 @@ public function getExpandedTree(Node ...$nodes): MerkleTree * Set the hash output size. * * @param int $size + * * @return self + * * @throws InvalidDigestLength */ public function setHashSize(int $size): self @@ -142,6 +139,7 @@ public function setHashSize(int $size): self * Sets the personalization string for the Merkle root calculation * * @param string $str + * * @return self */ public function setPersonalizationString(string $str = ''): self @@ -157,9 +155,10 @@ public function setPersonalizationString(string $str = ''): self * Explicitly recalculate the Merkle root * * @return self + * * @throws CannotPerformOperation - * @throws \TypeError - * @throws \SodiumException + * @throws TypeError + * @throws SodiumException * @codeCoverageIgnore */ public function triggerRootCalculation(): self @@ -174,6 +173,7 @@ public function triggerRootCalculation(): self * to protect against second-preimage attacks * * @return string + * * @throws CannotPerformOperation * @throws TypeError * @throws SodiumException @@ -250,6 +250,7 @@ protected function calculateRoot(): string * Let's go ahead and round up to the nearest multiple of 2 * * @param int $inputSize + * * @return int */ public static function getSizeRoundedUp(int $inputSize): int diff --git a/src/Structure/Node.php b/src/Structure/Node.php index 622cd1d8..8bc083df 100644 --- a/src/Structure/Node.php +++ b/src/Structure/Node.php @@ -24,13 +24,11 @@ */ class Node { - /** - * @var string - */ private string $data; /** * Node constructor. + * * @param string $data */ public function __construct(string $data) @@ -58,6 +56,7 @@ public function getData(): string * @param string $personalization * * @return string + * * @throws CannotPerformOperation * @throws TypeError * @throws SodiumException @@ -83,6 +82,7 @@ public function getHash( * Nodes are immutable, but you can create one with extra data. * * @param string $concat + * * @return Node */ public function getExpandedNode(string $concat): Node diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index 398c4dee..8a9a29d5 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -39,6 +39,7 @@ class TrimmedMerkleTree extends MerkleTree * to protect against second-preimage attacks * * @return string + * * @throws CannotPerformOperation * @throws SodiumException * @throws TypeError @@ -95,10 +96,12 @@ protected function calculateRoot(): string * Merkle Trees are immutable. Return a replacement with extra nodes. * * @param array $nodes + * * @return TrimmedMerkleTree + * * @throws InvalidDigestLength */ - public function getExpandedTree(Node ...$nodes): MerkleTree + public function getExpandedTree(Node ...$nodes): TrimmedMerkleTree { $thisTree = $this->nodes; foreach ($nodes as $node) { diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index afef733b..3ef8f517 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -21,6 +21,7 @@ final class AuthenticationKey extends SecretKey { /** * AuthenticationKey constructor. + * * @param HiddenString $keyMaterial - The actual key data * * @throws InvalidKey diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index db55d9c3..3f198773 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -35,6 +35,7 @@ final class Config extends BaseConfig * * @param string $header * @param string $mode + * * @return self * * @throws InvalidMessage @@ -74,7 +75,9 @@ public static function getConfig( * * @param int $major * @param int $minor + * * @return array + * * @throws InvalidMessage */ public static function getConfigEncrypt(int $major, int $minor): array @@ -123,7 +126,9 @@ public static function getConfigEncrypt(int $major, int $minor): array * * @param int $major * @param int $minor + * * @return array + * * @throws InvalidMessage */ public static function getConfigAuth(int $major, int $minor): array diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 2e3617ef..c62266a8 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -121,7 +121,7 @@ public static function decrypt( EncryptionKey $secretKey, bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { - return self::decryptWithAd( + return self::decryptWithAD( $ciphertext, $secretKey, '', @@ -132,6 +132,14 @@ public static function decrypt( /** * Decrypt a message using the Halite encryption protocol * + * Verifies the MAC before decryption + * - Halite 5+ verifies the BLAKE2b-MAC before decrypting with XChaCha20 + * - Halite 4 and below verifies the BLAKE2b-MAC before decrypting with XSalsa20 + * + * You don't need to worry about chosen-ciphertext attacks. + * You don't need to worry about Invisible Salamanders. + * You don't need to worry about timing attacks on MAC validation. + * * @param string $ciphertext * @param EncryptionKey $secretKey * @param string $additionalData @@ -147,7 +155,7 @@ public static function decrypt( * @throws SodiumException * @throws TypeError */ - public static function decryptWithAd( + public static function decryptWithAD( string $ciphertext, EncryptionKey $secretKey, string $additionalData = '', @@ -245,9 +253,6 @@ public static function decryptWithAd( /** * Encrypt a message using the Halite encryption protocol * - * (Encrypt then MAC -- xsalsa20 then keyed-Blake2b) - * You don't need to worry about chosen-ciphertext attacks. - * * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string|bool $encoding @@ -266,7 +271,7 @@ public static function encrypt( EncryptionKey $secretKey, bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { - return self::encryptWithAd( + return self::encryptWithAD( $plaintext, $secretKey, '', @@ -275,6 +280,15 @@ public static function encrypt( } /** + * Encrypt a message using the Halite encryption protocol + * + * Encrypt then MAC. + * - Halite 5+ uses XChaCha20 then BLAKE2b-MAC + * - Halite 4 and below use XSalsa20 then BLAKE2b-MAC + * + * You don't need to worry about chosen-ciphertext attacks. + * You don't need to worry about Invisible Salamanders. + * * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param string $additionalData @@ -289,7 +303,7 @@ public static function encrypt( * @throws SodiumException * @throws TypeError */ - public static function encryptWithAd( + public static function encryptWithAD( HiddenString $plaintext, EncryptionKey $secretKey, string $additionalData = '', From 7d6cdc8b3a3617c42553aec42de1c03268fd0ae1 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 01:27:17 -0500 Subject: [PATCH 069/134] README: Bump major version, document WithAD --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aabad76b..fc08b52c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ versions of Halite are briefly highlighted below. | | PHP | libsodium | PECL libsodium | Support | |--------------------------------------------------------------|-------|-----------|----------------|---------------------------| | Halite 5.0 and newer | 8.0.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | -| [Halite 4.1+](https://github.com/paragonie/halite/tree/v4.x) | 7.2.0 | 1.0.15 | N/A (standard) | :heavy_check_mark: Active | +| [Halite 4.1+](https://github.com/paragonie/halite/tree/v4.x) | 7.2.0 | 1.0.15 | N/A (standard) | :x: Not Supported | | [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :x: Not Supported | | [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | | [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | @@ -61,7 +61,7 @@ Once you have the prerequisites installed, install Halite through [Composer](htt ### Commercial Support for Older Halite Versions -Free (gratis) support for Halite only extends to the most recent major version (currently 4). +Free (gratis) support for Halite only extends to the most recent major version (currently 5). If your company requires support for an older version of Halite, [contact Paragon Initiative Enterprises](https://paragonie.com/contact) to inquire about @@ -76,14 +76,18 @@ Check out the [documentation](doc). The basic Halite API is designed for simplic * Encryption * Symmetric * `Symmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): `string` + * `Symmetric\Crypto::encryptWithAD`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md), `string`): `string` * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) + * `Symmetric\Crypto::decryptWithAD`(`string`, [`EncryptionKey`](doc/Classes/Symmetric/EncryptionKey.md), `string`): [`HiddenString`](doc/Classes/HiddenString.md) * Asymmetric * Anonymous * `Asymmetric\Crypto::seal`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string` * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) * Authenticated * `Asymmetric\Crypto::encrypt`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::encryptWithAD`([`HiddenString`](doc/Classes/HiddenString.md), [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md), `string`): `string` * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md)): [`HiddenString`](doc/Classes/HiddenString.md) + * `Asymmetric\Crypto::decryptWithAD`(`string`, [`EncryptionSecretKey`](doc/Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](doc/Classes/Asymmetric/EncryptionPublicKey.md), `string`): [`HiddenString`](doc/Classes/HiddenString.md) * Authentication * Symmetric * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](doc/Classes/Symmetric/AuthenticationKey.md)): `string` From 1bbf8acf7661e6f71bae0b0c63e7ade648bdd47e Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 01:44:27 -0500 Subject: [PATCH 070/134] Use HKDF info parameter instead of salt for randomness --- CHANGELOG.md | 3 ++ src/Asymmetric/Config.php | 4 --- src/Config.php | 9 +++++- src/File.php | 47 +++++------------------------- src/Symmetric/Config.php | 3 ++ src/Symmetric/Crypto.php | 43 +++------------------------ src/Util.php | 61 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 87 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e992124..1dc0bf09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ group element, but that isn't necessarily a uniformly random bit string. * **Security:** Halite v5 uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) strategy from PASETO to prevent canonicalization attacks. +* **Security:** Halite v5 appends the random salt to HKDF's `info` parameter instead of + the `salt` parameter. This allows us to meet the KDF Security Definition (which is + stronger than a mere Pseudo-Random Function). ## Version 4.8.0 (2021-04-18) diff --git a/src/Asymmetric/Config.php b/src/Asymmetric/Config.php index a03dd497..29563354 100644 --- a/src/Asymmetric/Config.php +++ b/src/Asymmetric/Config.php @@ -23,10 +23,6 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * @property string|bool ENCODING - * @property string HASH_DOMAIN_SEPARATION - * @property bool HASH_SCALARMULT */ final class Config extends BaseConfig { diff --git a/src/Config.php b/src/Config.php index 5402f7a2..f2d90adc 100644 --- a/src/Config.php +++ b/src/Config.php @@ -21,10 +21,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * + * @property string|bool ENCODING + * + * AsymmetricCrypto: + * @property string HASH_DOMAIN_SEPARATION + * @property bool HASH_SCALARMULT + * + * SymmetricCrypto: * @property bool CHECKSUM_PUBKEY * @property int BUFFER * @property int HASH_LEN - * @property string|bool ENCODING * @property int SHORTEST_CIPHERTEXT_LENGTH * @property int NONCE_BYTES * @property int HKDF_SALT_LEN @@ -32,6 +38,7 @@ * @property string MAC_ALGO * @property int MAC_SIZE * @property int PUBLICKEY_BYTES + * @property bool HKDF_USE_INFO * @property string HKDF_SBOX * @property string HKDF_AUTH * @property bool USE_PAE diff --git a/src/File.php b/src/File.php index 0b9a6d01..89d4e365 100644 --- a/src/File.php +++ b/src/File.php @@ -701,7 +701,7 @@ protected static function encryptData( // @codeCoverageIgnoreEnd // Let's split our key - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // Write the header $output->writeBytes( @@ -814,7 +814,7 @@ protected static function decryptData( $hkdfSalt = $input->readBytes((int) $config->HKDF_SALT_LEN); // Split our keys, begin the HMAC instance - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // VERSION 2+ uses BMAC $mac = sodium_crypto_generichash_init($authKey); @@ -939,7 +939,7 @@ protected static function sealData( * @var string $encKey * @var string $authKey */ - list ($encKey, $authKey) = self::splitKeys($sharedSecretKey, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($sharedSecretKey, $hkdfSalt, $config); // Write the header: $output->writeBytes( @@ -1083,7 +1083,7 @@ protected static function unsealData( * @var string $encKey * @var string $authKey */ - list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); + list ($encKey, $authKey) = Util::splitKeys($key, $hkdfSalt, $config); // We no longer need the original key after we split it unset($key); @@ -1274,6 +1274,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array 'MAC_SIZE' => 32, 'ENC_ALGO' => 'XChaCha20', 'USE_PAE' => true, + 'HKDF_USE_INFO' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1286,6 +1287,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array 'MAC_SIZE' => 32, 'ENC_ALGO' => 'XSalsa20', 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1319,6 +1321,7 @@ protected static function getConfigSeal(int $major, int $minor): array 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, 'ENC_ALGO' => 'XChaCha20', 'USE_PAE' => true, + 'HKDF_USE_INFO' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1334,6 +1337,7 @@ protected static function getConfigSeal(int $major, int $minor): array 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, 'ENC_ALGO' => 'XSalsa20', 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1373,41 +1377,6 @@ protected static function getConfigChecksum(int $major, int $minor): array // @codeCoverageIgnoreEnd } - /** - * Split a key using HKDF-BLAKE2b - * - * @param Key $master - * @param string $salt - * @param Config $config - * @return array - * - * @throws InvalidDigestLength - * @throws CannotPerformOperation - * @throws SodiumException - * @throws TypeError - */ - protected static function splitKeys( - Key $master, - string $salt, - Config $config - ): array { - $binary = $master->getRawKeyMaterial(); - return [ - Util::hkdfBlake2b( - $binary, - SODIUM_CRYPTO_SECRETBOX_KEYBYTES, - (string) $config->HKDF_SBOX, - $salt - ), - Util::hkdfBlake2b( - $binary, - SODIUM_CRYPTO_AUTH_KEYBYTES, - (string) $config->HKDF_AUTH, - $salt - ) - ]; - } - /** * Stream encryption - Do not call directly * diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 3f198773..d7dec55e 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -94,6 +94,7 @@ public static function getConfigEncrypt(int $major, int $minor): array 'USE_PAE' => true, 'MAC_ALGO' => 'BLAKE2b', 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'HKDF_USE_INFO' => true, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -111,6 +112,7 @@ public static function getConfigEncrypt(int $major, int $minor): array 'USE_PAE' => false, 'MAC_ALGO' => 'BLAKE2b', 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -142,6 +144,7 @@ public static function getConfigAuth(int $major, int $minor): array 'MAC_ALGO' => 'BLAKE2b', 'MAC_SIZE' => SODIUM_CRYPTO_GENERICHASH_BYTES_MAX, 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'HKDF_USE_INFO' => $major > 4, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index c62266a8..5d623c00 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -33,7 +33,8 @@ random_bytes, sodium_crypto_generichash, sodium_crypto_stream_xchacha20_xor, - sodium_crypto_stream_xor; + sodium_crypto_stream_xor, + str_repeat; /** * Class Crypto @@ -203,7 +204,7 @@ public static function decryptWithAD( * @var string $encKey * @var string $authKey */ - $split = self::splitKeys($secretKey, $salt, $config); + $split = Util::splitKeys($secretKey, $salt, $config); $encKey = $split[0]; $authKey = $split[1]; @@ -327,7 +328,7 @@ public static function encryptWithAD( This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ - [$encKey, $authKey] = self::splitKeys($secretKey, $salt, $config); + [$encKey, $authKey] = Util::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: @@ -383,42 +384,6 @@ public static function encryptWithAD( return $message; } - /** - * Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*) - * - * @param EncryptionKey $master - * @param string $salt - * @param BaseConfig $config - * - * @return string[] - * - * @throws CannotPerformOperation - * @throws InvalidDigestLength - * @throws SodiumException - * @throws TypeError - */ - public static function splitKeys( - EncryptionKey $master, - string $salt, - BaseConfig $config - ): array { - $binary = $master->getRawKeyMaterial(); - return [ - Util::hkdfBlake2b( - $binary, - SODIUM_CRYPTO_SECRETBOX_KEYBYTES, - (string) $config->HKDF_SBOX, - $salt - ), - Util::hkdfBlake2b( - $binary, - SODIUM_CRYPTO_AUTH_KEYBYTES, - (string) $config->HKDF_AUTH, - $salt - ) - ]; - } - /** * Unpack a message string into an array (assigned to variables via list()). * diff --git a/src/Util.php b/src/Util.php index b8ec0915..4094028b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -12,6 +12,7 @@ InvalidDigestLength, InvalidType }; +use ParagonIE\Halite\Symmetric\EncryptionKey; use RangeException; use SodiumException; use Throwable; @@ -331,6 +332,66 @@ public static function safeStrcpy(string $string): string return $return; } + /** + * Split a key (using HKDF-BLAKE2b instead of HKDF-HMAC-*) + * + * @param EncryptionKey $master + * @param string $salt + * @param Config $config + * + * @return string[] + * + * @throws CannotPerformOperation + * @throws InvalidDigestLength + * @throws SodiumException + * @throws TypeError + */ + public static function splitKeys( + EncryptionKey $master, + string $salt, + Config $config + ): array { + $binary = $master->getRawKeyMaterial(); + + /* + * From Halite version 5, we use the HKDF info parameter instead of the salt. + * This does two things: + * + * 1. It allows us to use the HKDF security definition (which is stronger than a PRF) + * 2. It allows us to reuse the intermediary step and make key derivation faster. + */ + if ($config->HKDF_USE_INFO) { + $prk = self::raw_keyed_hash( + $binary, + str_repeat("\x00", SODIUM_CRYPTO_GENERICHASH_KEYBYTES) + ); + $return = [ + self::raw_keyed_hash(((string) $config->HKDF_SBOX) . $salt . "\x01", $prk), + self::raw_keyed_hash(((string) $config->HKDF_AUTH) . $salt . "\x01", $prk) + ]; + self::memzero($prk); + return $return; + } + + /* + * Halite 4 and blow used this strategy: + */ + return [ + Util::hkdfBlake2b( + $binary, + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + (string) $config->HKDF_SBOX, + $salt + ), + Util::hkdfBlake2b( + $binary, + SODIUM_CRYPTO_AUTH_KEYBYTES, + (string) $config->HKDF_AUTH, + $salt + ) + ]; + } + /** * Turn a string into an array of integers * From 21cb61dbb67ae9fd5b12cbbfdda06490ed749c35 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:05:56 -0500 Subject: [PATCH 071/134] Begin Halite v5 documentation changes --- doc/Basic.md | 117 +++++++++++++++++---- doc/Classes/Alerts/FileError.md | 5 + doc/Classes/Alerts/FileModified.md | 2 +- doc/Classes/Alerts/HaliteAlertInterface.md | 5 + doc/Classes/README.md | 2 + doc/README.md | 2 + 6 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 doc/Classes/Alerts/FileError.md create mode 100644 doc/Classes/Alerts/HaliteAlertInterface.md diff --git a/doc/Basic.md b/doc/Basic.md index 39724878..95273105 100644 --- a/doc/Basic.md +++ b/doc/Basic.md @@ -2,24 +2,28 @@ This is the Basic Halite API: - * Encryption +* Encryption * Symmetric - * `Symmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `string` - * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `bool?`): `HiddenString` + * `Symmetric\Crypto::encrypt`([`HiddenString`](Classes/HiddenString.md), [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)): `string` + * `Symmetric\Crypto::encryptWithAD`([`HiddenString`](Classes/HiddenString.md), [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `string`): `string` + * `Symmetric\Crypto::decrypt`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)): [`HiddenString`](Classes/HiddenString.md) + * `Symmetric\Crypto::decryptWithAD`(`string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md), `string`): [`HiddenString`](Classes/HiddenString.md) * Asymmetric - * Anonymous - * `Asymmetric\Crypto::seal`(`HiddenString`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string` - * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), `bool?`): `HiddenString` - * Authenticated - * `Asymmetric\Crypto::encrypt`(`HiddenString`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `string` - * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `bool?`): `HiddenString` - * Authentication + * Anonymous + * `Asymmetric\Crypto::seal`([`HiddenString`](Classes/HiddenString.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::unseal`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): [`HiddenString`](Classes/HiddenString.md) + * Authenticated + * `Asymmetric\Crypto::encrypt`([`HiddenString`](Classes/HiddenString.md), [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `string` + * `Asymmetric\Crypto::encryptWithAD`([`HiddenString`](Classes/HiddenString.md), [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `string`): `string` + * `Asymmetric\Crypto::decrypt`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): [`HiddenString`](Classes/HiddenString.md) + * `Asymmetric\Crypto::decryptWithAD`(`string`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md), [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md), `string`): [`HiddenString`](Classes/HiddenString.md) +* Authentication * Symmetric - * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` - * `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `string`, `bool?`): `bool` + * `Symmetric\Crypto::authenticate`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md)): `string` + * `Symmetric\Crypto::verify`(`string`, [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md), `string`): `bool` * Asymmetric - * `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md), `bool?`): `string` - * `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md), `string`, `bool?`): `bool` + * `Asymmetric\Crypto::sign`(`string`, [`SignatureSecretKey`](Classes/Asymmetric/SignatureSecretKey.md)): `string` + * `Asymmetric\Crypto::verify`(`string`, [`SignaturePublicKey`](Classes/Asymmetric/SignaturePublicKey.md), `string`): `bool` Most of the other [Halite features](Features.md) build on top of these simple APIs. @@ -67,7 +71,7 @@ Later, you can load it like so: $enc_key = KeyFactory::loadEncryptionKey('/path/to/encryption.key'); ``` -Or if you want to store it in a string +Or if you want to store it in a string, rather than on the filesystem: ```php $key_hex = KeyFactory::export($enc_key)->getString(); @@ -94,7 +98,7 @@ $ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encrypt( ); ``` -By default, `Crypto::encrypt()` will return a hexadecimal encoded string. If you +By default, `Crypto::encrypt()` will return a base64url-encoded string. If you want raw binary, simply pass `true` as the third argument (similar to the API used by PHP's `hash()` function). @@ -113,6 +117,57 @@ instance of `\ParagonIE\Halite\Symmetric\EncryptionKey`. If you're attempting to decrypt a raw binary string rather than a hex-encoded string, pass `true` to the third argument of `Crypto::decrypt`. +#### Additional Associated Data + +Sometimes encrypting a message isn't sufficient protection, and you also want to +bind an encrypted message to some context. Usually, this happens when you're concerned +with Confused Deputy Attacks. + +The simplest way to accomplish this is to use Halite's `EncryptWithAD()` and `DecryptWithAD()` +methods. + +**Note:** The Additional Associated Data is **NOT** stored in the encrypted message. +You must manage these strings yourself to ensure successful decryption. + +```php +use ParagonIE\HiddenString\HiddenString; + +$ad = 'Additional data that must be passed to both encrypt and decrypt calls'; + +$ciphertext = \ParagonIE\Halite\Symmetric\Crypto::encryptWithAD( + new HiddenString( + "Your message here. Any string content will do just fine." + ), + $enc_key, + $ad +); +``` + +This string must also be provided in the other direction: + +```php +$plaintext = \ParagonIE\Halite\Symmetric\Crypto::decryptWithAD( + $ciphertext, + $enc_key, + $ad +); +``` + +This will not succeed: + +```php +try { + \ParagonIE\Halite\Symmetric\Crypto::decryptWithAD( + $ciphertext, + $enc_key, + 'Incorrect String' + ); +} catch (\ParagonIE\Halite\Alerts\HaliteAlert $ex) { + var_dump($ex->getMessage()); + exit; +} +``` + ### Authenticated Asymmetric-Key Encryption (Encrypting) This API facilitates message encryption between to participants in a @@ -131,8 +186,6 @@ $send_to_bob = sodium_bin2hex($alice_public->getRawKeyMaterial()); Alice will then load Bob's public key into the appropriate object like so: ```php -use ParagonIE\HiddenString\HiddenString; - $bob_public = new \ParagonIE\Halite\Asymmetric\EncryptionPublicKey( new HiddenString( sodium_hex2bin($recv_from_bob) @@ -168,6 +221,34 @@ $message = \ParagonIE\Halite\Asymmetric\Crypto::decrypt( ); ``` +#### Additional Associated Data with Asymmetric Encryption + +If you've read the section on Symmetric Encryption, this should be unsurprising. + +```php +$ad = 'Additional Data that must be asserted on decrypt'; + +$send_to_bob = \ParagonIE\Halite\Asymmetric\Crypto::encryptWithAD( + new HiddenString( + "Your message here. Any string content will do just fine." + ), + $alice_secret, + $bob_public, + $ad +); +``` + +And decryption is similarly straightforward: + +```php +$message = \ParagonIE\Halite\Asymmetric\Crypto::decryptWithAD( + $received_ciphertext, + $alice_secret, + $bob_public, + $ad +); +``` + ### Anonymous Asymmetric-Key Encryption (Sealing) A sealing interface is one where you encrypt a message with a public key, such diff --git a/doc/Classes/Alerts/FileError.md b/doc/Classes/Alerts/FileError.md new file mode 100644 index 00000000..1068243e --- /dev/null +++ b/doc/Classes/Alerts/FileError.md @@ -0,0 +1,5 @@ +# FileError extends [HaliteAlert](HaliteAlert.md) + +**Namespace**: `\ParagonIE\Halite\Alerts` + +This indicates a filesystem error occurred. diff --git a/doc/Classes/Alerts/FileModified.md b/doc/Classes/Alerts/FileModified.md index 6f0263a8..feccf3be 100644 --- a/doc/Classes/Alerts/FileModified.md +++ b/doc/Classes/Alerts/FileModified.md @@ -1,4 +1,4 @@ -# FileModified extends [HaliteAlert](HaliteAlert.md) +# FileModified extends [FileError](FileError.md) **Namespace**: `\ParagonIE\Halite\Alerts` diff --git a/doc/Classes/Alerts/HaliteAlertInterface.md b/doc/Classes/Alerts/HaliteAlertInterface.md new file mode 100644 index 00000000..f2a5cc52 --- /dev/null +++ b/doc/Classes/Alerts/HaliteAlertInterface.md @@ -0,0 +1,5 @@ +# HaliteAlertInterface extends Throwable + +**Namespace**: `\ParagonIE\Halite\Alerts` + +This is just a common interface for all Halite Alerts. diff --git a/doc/Classes/README.md b/doc/Classes/README.md index 653dc05f..c968f7bb 100644 --- a/doc/Classes/README.md +++ b/doc/Classes/README.md @@ -6,8 +6,10 @@ * [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Alerts/CannotSerializeKey.md) * [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Alerts/ConfigDirectiveNotFound.md) * [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Alerts/FileAccessDenied.md) + * [`\ParagonIE\Halite\Alerts\FileError`](Alerts/FileError.md) * [`\ParagonIE\Halite\Alerts\FileModified`](Alerts/FileModified.md) * [`\ParagonIE\Halite\Alerts\HaliteAlert`](Alerts/HaliteAlert.md) (Base Exception for all Alerts) + * [`\ParagonIE\Halite\Alerts\HaliteAlertInterface`](Alerts/HaliteAlertInterface.md) (Common Interface) * [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Alerts/InvalidDigestLength.md) * [`\ParagonIE\Halite\Alerts\InvalidFlags`](Alerts/InvalidFlags.md) * [`\ParagonIE\Halite\Alerts\InvalidKey`](Alerts/InvalidKey.md) diff --git a/doc/README.md b/doc/README.md index 0d6ef7f5..d97456f5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,8 +13,10 @@ * [`\ParagonIE\Halite\Alerts\CannotSerializeKey`](Classes/Alerts/CannotSerializeKey.md) * [`\ParagonIE\Halite\Alerts\ConfigDirectiveNotFound`](Classes/Alerts/ConfigDirectiveNotFound.md) * [`\ParagonIE\Halite\Alerts\FileAccessDenied`](Classes/Alerts/FileAccessDenied.md) + * [`\ParagonIE\Halite\Alerts\FileError`](Classes/Alerts/FileError.md) * [`\ParagonIE\Halite\Alerts\FileModified`](Classes/Alerts/FileModified.md) * [`\ParagonIE\Halite\Alerts\HaliteAlert`](Classes/Alerts/HaliteAlert.md) (Base Exception for all Alerts) + * [`\ParagonIE\Halite\Alerts\HaliteAlertInterface`](Classes/Alerts/HaliteAlertInterface.md) (Common Interface) * [`\ParagonIE\Halite\Alerts\InvalidDigestLength`](Classes/Alerts/InvalidDigestLength.md) * [`\ParagonIE\Halite\Alerts\InvalidFlags`](Classes/Alerts/InvalidFlags.md) * [`\ParagonIE\Halite\Alerts\InvalidKey`](Classes/Alerts/InvalidKey.md) From 40fd1cea72c92ea9a04892a8a287c2f6fe228101 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:10:41 -0500 Subject: [PATCH 072/134] Cover new File methods --- doc/Classes/File.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doc/Classes/File.md b/doc/Classes/File.md index 88800e08..998df7cf 100644 --- a/doc/Classes/File.md +++ b/doc/Classes/File.md @@ -36,6 +36,34 @@ Both `$input` and `$output` can be a string, a resource, or an object whose clas In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must be an instance of [`MutableFile`](Stream/MutableFile.md). +### `asymmetricDecrypt()` + +> `public static` asymmetricDecrypt(`$input`, `$output`, [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md) `$recipientSK`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$senderPK`, `string $aad = null`): `int` + +Decrypt the contents of `$input` (either a string containing the path to a file, or an open file +handle), and store it in the file (handle?) at `$output`. + +Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`. +In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must +be an instance of [`MutableFile`](Stream/MutableFile.md). + +The difference between `asymmetricDecrypt()` and `deseal()` is that `asymmetricDecrypt()` authenticates the sender, +while `unseal()` does not. (You can think of `unseal()` as anonymous public-key decryption.) + +### `asymmetricEncrypt()` + +> `public static` asymmetricEncrypt(`$input`, `$output`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$recipientPK`, [`EncryptionSecretKey`](Asymmetric/EncryptionSecretKey.md) `$senderSK`, `string $aad = null`): `int` + +Encrypt the contents of `$input` (either a string containing the path to a file, or an open file +handle), and store it in the file (handle?) at `$output`. + +Both `$input` and `$output` can be a string, a resource, or an object whose class implements `StreamInterface`. +In the object case, `$input` must be an instance of [`ReadOnlyFile`](Stream/ReadOnlyFile.md) and `$output` must +be an instance of [`MutableFile`](Stream/MutableFile.md). + +The difference between `asymmetricEncrypt()` and `seal()` is that `asymmetricEncrypt()` authenticates the sender, while +`seal()` does not. (You can think of `seal()` as anonymous public-key encryption.) + ### `seal()` > `public static` seal(`$input`, `$output`, [`EncryptionPublicKey`](Asymmetric/EncryptionPublicKey.md) `$key`): `string` From 81038227cebf936ffbe48372ef7e35065c0ddfe9 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:14:37 -0500 Subject: [PATCH 073/134] Cover AEAD better --- doc/Classes/Asymmetric/Crypto.md | 23 ++++++++++++++++------- doc/Classes/Symmetric/Crypto.md | 14 ++++++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/doc/Classes/Asymmetric/Crypto.md b/doc/Classes/Asymmetric/Crypto.md index 5398e846..63a74bb8 100644 --- a/doc/Classes/Asymmetric/Crypto.md +++ b/doc/Classes/Asymmetric/Crypto.md @@ -6,11 +6,14 @@ ### `getSharedSecret()` -> `public` getSharedSecret([`EncryptionSecretKey`](EncryptionSecretKey.md) `$privateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$get_as_object = false`) : [`EncryptionKey`](../Symmetric/EncryptionKey.md) +> `public` getSharedSecret([`EncryptionSecretKey`](EncryptionSecretKey.md) `$privateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$publicKey`, `$get_as_object = false`, [`?Config`](Config.md) `$config = null`) : [`EncryptionKey`](../Symmetric/EncryptionKey.md) This method calculates a shared [`EncryptionKey`](../Symmetric/EncryptionKey.md) using X25519 (Elliptic Curve Diffie Hellman key agreement over Curve25519). +In Halite v5+, this X25519 output is processed with HKDF-BLAKE2b to ensure a uniformly +random bit string is returned, rather than merely a random group element. + ### `encrypt()` > `public` encrypt(`HiddenString $source`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `$encoding = Halite::ENCODE_BASE64URLSAFE`) : `string` @@ -44,17 +47,23 @@ This method will: key (step 4). 7. Return what should be the original plaintext. -### `encryptWithAd()` +### `encryptWithAD()` + +> `public` encryptWithAD(`HiddenString $plaintext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` + +This is similar to `encrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -> `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. -This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +### `decryptWithAD()` -### `decryptWithAd()` +> `public` decryptWithAD(`string $ciphertext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` -> `public` decryptWithAd(`string $ciphertext`, [`EncryptionSecretKey`](EncryptionSecretKey.md) `$ourPrivateKey`, [`EncryptionPublicKey`](EncryptionPublicKey.md) `$theirPublicKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` +This is similar to `decrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. ### `seal()` diff --git a/doc/Classes/Symmetric/Crypto.md b/doc/Classes/Symmetric/Crypto.md index 9b7d560c..b9b5ec96 100644 --- a/doc/Classes/Symmetric/Crypto.md +++ b/doc/Classes/Symmetric/Crypto.md @@ -45,13 +45,19 @@ Verify-then-decrypt a message. This method will: > `public` encryptWithAd(`HiddenString $plaintext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `string` -This is similar to `encrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +This is similar to `encrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). -### `decryptWithAd()` +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. -> `public` decryptWithAd(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` +### `decryptWithAD()` -This is similar to `decrypt()`, except the `$additionalData` string is prepended to the ciphertext (after the nonce) when calculating the Message Authentication Code (MAC). +> `public` decryptWithAD(`string $ciphertext`, [`EncryptionKey`](EncryptionKey.md) `$secretKey`, `string $additionalData = ''`, `$encoding = Halite::ENCODE_BASE64URLSAFE`): `HiddenString` + +This is similar to `decrypt()`, except the `$additionalData` string is covered by the Message Authentication Code (MAC). + +Since Halite v5, this uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) +concept from PASETO. ### `verify()` From f84ca26bf4f425df52e5ee8f063e573c73dd67d9 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:15:22 -0500 Subject: [PATCH 074/134] Fix nit --- doc/Classes/File.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Classes/File.md b/doc/Classes/File.md index 998df7cf..04b0bd98 100644 --- a/doc/Classes/File.md +++ b/doc/Classes/File.md @@ -96,7 +96,7 @@ Calculate a digital signature of a file. ### `verify()` -> `public static` sign(`$input`, [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md) `$key`, `string $signature`, `boolean $raw_binary`): `bool` +> `public static` verify(`$input`, [`SignaturePublicKey`](Asymmetric/SignaturePublicKey.md) `$key`, `string $signature`, `boolean $raw_binary`): `bool` Verifies a digital signature of a file. From e8086190f2a0215eeae242dc68922330ca5a009c Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:17:56 -0500 Subject: [PATCH 075/134] Document splitKeys --- doc/Classes/Util.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/Classes/Util.md b/doc/Classes/Util.md index 0aa7647f..aabac892 100644 --- a/doc/Classes/Util.md +++ b/doc/Classes/Util.md @@ -51,6 +51,16 @@ Returns a copy of a string without triggering PHP's optimizations. The string returned by this method can safely be used with `sodium_memzero()` without corrupting other copies of the same string. +### `splitKeys()` + +Splits a single key into two distinct keys (one for encryption, one for authentication). + +Since Halite v5, the HKDF salt parameter is not used. Instead, this randomness is appended +to the HKDF info parameter, in order to meet the [standard security definition for HKDF](https://eprint.iacr.org/2010/264). + +Additionally, this allows us to reuse the PRK (the value affected by the HKDF salt) value +for both derived keys, which results in a nice performance gain. + ### `xorStrings()` > `public static` xorStrings(`string $left`, `string $right`): `string` From 0580191a547977a45538f863c666bbf5d946eb4a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:21:18 -0500 Subject: [PATCH 076/134] Update Primitives doc --- doc/Primitives.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/Primitives.md b/doc/Primitives.md index bec1e7ce..0fabd1db 100644 --- a/doc/Primitives.md +++ b/doc/Primitives.md @@ -1,16 +1,17 @@ # Cryptography Primitives used in Halite * Symmetric-key encryption: (note: only [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken) is available through Halite) - * [**XChaCha20**](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20) - * Previously, [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) + * [**XChaCha20**](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20) then BLAKE2b-MAC + * Previously, [**XSalsa20**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-stream) then BLAKE2b-MAC * Symmetric-key authentication: **[BLAKE2b](https://download.libsodium.org/doc/hashing/generic_hashing.html#singlepart-example-with-a-key)** (keyed) -* Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) followed by symmetric-key authenticated encryption +* Asymmetric-key encryption: [**X25519**](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-scalarmult) + then [**HKDF-BLAKE2b**](Classes/Util.md#raw_keyed_hash), followed by symmetric-key authenticated encryption * Asymmetric-key digital signatures: [**Ed25519**](https://paragonie.com/book/pecl-libsodium/read/05-publickey-crypto.md#crypto-sign) * Checksums: [**BLAKE2b**](https://paragonie.com/book/pecl-libsodium/read/06-hashing.md#crypto-generichash) -* Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md) +* Key splitting: [**HKDF-BLAKE2b**](Classes/Util.md#splitkeys) * Password-Based Key Derivation: [**Argon2**](https://paragonie.com/book/pecl-libsodium/read/07-password-hashing.md#crypto-pwhash-str) -In all cases, we follow an Encrypt then MAC construction, thus avoiding the [cryptographic doom principle](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). +In all cases, we follow an Encrypt-then-MAC construction, thus avoiding the [cryptographic doom principle](https://moxie.org/2011/12/13/the-cryptographic-doom-principle.html). As a consequence of our use of a keyed BLAKE2b hash as a MAC, instead of GCM/Poly1305, Halite ciphertexts are [**message committing**](https://eprint.iacr.org/2020/1456) which makes ciphertexts random key robust. From 51453605277b04bc920a9600209856251f08a932 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 19 Jan 2022 02:35:42 -0500 Subject: [PATCH 077/134] Prioritize security entries in CHANGELOG Also, clarify the risk of not hashing the ECDH output for asymmetric encryption --- CHANGELOG.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc0bf09..723daebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,24 @@ # Changelog -## Version 5.0.0 (Unreleased) +## Version 5.0.0 (2022-01-19) * Increased minimum PHP version to 8.0. -* Encryption now uses XChaCha20 instead of XSalsa20. -* The `File` class no longer supports the `resource` type. To migrate code, wrap your - `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. -* Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. -* **Security:** Asymmetric encryption now uses HKDF-BLAKE2b to extract a 256-bit - uniformly random bit string for the encryption key, rather than using the raw - X25519 output directly as an encryption key. - - This is important because Elliptic Curve Diffie-Hellman results in a random - group element, but that isn't necessarily a uniformly random bit string. +* **Security:** Asymmetric encryption now uses HKDF-BLAKE2b to extract a 256-bit uniformly random bit string for the + encryption key, rather than using the raw X25519 output directly as an encryption key. This is important because + Elliptic Curve Diffie-Hellman results in a random group element, but that isn't necessarily a uniformly random bit + string. + * Because Halite v4 and earlier did not perform this step, it's superficially susceptible to + [Cheon's attack](https://crypto.stackexchange.com/a/67609). This reduces the effective security + from 125 bits (Pollard's rho) to 123 bits, but neither is a practical concern today. * **Security:** Halite v5 uses the [PAE](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md#pae-definition) strategy from PASETO to prevent canonicalization attacks. * **Security:** Halite v5 appends the random salt to HKDF's `info` parameter instead of the `salt` parameter. This allows us to meet the KDF Security Definition (which is stronger than a mere Pseudo-Random Function). +* Encryption now uses XChaCha20 instead of XSalsa20. +* The `File` class no longer supports the `resource` type. To migrate code, wrap your + `resource` arguments in a `ReadOnlyFile` or `MutableFile` object. +* Added `File::asymmetricEncrypt()` and `File::asymmetricDecrypt()`. ## Version 4.8.0 (2021-04-18) From 666f9770a12bd8a49cefba16c759baa4843dbf5a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 23 May 2022 01:02:50 -0400 Subject: [PATCH 078/134] Halite 5.1: Drop PHP 8.0 support See #178 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 9 +++++++++ README.md | 7 +++++-- composer.json | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebb19e60..04dffdc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.0', '8.1'] + php-versions: ['8.1'] phpunit-versions: ['latest'] steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 723daebd..acd5aa81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Version 5.1.0 (2202-05-23) + +* Dropped PHP 8.0 support, increased minimum PHP version to 8.1. + * This is due to the significant performance difference between ext/sodium + and sodium_compat, and the functions we use in 5.x aren't available until + PHP 8.1. See [#178](https://github.com/paragonie/halite/issues/178). +* The 5.0.x branch will continue to *function* on PHP 8.0 but performance is + not guaranteed. + ## Version 5.0.0 (2022-01-19) * Increased minimum PHP version to 8.0. diff --git a/README.md b/README.md index fc08b52c..143543c4 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,17 @@ versions of Halite are briefly highlighted below. | | PHP | libsodium | PECL libsodium | Support | |--------------------------------------------------------------|-------|-----------|----------------|---------------------------| -| Halite 5.0 and newer | 8.0.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | +| Halite 5.1 and newer | 8.1.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | +| Halite 5.0.x | 8.0.0 | 1.0.18 | N/A (standard) | :heavy_check_mark: Active | | [Halite 4.1+](https://github.com/paragonie/halite/tree/v4.x) | 7.2.0 | 1.0.15 | N/A (standard) | :x: Not Supported | | [Halite 4.0](https://github.com/paragonie/halite/tree/v4.0) | 7.2.0 | 1.0.13 | N/A (standard) | :x: Not Supported | | [Halite 3](https://github.com/paragonie/halite/tree/v3.x) | 7.0.0 | 1.0.9 | 1.0.6 / 2.0.4 | :x: Not Supported | | [Halite 2](https://github.com/paragonie/halite/tree/v2.2) | 7.0.0 | 1.0.9 | 1.0.6 | :x: Not Supported | | [Halite 1](https://github.com/paragonie/halite/tree/v1.x) | 5.6.0 | 1.0.6 | 1.0.2 | :x: Not Supported | -If you need a version of Halite before 5.0, see the documentation relevant to that +Note: Halite 5.0.x works on PHP 8.0, but performance is worse than on PHP 8.1. + +If you need a version of Halite before 5.1, see the documentation relevant to that particular branch. **To install Halite, you first need to [install libsodium](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium).** diff --git a/composer.json b/composer.json index f159981b..c32fea21 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ } ], "require": { - "php": "^8", + "php": "^8.1", "ext-json": "*", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1|^2", From ae6a31c6f0bde20097b7f31217163645c8711af8 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 23 May 2022 01:06:39 -0400 Subject: [PATCH 079/134] Use newer dependencies in CI This will prevent weird behavior with composer.lock caching. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04dffdc1..c317f985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: tools: psalm, phpunit:${{ matrix.phpunit-versions }} - name: Install dependencies - run: composer install + run: composer update - name: PHPUnit tests uses: php-actions/phpunit@v2 From 09fbcb5867f95daffd3fde6e52b73e56434fd06d Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 23 May 2022 01:09:44 -0400 Subject: [PATCH 080/134] Fix CI --- .github/workflows/ci.yml | 28 +++++++++++++--------------- .github/workflows/psalm.yml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/psalm.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c317f985..79827938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,36 +1,34 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: - modern: + phpunit: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} strategy: matrix: operating-system: ['ubuntu-latest'] php-versions: ['8.1'] - phpunit-versions: ['latest'] + steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl, sodium - ini-values: post_max_size=256M, max_execution_time=180 - tools: psalm, phpunit:${{ matrix.phpunit-versions }} + ini-values: error_reporting=-1, display_errors=On + coverage: none - - name: Install dependencies - run: composer update + - name: Install Composer dependencies + uses: "ramsey/composer-install@v1" - name: PHPUnit tests - uses: php-actions/phpunit@v2 - timeout-minutes: 30 - with: - memory_limit: 256M - - - name: Static Analysis - run: vendor/bin/psalm + run: vendor/bin/phpunit \ No newline at end of file diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 00000000..81068412 --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,34 @@ +name: Psalm + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + psalm: + name: Psalm on PHP ${{ matrix.php-versions }} + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.1'] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: psalm:4 + coverage: none + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v1" + with: + composer-options: --no-dev + + - name: Static Analysis + run: psalm From 9cad0983cb025f8ed19810e98391a053c6810329 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 23 May 2022 01:15:34 -0400 Subject: [PATCH 081/134] Fix psalm.xml --- psalm.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psalm.xml b/psalm.xml index 69c5b316..5d159e6a 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,7 @@ +> @@ -11,6 +11,7 @@ + From cfafc301614493a8e2f73e4ba7fe430fcb6d8a1d Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 23 May 2022 01:16:04 -0400 Subject: [PATCH 082/134] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 143543c4..78f99156 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Halite [![Build Status](https://github.com/paragonie/halite/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/halite/actions) +[![Static Analysis](https://github.com/paragonie/halite/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/halite/actions) [![Latest Stable Version](https://poser.pugx.org/paragonie/halite/v/stable)](https://packagist.org/packages/paragonie/halite) [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) From 94e4bedc9f63823043d3fa291e32f61c9b0cbba1 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Fri, 19 Apr 2024 19:28:18 -0400 Subject: [PATCH 083/134] Update dependencies, test on newer PHP --- .github/workflows/ci.yml | 6 +++--- .github/workflows/psalm.yml | 6 +++--- CHANGELOG.md | 7 ++++++- composer.json | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79827938..9ca94603 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,11 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1'] + php-versions: ['8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -28,7 +28,7 @@ jobs: coverage: none - name: Install Composer dependencies - uses: "ramsey/composer-install@v1" + uses: "ramsey/composer-install@v2" - name: PHPUnit tests run: vendor/bin/phpunit \ No newline at end of file diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 81068412..f5bd5c58 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,10 +13,10 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1'] + php-versions: ['8.3'] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -26,7 +26,7 @@ jobs: coverage: none - name: Install Composer dependencies - uses: "ramsey/composer-install@v1" + uses: "ramsey/composer-install@v2" with: composer-options: --no-dev diff --git a/CHANGELOG.md b/CHANGELOG.md index acd5aa81..b4a7cc40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## Version 5.1.0 (2202-05-23) +## Version 5.1.1 (2024-04-19) + +* Support both sodium_compat v1 and v2. + [Learn more here](https://paragonie.com/blog/2024/04/release-sodium-compat-v2-and-future-our-polyfill-libraries). + +## Version 5.1.0 (2022-05-23) * Dropped PHP 8.0 support, increased minimum PHP version to 8.1. * This is due to the significant performance difference between ext/sodium diff --git a/composer.json b/composer.json index c32fea21..e9937d53 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "ext-json": "*", "paragonie/constant_time_encoding": "^2", "paragonie/hidden-string": "^1|^2", - "paragonie/sodium_compat": "^1.17" + "paragonie/sodium_compat": "^1|^2" }, "autoload": { "psr-4": { From 9744775a6d4c0157bdd93877d69b55762afb397b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 8 May 2024 08:50:55 -0400 Subject: [PATCH 084/134] Update dependency on constant_time_encoding --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e9937d53..cae05136 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require": { "php": "^8.1", "ext-json": "*", - "paragonie/constant_time_encoding": "^2", + "paragonie/constant_time_encoding": "^2|^3", "paragonie/hidden-string": "^1|^2", "paragonie/sodium_compat": "^1|^2" }, From ce69a91f59ffb8fd19353d01d72459c63794191a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 8 May 2024 08:57:18 -0400 Subject: [PATCH 085/134] Use SensitiveParameter --- src/Asymmetric/Crypto.php | 14 ++++++++++++++ src/Asymmetric/EncryptionSecretKey.php | 7 +++++-- src/Asymmetric/SecretKey.php | 7 +++++-- src/Asymmetric/SignatureSecretKey.php | 7 +++++-- src/Cookie.php | 8 ++++++-- src/EncryptionKeyPair.php | 6 ++++-- src/Password.php | 10 ++++++++++ src/SignatureKeyPair.php | 6 ++++-- src/Symmetric/AuthenticationKey.php | 6 ++++-- src/Symmetric/EncryptionKey.php | 6 ++++-- 10 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 3f9362c9..42c8baa3 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -84,7 +84,9 @@ final private function __construct() * @throws TypeError */ public static function encrypt( + #[\SensitiveParameter] HiddenString $plaintext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE @@ -118,9 +120,12 @@ public static function encrypt( * @throws TypeError */ public static function encryptWithAD( + #[\SensitiveParameter] HiddenString $plaintext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, + #[\SensitiveParameter] string $additionalData = '', string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { @@ -163,6 +168,7 @@ public static function encryptWithAD( */ public static function decrypt( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE @@ -198,8 +204,10 @@ public static function decrypt( */ public static function decryptWithAD( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $ourPrivateKey, EncryptionPublicKey $theirPublicKey, + #[\SensitiveParameter] string $additionalData = '', string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { @@ -241,6 +249,7 @@ public static function decryptWithAD( * @throws TypeError */ public static function getSharedSecret( + #[\SensitiveParameter] EncryptionSecretKey $privateKey, EncryptionPublicKey $publicKey, bool $get_as_object = false, @@ -291,6 +300,7 @@ public static function getSharedSecret( * @throws TypeError */ public static function seal( + #[\SensitiveParameter] HiddenString $plaintext, EncryptionPublicKey $publicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE @@ -321,6 +331,7 @@ public static function seal( */ public static function sign( string $message, + #[\SensitiveParameter] SignatureSecretKey $privateKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { @@ -355,6 +366,7 @@ public static function sign( */ public static function signAndEncrypt( HiddenString $message, + #[\SensitiveParameter] SignatureSecretKey $secretKey, PublicKey $recipientPublicKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE @@ -393,6 +405,7 @@ public static function signAndEncrypt( */ public static function unseal( string $ciphertext, + #[\SensitiveParameter] EncryptionSecretKey $privateKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { @@ -505,6 +518,7 @@ public static function verify( public static function verifyAndDecrypt( string $ciphertext, SignaturePublicKey $senderPublicKey, + #[\SensitiveParameter] SecretKey $givenSecretKey, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): HiddenString { diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index ede9f8b2..c361ab15 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -28,8 +28,11 @@ final class EncryptionSecretKey extends SecretKey * @throws InvalidKey * @throws TypeError */ - public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey( sprintf( diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index 6da48fa3..ee8d7f62 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -24,8 +24,11 @@ class SecretKey extends Key * * @throws TypeError */ - public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { parent::__construct($keyMaterial); if (!is_null($pk)) { $this->cachedPublicKey = $pk->getString(); diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index f834f6c0..355db43d 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -33,8 +33,11 @@ final class SignatureSecretKey extends SecretKey * @throws InvalidKey * @throws TypeError */ - public function __construct(HiddenString $keyMaterial, ?HiddenString $pk = null) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial, + ?HiddenString $pk = null + ) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey( sprintf( diff --git a/src/Cookie.php b/src/Cookie.php index c905f151..beb493b0 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -86,8 +86,10 @@ public function __debugInfo() * @throws SodiumException * @throws TypeError */ - public function fetch(string $name) - { + public function fetch( + #[\SensitiveParameter] + string $name + ) { if (!isset($_COOKIE[$name])) { return null; } @@ -165,7 +167,9 @@ protected static function getConfig(string $stored): SymmetricConfig * @psalm-suppress MixedArgument */ public function store( + #[\SensitiveParameter] string $name, + #[\SensitiveParameter] $value, int $expire = 0, string $path = '/', diff --git a/src/EncryptionKeyPair.php b/src/EncryptionKeyPair.php index 4b675d56..182c855a 100644 --- a/src/EncryptionKeyPair.php +++ b/src/EncryptionKeyPair.php @@ -131,8 +131,10 @@ public function __construct(Key ...$keys) * @throws InvalidKey * @throws \TypeError */ - protected function setupKeyPair(EncryptionSecretKey $secret): void - { + protected function setupKeyPair( + #[\SensitiveParameter] + EncryptionSecretKey $secret + ): void { $this->secretKey = $secret; $this->publicKey = $this->secretKey->derivePublicKey(); } diff --git a/src/Password.php b/src/Password.php index 8acd6d5f..ba8dda97 100644 --- a/src/Password.php +++ b/src/Password.php @@ -64,9 +64,12 @@ final class Password * @throws TypeError */ public static function hash( + #[\SensitiveParameter] HiddenString $password, + #[\SensitiveParameter] EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE, + #[\SensitiveParameter] string $additionalData = '' ): string { $kdfLimits = KeyFactory::getSecurityLevels($level); @@ -105,9 +108,12 @@ public static function hash( * @throws TypeError */ public static function needsRehash( + #[\SensitiveParameter] string $stored, + #[\SensitiveParameter] EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE, + #[\SensitiveParameter] string $additionalData = '' ): bool { $config = self::getConfig($stored); @@ -203,9 +209,13 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws TypeError */ public static function verify( + #[\SensitiveParameter] HiddenString $password, + #[\SensitiveParameter] string $stored, + #[\SensitiveParameter] EncryptionKey $secretKey, + #[\SensitiveParameter] string $additionalData = '' ): bool { $config = self::getConfig($stored); diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index d36386d5..721b9e9d 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -157,8 +157,10 @@ public function getEncryptionKeyPair(): EncryptionKeyPair * @throws InvalidKey * @throws SodiumException */ - protected function setupKeyPair(SignatureSecretKey $secret): void - { + protected function setupKeyPair( + #[\SensitiveParameter] + SignatureSecretKey $secret + ): void { $this->secretKey = $secret; $this->publicKey = $this->secretKey->derivePublicKey(); } diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index 3ef8f517..a53a69b6 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -27,8 +27,10 @@ final class AuthenticationKey extends SecretKey * @throws InvalidKey * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial + ) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey( sprintf( diff --git a/src/Symmetric/EncryptionKey.php b/src/Symmetric/EncryptionKey.php index 24439803..ea8fee2d 100644 --- a/src/Symmetric/EncryptionKey.php +++ b/src/Symmetric/EncryptionKey.php @@ -25,8 +25,10 @@ final class EncryptionKey extends SecretKey * @throws InvalidKey * @throws TypeError */ - public function __construct(HiddenString $keyMaterial) - { + public function __construct( + #[\SensitiveParameter] + HiddenString $keyMaterial + ) { if (Binary::safeStrlen($keyMaterial->getString()) !== SODIUM_CRYPTO_STREAM_KEYBYTES) { throw new InvalidKey( sprintf( From d34609d79505f6e9314c1ecea354ffed5ed29058 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 8 May 2024 08:58:43 -0400 Subject: [PATCH 086/134] Update CHANGELOG, CI config --- .github/workflows/ci.yml | 2 +- .github/workflows/psalm.yml | 2 +- CHANGELOG.md | 6 ++++++ psalm.xml | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ca94603..7eba2b9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: coverage: none - name: Install Composer dependencies - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" - name: PHPUnit tests run: vendor/bin/phpunit \ No newline at end of file diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index f5bd5c58..489eb959 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -26,7 +26,7 @@ jobs: coverage: none - name: Install Composer dependencies - uses: "ramsey/composer-install@v2" + uses: "ramsey/composer-install@v3" with: composer-options: --no-dev diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a7cc40..1a3daf26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 5.1.2 (2024-05-08) + +* Use `#[SensitiveParameter]` annotation on some inputs + * This is defense in depth; we already wrapped most in `HiddenString` +* Updated dependencies + ## Version 5.1.1 (2024-04-19) * Support both sodium_compat v1 and v2. diff --git a/psalm.xml b/psalm.xml index 5d159e6a..132ad1c5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,6 +11,7 @@ + From 365e6e7f79e9a6289bba27b02c1d1908d0a48c64 Mon Sep 17 00:00:00 2001 From: Adam Bramley Date: Thu, 23 Jan 2025 13:27:27 +1100 Subject: [PATCH 087/134] Fix PHP 8.4 deprecations --- src/File.php | 4 ++-- src/Stream/ReadOnlyFile.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index 89d4e365..bbb5c03c 100644 --- a/src/File.php +++ b/src/File.php @@ -103,7 +103,7 @@ private function __construct() */ public static function checksum( string|ReadonlyFile $filePath, - Key $key = null, + ?Key $key = null, bool|string $encoding = Halite::ENCODE_BASE64URLSAFE ): string { if ($filePath instanceof ReadOnlyFile) { @@ -593,7 +593,7 @@ public static function verify( */ protected static function checksumData( StreamInterface $fileStream, - Key $key = null, + ?Key $key = null, string|bool $encoding = Halite::ENCODE_BASE64URLSAFE ): string { $config = self::getConfig( diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index a2bac8dc..639e570e 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -78,7 +78,7 @@ class ReadOnlyFile implements StreamInterface * @throws TypeError * @psalm-suppress RedundantConditionGivenDocblockType */ - public function __construct($file, Key $key = null) + public function __construct($file, ?Key $key = null) { if (is_string($file)) { if (!is_readable($file)) { From 07dff26f01b103b98b3c39c0d50f0dadd01815eb Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 23 Jan 2025 16:32:27 -0500 Subject: [PATCH 088/134] Update docs --- doc/Classes/File.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/Classes/File.md b/doc/Classes/File.md index 04b0bd98..2a94da45 100644 --- a/doc/Classes/File.md +++ b/doc/Classes/File.md @@ -6,7 +6,7 @@ ### `checksum()` -> `public static` checksum(`$filepath`, [`Key`](Key.md) `$key = null`, `$raw = false`) : `string` +> `public static` checksum(`$filepath`, [`?Key`](Key.md) `$key = null`, `$raw = false`) : `string` Calculates a BLAKE2b-512 hash of the given file. From ef9c72278377d634cd4e08f8ec30198feb7ad167 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 23 Jan 2025 16:33:58 -0500 Subject: [PATCH 089/134] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3daf26..6d2fbb0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 5.1.3 (2025-01-23) + +* Merged [#184](https://github.com/paragonie/halite/pull/194), which fixes PHP 8.4 deprecations with nullable types. + ## Version 5.1.2 (2024-05-08) * Use `#[SensitiveParameter]` annotation on some inputs From 8d9358c0e5d5e9eb583ce013477cbfb059f53f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 00:18:47 +0100 Subject: [PATCH 090/134] Install PHPStan, level 5 for now HiddenString is in bootstrapFiles because it defines a class alias --- composer.json | 1 + phpstan.neon | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index cae05136..dac0c8fc 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ } }, "require-dev": { + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9", "vimeo/psalm": "^4" }, diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..dee93f66 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +parameters: + paths: + - src + - test + level: 5 + bootstrapFiles: + - src/HiddenString.php From 4ee114ed01b1a7ec69a17bc19430c04eadb8e465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 00:19:37 +0100 Subject: [PATCH 091/134] Add $ before the variable name in `@property` --- src/Config.php | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Config.php b/src/Config.php index f2d90adc..a48862ae 100644 --- a/src/Config.php +++ b/src/Config.php @@ -21,27 +21,27 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * - * @property string|bool ENCODING + * @property string|bool $ENCODING * * AsymmetricCrypto: - * @property string HASH_DOMAIN_SEPARATION - * @property bool HASH_SCALARMULT + * @property string $HASH_DOMAIN_SEPARATION + * @property bool $HASH_SCALARMULT * * SymmetricCrypto: - * @property bool CHECKSUM_PUBKEY - * @property int BUFFER - * @property int HASH_LEN - * @property int SHORTEST_CIPHERTEXT_LENGTH - * @property int NONCE_BYTES - * @property int HKDF_SALT_LEN - * @property string ENC_ALGO - * @property string MAC_ALGO - * @property int MAC_SIZE - * @property int PUBLICKEY_BYTES - * @property bool HKDF_USE_INFO - * @property string HKDF_SBOX - * @property string HKDF_AUTH - * @property bool USE_PAE + * @property bool $CHECKSUM_PUBKEY + * @property int $BUFFER + * @property int $HASH_LEN + * @property int $SHORTEST_CIPHERTEXT_LENGTH + * @property int $NONCE_BYTES + * @property int $HKDF_SALT_LEN + * @property string $ENC_ALGO + * @property string $MAC_ALGO + * @property int $MAC_SIZE + * @property int $PUBLICKEY_BYTES + * @property bool $HKDF_USE_INFO + * @property string $HKDF_SBOX + * @property string $HKDF_AUTH + * @property bool $USE_PAE */ class Config { @@ -79,11 +79,10 @@ public function __get(string $key) * * @param string $key * @param mixed $value - * @return bool + * @return void * @codeCoverageIgnore */ - public function __set(string $key, mixed $value = null) + public function __set(string $key, mixed $value = null): void { - return false; } } From 1dc7546666fd232b9c73eea304acf28f221fbbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 00:32:29 +0100 Subject: [PATCH 092/134] Unreachable statement - code above always terminates --- src/File.php | 1 - test/unit/StreamTest.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/File.php b/src/File.php index bbb5c03c..48cff7d4 100644 --- a/src/File.php +++ b/src/File.php @@ -130,7 +130,6 @@ public static function checksum( $readOnly->close(); } } - throw new InvalidType('Argument 1: Expected a filename'); } /** diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index c433fa72..823c3df4 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -53,14 +53,12 @@ public function testUnreadableFile() $perms = fileperms($filename); if (!is_int($perms) || ($perms & 0777) !== 0 || is_readable($filename)) { $this->markTestSkipped('chmod failed to remove read access, so the test will fail; skipping'); - return; } try { new ReadOnlyFile($filename); if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Windows permissions are weird.'); - return; } $this->fail('File should not be readable'); } catch (CryptoException\FileAccessDenied $ex) { From 7f1a551d406e078d5b65ac0f8d318eb93fbecd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 00:45:55 +0100 Subject: [PATCH 093/134] array is incompatible with native type TYPE when using TYPE ...$var --- src/EncryptionKeyPair.php | 2 +- src/SignatureKeyPair.php | 2 +- src/Structure/MerkleTree.php | 4 ++-- src/Structure/TrimmedMerkleTree.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/EncryptionKeyPair.php b/src/EncryptionKeyPair.php index 182c855a..a4f8f29c 100644 --- a/src/EncryptionKeyPair.php +++ b/src/EncryptionKeyPair.php @@ -40,7 +40,7 @@ final class EncryptionKeyPair extends KeyPair /** * Pass it a secret key, it will automatically generate a public key * - * @param array $keys + * @param Key ...$keys * * @throws InvalidKey * @throws \InvalidArgumentException diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index 721b9e9d..667a1dcc 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -49,7 +49,7 @@ final class SignatureKeyPair extends KeyPair /** * Pass it a secret key, it will automatically generate a public key * - * @param array $keys + * @param Key ...$keys * * @throws CannotPerformOperation * @throws InvalidKey diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 2a59fbb1..3e71fd46 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -53,7 +53,7 @@ class MerkleTree /** * Instantiate a Merkle tree * - * @param array $nodes + * @param Node ...$nodes */ public function __construct(Node ...$nodes) { @@ -84,7 +84,7 @@ public function getRoot(bool $raw = false): string /** * Merkle Trees are immutable. Return a replacement with extra nodes. * - * @param array $nodes + * @param Node ...$nodes * * @return MerkleTree * diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index 8a9a29d5..d52172e0 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -95,7 +95,7 @@ protected function calculateRoot(): string /** * Merkle Trees are immutable. Return a replacement with extra nodes. * - * @param array $nodes + * @param Node ...$nodes * * @return TrimmedMerkleTree * From c1c5d200e4e3bb0ca68b27ed6382c3ef25908a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 00:58:57 +0100 Subject: [PATCH 094/134] Call to function is_string() with string will always evaluate to true. --- src/File.php | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/File.php b/src/File.php index 48cff7d4..daaac1fd 100644 --- a/src/File.php +++ b/src/File.php @@ -118,17 +118,15 @@ public static function checksum( return $checksum; } - if (is_string($filePath)) { - $readOnly = new ReadOnlyFile($filePath); - try { - return self::checksumData( - $readOnly, - $key, - $encoding - ); - } finally { - $readOnly->close(); - } + $readOnly = new ReadOnlyFile($filePath); + try { + return self::checksumData( + $readOnly, + $key, + $encoding + ); + } finally { + $readOnly->close(); } } From 060615e11ebd8821c6f70270ed3014b5a3f8f76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 01:17:15 +0100 Subject: [PATCH 095/134] Document exceptions --- src/SignatureKeyPair.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index 667a1dcc..dd37dadf 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -53,6 +53,7 @@ final class SignatureKeyPair extends KeyPair * * @throws CannotPerformOperation * @throws InvalidKey + * @throws InvalidArgumentException * @throws SodiumException * @throws TypeError */ @@ -153,7 +154,6 @@ public function getEncryptionKeyPair(): EncryptionKeyPair * @param SignatureSecretKey $secret * @return void * - * @throws CannotPerformOperation * @throws InvalidKey * @throws SodiumException */ From 298652fc65a51ba04381aa844ab91a25bde191b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 01:52:08 +0100 Subject: [PATCH 096/134] Proper types --- src/Cookie.php | 2 +- src/Stream/MutableFile.php | 2 +- src/Stream/ReadOnlyFile.php | 2 +- src/Structure/MerkleTree.php | 1 - src/Symmetric/Crypto.php | 6 +----- test/unit/ConfigTest.php | 1 + 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Cookie.php b/src/Cookie.php index beb493b0..1d33403e 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -65,7 +65,7 @@ public function __construct(EncryptionKey $key) * * @return array */ - public function __debugInfo() + public function __debugInfo(): array { return [ 'key' => 'private' diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index d1cb7a55..66b4fc4b 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -217,7 +217,7 @@ public function readBytes(int $num, bool $skipTests = false): string // @codeCoverageIgnoreEnd } $bufSize = min($remaining, self::CHUNK); - /** @var string|bool $read */ + /** @var string|false $read */ $read = fread($this->fp, $bufSize); if (!is_string($read)) { // @codeCoverageIgnoreStart diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 639e570e..33c63992 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -86,7 +86,7 @@ public function __construct($file, ?Key $key = null) 'Could not open file for reading' ); } - /** @var resource|bool $fp */ + /** @var resource|false $fp */ $fp = fopen($file, 'rb'); // @codeCoverageIgnoreStart if (!is_resource($fp)) { diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 3e71fd46..9e58fa05 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -214,7 +214,6 @@ protected function calculateRoot(): string $tmp = []; $j = 0; for ($i = 0; $i < $order; $i += 2) { - /** @var string $prev */ $curr = (string) ($hash[$i] ?? ''); if (empty($hash[$i + 1])) { // @codeCoverageIgnoreStart diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 5d623c00..dcd035b0 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -199,11 +199,7 @@ public static function decryptWithAD( This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ - /** - * @var array $split - * @var string $encKey - * @var string $authKey - */ + /** @var array $split */ $split = Util::splitKeys($secretKey, $salt, $config); $encKey = $split[0]; $authKey = $split[1]; diff --git a/test/unit/ConfigTest.php b/test/unit/ConfigTest.php index 21238c7a..a78caf55 100644 --- a/test/unit/ConfigTest.php +++ b/test/unit/ConfigTest.php @@ -9,6 +9,7 @@ class ConfigTest extends TestCase { public function testConfig() { + /** @var object{abc:12345}&Config $config */ $config = new Config([ 'abc' => 12345 ]); From 918aaa4e19d6a334f9d99d80444c61c5dcb3914b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 01:54:06 +0100 Subject: [PATCH 097/134] Use assertInstanceOf and assertIsString to perform an assertion while keeping PHPStan semi-happy --- test/unit/AsymmetricTest.php | 8 ++++---- test/unit/FileTest.php | 4 ++-- test/unit/KeyPairTest.php | 8 ++++---- test/unit/KeyTest.php | 16 ++++++---------- test/unit/PasswordTest.php | 6 +++--- test/unit/StreamTest.php | 4 +--- test/unit/SymmetricTest.php | 2 +- 7 files changed, 21 insertions(+), 27 deletions(-) diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index 76c8489a..c13a672b 100644 --- a/test/unit/AsymmetricTest.php +++ b/test/unit/AsymmetricTest.php @@ -186,7 +186,7 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } @@ -285,9 +285,9 @@ public function testSealFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidKey $e) { - $this->assertTrue($e instanceof CryptoException\InvalidKey); + $this->assertInstanceOf(CryptoException\InvalidKey::class, $e); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } @@ -394,7 +394,7 @@ public function testSignEncryptFail() ); $this->fail('Invalid signature was accepted.'); } catch (CryptoException\InvalidSignature $ex) { - $this->assertTrue(true); + $this->assertInstanceOf(CryptoException\InvalidSignature::class, $ex); } // http://time.com/4261796/tim-cook-transcript/ diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index adf32ee8..1d1772b8 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -284,7 +284,7 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); unlink(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png'); } @@ -632,7 +632,7 @@ public function testSealFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); unlink(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); unlink(__DIR__.'/tmp/paragon_avatar.open_fail.png'); } diff --git a/test/unit/KeyPairTest.php b/test/unit/KeyPairTest.php index 31da70d3..d625cac1 100644 --- a/test/unit/KeyPairTest.php +++ b/test/unit/KeyPairTest.php @@ -37,8 +37,8 @@ public function testDeriveSigningKey() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -83,8 +83,8 @@ public function testDeriveSigningKeyOldArgon2i() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; diff --git a/test/unit/KeyTest.php b/test/unit/KeyTest.php index c7275eae..8c24a3aa 100644 --- a/test/unit/KeyTest.php +++ b/test/unit/KeyTest.php @@ -118,8 +118,8 @@ public function testDeriveSigningKey() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -159,8 +159,8 @@ public function testDeriveSigningKeyOldArgon2i() $sign_secret = $keypair->getSecretKey(); $sign_public = $keypair->getPublicKey(); - $this->assertTrue($sign_secret instanceof SignatureSecretKey); - $this->assertTrue($sign_public instanceof SignaturePublicKey); + $this->assertInstanceOf(SignatureSecretKey::class, $sign_secret); + $this->assertInstanceOf(SignaturePublicKey::class, $sign_public); // Can this be used? $message = 'This is a test message'; @@ -354,9 +354,7 @@ public function testEncKeyStorage() ); $load_public = KeyFactory::loadEncryptionPublicKey($file_public); - $this->assertTrue( - $load_public instanceof EncryptionPublicKey - ); + $this->assertInstanceOf(EncryptionPublicKey::class, $load_public); $this->assertTrue( \hash_equals($enc_public->getRawKeyMaterial(), $load_public->getRawKeyMaterial()) ); @@ -403,9 +401,7 @@ public function testSignKeyStorage() ); $load_public = KeyFactory::loadSignaturePublicKey($file_public); - $this->assertTrue( - $load_public instanceof SignaturePublicKey - ); + $this->assertInstanceOf(SignaturePublicKey::class, $load_public); $this->assertTrue( \hash_equals($sign_public->getRawKeyMaterial(), $load_public->getRawKeyMaterial()) ); diff --git a/test/unit/PasswordTest.php b/test/unit/PasswordTest.php index 8962d0dc..c9edc2f1 100644 --- a/test/unit/PasswordTest.php +++ b/test/unit/PasswordTest.php @@ -28,7 +28,7 @@ public function testEncrypt() $key = new EncryptionKey(new HiddenString(str_repeat('A', 32))); $hash = Password::hash(new HiddenString('test password'), $key); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertTrue( Password::verify( @@ -71,7 +71,7 @@ public function testEncryptWithAd() KeyFactory::INTERACTIVE, $aad ); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertTrue( Password::verify( @@ -145,7 +145,7 @@ public function testKeyLevels() $passwd = new HiddenString('test password'); foreach ([KeyFactory::INTERACTIVE, KeyFactory::MODERATE, KeyFactory::SENSITIVE] as $level) { $hash = Password::hash($passwd, $key, $level, $aad); - $this->assertTrue(is_string($hash)); + $this->assertIsString($hash); $this->assertFalse(Password::needsRehash($hash, $key, $level, $aad)); $this->assertTrue(Password::verify($passwd, $hash, $key, $aad)); } diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index 823c3df4..ec662279 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -182,9 +182,7 @@ public function testFileRead() $fStream->readBytes(65537); $this->fail('File was mutated after being read'); } catch (CryptoException\FileModified $ex) { - $this->assertTrue( - $ex instanceof CryptoException\FileModified - ); + $this->assertInstanceOf(CryptoException\FileModified::class, $ex); } $fStream = new ReadOnlyFile($filename); diff --git a/test/unit/SymmetricTest.php b/test/unit/SymmetricTest.php index 2939b2b4..48eb0ef2 100644 --- a/test/unit/SymmetricTest.php +++ b/test/unit/SymmetricTest.php @@ -229,7 +229,7 @@ public function testEncryptFail() 'This should have thrown an InvalidMessage exception!' ); } catch (CryptoException\InvalidMessage $e) { - $this->assertTrue($e instanceof CryptoException\InvalidMessage); + $this->assertInstanceOf(CryptoException\InvalidMessage::class, $e); } } From 0aac7a1fba587353fee6ccf212f58041e766afab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 02:22:45 +0100 Subject: [PATCH 098/134] $config instanceof Config already checked --- test/unit/SymmetricTest.php | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/test/unit/SymmetricTest.php b/test/unit/SymmetricTest.php index 48eb0ef2..b110875f 100644 --- a/test/unit/SymmetricTest.php +++ b/test/unit/SymmetricTest.php @@ -262,21 +262,17 @@ public function testUnpack() $this->assertSame(Binary::safeStrlen($unpacked[0]), Halite::VERSION_TAG_LEN); $this->assertTrue($unpacked[1] instanceof Config); $config = $unpacked[1]; - if ($config instanceof Config) { - $this->assertSame(Binary::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); - $this->assertSame(Binary::safeStrlen($unpacked[3]), SODIUM_CRYPTO_STREAM_NONCEBYTES); - $this->assertSame( - Binary::safeStrlen($unpacked[4]), - Binary::safeStrlen($message) - ( - Halite::VERSION_TAG_LEN + - $config->HKDF_SALT_LEN + - SODIUM_CRYPTO_STREAM_NONCEBYTES + - $config->MAC_SIZE - ) - ); - $this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE); - } else { - $this->fail('Cannot continue'); - } + $this->assertSame(Binary::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); + $this->assertSame(Binary::safeStrlen($unpacked[3]), SODIUM_CRYPTO_STREAM_NONCEBYTES); + $this->assertSame( + Binary::safeStrlen($unpacked[4]), + Binary::safeStrlen($message) - ( + Halite::VERSION_TAG_LEN + + $config->HKDF_SALT_LEN + + SODIUM_CRYPTO_STREAM_NONCEBYTES + + $config->MAC_SIZE + ) + ); + $this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE); } } From 5f045c0f1beaf9c3f86a91588ee5d112e3b90370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 02:36:21 +0100 Subject: [PATCH 099/134] Add PHPStan baseline ignoring "defensive development" errors --- phpstan-baseline.neon | 115 ++++++++++++++++++++++++++++++++++++++++++ phpstan.neon | 2 + 2 files changed, 117 insertions(+) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..93e35770 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,115 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function is_string\(\) with non\-empty\-string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 4 + path: src/File.php + + - + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: src/File.php + + - + message: '#^Comparison operation "\<" between 10 and 10 is always false\.$#' + identifier: smaller.alwaysFalse + count: 1 + path: src/Halite.php + + - + message: '#^Property ParagonIE\\Halite\\Key\:\:\$keyMaterial \(string\) does not accept null\.$#' + identifier: assign.propertyType + count: 1 + path: src/Key.php + + - + message: '#^Comparison operation "\<\=" between int\<1, max\> and 0 is always false\.$#' + identifier: smallerOrEqual.alwaysFalse + count: 1 + path: src/Stream/MutableFile.php + + - + message: '#^Comparison operation "\<\=" between int\<1, max\> and 0 is always false\.$#' + identifier: smallerOrEqual.alwaysFalse + count: 1 + path: src/Stream/ReadOnlyFile.php + + - + message: '#^Offset 1\|int\<3, max\> on array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: src/Structure/MerkleTree.php + + - + message: '#^Parameter &\$var @param\-out type of method ParagonIE\\Halite\\Util\:\:memzero\(\) expects null, int given\.$#' + identifier: paramOut.type + count: 1 + path: src/Util.php + + - + message: '#^Comparison operation "\<" between 10 and 7 is always false\.$#' + identifier: smaller.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Comparison operation "\<" between 3 and 5 is always true\.$#' + identifier: smaller.alwaysTrue + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Loose comparison using \=\= between 10 and 7 will always evaluate to false\.$#' + identifier: equal.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse + count: 2 + path: test/unit/AsymmetricTest.php + + - + message: '#^Access to an undefined property object\{abc\: int\}&ParagonIE\\Halite\\Config\:\:\$missing\.$#' + identifier: property.notFound + count: 1 + path: test/unit/ConfigTest.php + + - + message: '#^Dead catch \- ParagonIE\\Halite\\Alerts\\ConfigDirectiveNotFound is never thrown in the try block\.$#' + identifier: catch.neverThrown + count: 1 + path: test/unit/ConfigTest.php + + - + message: '#^Parameter \#1 \$key of static method ParagonIE\\Halite\\KeyFactory\:\:export\(\) expects ParagonIE\\Halite\\Key\|ParagonIE\\Halite\\KeyPair, stdClass given\.$#' + identifier: argument.type + count: 1 + path: test/unit/KeyTest.php + + - + message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#' + identifier: method.alreadyNarrowedType + count: 3 + path: test/unit/PasswordTest.php + + - + message: '#^Parameter \#1 \$file of class ParagonIE\\Halite\\Stream\\MutableFile constructor expects resource\|string, int given\.$#' + identifier: argument.type + count: 1 + path: test/unit/StreamTest.php + + - + message: '#^Parameter \#1 \$file of class ParagonIE\\Halite\\Stream\\ReadOnlyFile constructor expects resource\|string, int given\.$#' + identifier: argument.type + count: 1 + path: test/unit/StreamTest.php diff --git a/phpstan.neon b/phpstan.neon index dee93f66..0e73c775 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,3 +5,5 @@ parameters: level: 5 bootstrapFiles: - src/HiddenString.php +includes: + - phpstan-baseline.neon From 46bfedde8d224ac3c04c03ca59a1238448a8bceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 02:49:11 +0100 Subject: [PATCH 100/134] Allow the developer to use local phpstan config --- .gitignore | 1 + phpstan.neon => phpstan.dist.neon | 0 2 files changed, 1 insertion(+) rename phpstan.neon => phpstan.dist.neon (100%) diff --git a/.gitignore b/.gitignore index a82c535d..321fa666 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /composer.phar /.idea/ /.phpunit.result.cache +/phpstan.neon diff --git a/phpstan.neon b/phpstan.dist.neon similarity index 100% rename from phpstan.neon rename to phpstan.dist.neon From ac1f3de682d90148fd39495a1c556f8fbf00c45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 24 Jan 2025 02:51:52 +0100 Subject: [PATCH 101/134] Can run PHPStan analysis with `composer test` and in CI In CI on all supported versions --- .github/workflows/ci.yml | 5 ++++- composer.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7eba2b9e..fbda4e64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,4 +31,7 @@ jobs: uses: "ramsey/composer-install@v3" - name: PHPUnit tests - run: vendor/bin/phpunit \ No newline at end of file + run: vendor/bin/phpunit + + - name: PHPStan analysis + run: vendor/bin/phpstan diff --git a/composer.json b/composer.json index dac0c8fc..092d93ea 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "vimeo/psalm": "^4" }, "scripts": { - "test": "phpunit && psalm" + "test": "phpunit && phpstan && psalm" }, "support": { "docs": "https://github.com/paragonie/halite/tree/master/doc" From ccbaf5b1f766db2ddd4cd9caba8d548834c981a4 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sat, 1 Feb 2025 10:49:26 +0000 Subject: [PATCH 102/134] Replace all `http://` links with the `https://` URL they redirect to --- LICENSE | 2 +- src/Alerts/CannotCloneKey.php | 2 +- src/Alerts/CannotPerformOperation.php | 2 +- src/Alerts/CannotSerializeKey.php | 2 +- src/Alerts/ConfigDirectiveNotFound.php | 2 +- src/Alerts/FileAccessDenied.php | 2 +- src/Alerts/FileError.php | 2 +- src/Alerts/FileModified.php | 2 +- src/Alerts/HaliteAlert.php | 2 +- src/Alerts/HaliteAlertInterface.php | 2 +- src/Alerts/InvalidDigestLength.php | 2 +- src/Alerts/InvalidFlags.php | 2 +- src/Alerts/InvalidKey.php | 2 +- src/Alerts/InvalidMessage.php | 2 +- src/Alerts/InvalidSalt.php | 2 +- src/Alerts/InvalidSignature.php | 2 +- src/Alerts/InvalidType.php | 2 +- src/Asymmetric/Config.php | 4 ++-- src/Asymmetric/Crypto.php | 4 ++-- src/Asymmetric/EncryptionPublicKey.php | 2 +- src/Asymmetric/EncryptionSecretKey.php | 2 +- src/Asymmetric/PublicKey.php | 2 +- src/Asymmetric/SecretKey.php | 2 +- src/Asymmetric/SignaturePublicKey.php | 2 +- src/Asymmetric/SignatureSecretKey.php | 2 +- src/Config.php | 4 ++-- src/Contract/StreamInterface.php | 4 ++-- src/Cookie.php | 4 ++-- src/EncryptionKeyPair.php | 4 ++-- src/File.php | 4 ++-- src/Halite.php | 4 ++-- src/Key.php | 4 ++-- src/KeyFactory.php | 4 ++-- src/KeyPair.php | 4 ++-- src/Password.php | 4 ++-- src/SignatureKeyPair.php | 4 ++-- src/Stream/MutableFile.php | 4 ++-- src/Stream/ReadOnlyFile.php | 4 ++-- src/Structure/MerkleTree.php | 4 ++-- src/Structure/Node.php | 4 ++-- src/Structure/TrimmedMerkleTree.php | 4 ++-- src/Symmetric/AuthenticationKey.php | 2 +- src/Symmetric/Config.php | 4 ++-- src/Symmetric/Crypto.php | 4 ++-- src/Symmetric/EncryptionKey.php | 2 +- src/Symmetric/SecretKey.php | 2 +- src/Util.php | 6 +++--- test/unit/AsymmetricTest.php | 6 +++--- test/unit/UtilTest.php | 2 +- 49 files changed, 73 insertions(+), 73 deletions(-) diff --git a/LICENSE b/LICENSE index a612ad98..4f573b70 100644 --- a/LICENSE +++ b/LICENSE @@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. + file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE diff --git a/src/Alerts/CannotCloneKey.php b/src/Alerts/CannotCloneKey.php index a76d08db..3ecadf68 100644 --- a/src/Alerts/CannotCloneKey.php +++ b/src/Alerts/CannotCloneKey.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class CannotCloneKey extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/CannotPerformOperation.php b/src/Alerts/CannotPerformOperation.php index 0685f285..3d53a1ca 100644 --- a/src/Alerts/CannotPerformOperation.php +++ b/src/Alerts/CannotPerformOperation.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class CannotPerformOperation extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/CannotSerializeKey.php b/src/Alerts/CannotSerializeKey.php index 116d091c..3b088cfb 100644 --- a/src/Alerts/CannotSerializeKey.php +++ b/src/Alerts/CannotSerializeKey.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class CannotSerializeKey extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/ConfigDirectiveNotFound.php b/src/Alerts/ConfigDirectiveNotFound.php index d8711b7e..11acfbc6 100644 --- a/src/Alerts/ConfigDirectiveNotFound.php +++ b/src/Alerts/ConfigDirectiveNotFound.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class ConfigDirectiveNotFound extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/FileAccessDenied.php b/src/Alerts/FileAccessDenied.php index 5ab7ec9d..6c78cfb7 100644 --- a/src/Alerts/FileAccessDenied.php +++ b/src/Alerts/FileAccessDenied.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class FileAccessDenied extends FileError implements HaliteAlertInterface { diff --git a/src/Alerts/FileError.php b/src/Alerts/FileError.php index 2679a70d..b99138e9 100644 --- a/src/Alerts/FileError.php +++ b/src/Alerts/FileError.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class FileError extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/FileModified.php b/src/Alerts/FileModified.php index 7f7cf325..715d7511 100644 --- a/src/Alerts/FileModified.php +++ b/src/Alerts/FileModified.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class FileModified extends FileError implements HaliteAlertInterface { diff --git a/src/Alerts/HaliteAlert.php b/src/Alerts/HaliteAlert.php index 6fdce62f..02297d6e 100644 --- a/src/Alerts/HaliteAlert.php +++ b/src/Alerts/HaliteAlert.php @@ -11,7 +11,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class HaliteAlert extends Exception implements HaliteAlertInterface { diff --git a/src/Alerts/HaliteAlertInterface.php b/src/Alerts/HaliteAlertInterface.php index 6e86b599..23ee19cd 100644 --- a/src/Alerts/HaliteAlertInterface.php +++ b/src/Alerts/HaliteAlertInterface.php @@ -11,7 +11,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ interface HaliteAlertInterface extends Throwable { diff --git a/src/Alerts/InvalidDigestLength.php b/src/Alerts/InvalidDigestLength.php index d5674372..2da6e396 100644 --- a/src/Alerts/InvalidDigestLength.php +++ b/src/Alerts/InvalidDigestLength.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidDigestLength extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidFlags.php b/src/Alerts/InvalidFlags.php index ff6f68e7..81c8db80 100644 --- a/src/Alerts/InvalidFlags.php +++ b/src/Alerts/InvalidFlags.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidFlags extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidKey.php b/src/Alerts/InvalidKey.php index 9306dc98..3fb08d3f 100644 --- a/src/Alerts/InvalidKey.php +++ b/src/Alerts/InvalidKey.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidKey extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidMessage.php b/src/Alerts/InvalidMessage.php index c75044b7..ebc8a83d 100644 --- a/src/Alerts/InvalidMessage.php +++ b/src/Alerts/InvalidMessage.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidMessage extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidSalt.php b/src/Alerts/InvalidSalt.php index 8cb0cd6c..bb9f0a99 100644 --- a/src/Alerts/InvalidSalt.php +++ b/src/Alerts/InvalidSalt.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidSalt extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidSignature.php b/src/Alerts/InvalidSignature.php index c3bc092f..682c5806 100644 --- a/src/Alerts/InvalidSignature.php +++ b/src/Alerts/InvalidSignature.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidSignature extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Alerts/InvalidType.php b/src/Alerts/InvalidType.php index 01b3f681..5d597abf 100644 --- a/src/Alerts/InvalidType.php +++ b/src/Alerts/InvalidType.php @@ -9,7 +9,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class InvalidType extends HaliteAlert implements HaliteAlertInterface { diff --git a/src/Asymmetric/Config.php b/src/Asymmetric/Config.php index 29563354..02ec5cb7 100644 --- a/src/Asymmetric/Config.php +++ b/src/Asymmetric/Config.php @@ -16,13 +16,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Asymmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Config extends BaseConfig { diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 42c8baa3..7b3fb1f3 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -44,13 +44,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Asymmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Crypto { diff --git a/src/Asymmetric/EncryptionPublicKey.php b/src/Asymmetric/EncryptionPublicKey.php index ce44766c..1fde7c9c 100644 --- a/src/Asymmetric/EncryptionPublicKey.php +++ b/src/Asymmetric/EncryptionPublicKey.php @@ -14,7 +14,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionPublicKey extends PublicKey { diff --git a/src/Asymmetric/EncryptionSecretKey.php b/src/Asymmetric/EncryptionSecretKey.php index c361ab15..233dfee1 100644 --- a/src/Asymmetric/EncryptionSecretKey.php +++ b/src/Asymmetric/EncryptionSecretKey.php @@ -18,7 +18,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionSecretKey extends SecretKey { diff --git a/src/Asymmetric/PublicKey.php b/src/Asymmetric/PublicKey.php index 0d9ee986..3987c4f4 100644 --- a/src/Asymmetric/PublicKey.php +++ b/src/Asymmetric/PublicKey.php @@ -12,7 +12,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class PublicKey extends Key { diff --git a/src/Asymmetric/SecretKey.php b/src/Asymmetric/SecretKey.php index ee8d7f62..914941b9 100644 --- a/src/Asymmetric/SecretKey.php +++ b/src/Asymmetric/SecretKey.php @@ -12,7 +12,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class SecretKey extends Key { diff --git a/src/Asymmetric/SignaturePublicKey.php b/src/Asymmetric/SignaturePublicKey.php index db782937..6d9e4b06 100644 --- a/src/Asymmetric/SignaturePublicKey.php +++ b/src/Asymmetric/SignaturePublicKey.php @@ -18,7 +18,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignaturePublicKey extends PublicKey { diff --git a/src/Asymmetric/SignatureSecretKey.php b/src/Asymmetric/SignatureSecretKey.php index 355db43d..f92d4e9b 100644 --- a/src/Asymmetric/SignatureSecretKey.php +++ b/src/Asymmetric/SignatureSecretKey.php @@ -21,7 +21,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignatureSecretKey extends SecretKey { diff --git a/src/Config.php b/src/Config.php index a48862ae..b57f02a8 100644 --- a/src/Config.php +++ b/src/Config.php @@ -13,13 +13,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. * * @property string|bool $ENCODING * diff --git a/src/Contract/StreamInterface.php b/src/Contract/StreamInterface.php index 15f39f77..be60a351 100644 --- a/src/Contract/StreamInterface.php +++ b/src/Contract/StreamInterface.php @@ -15,13 +15,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Contract * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ interface StreamInterface { diff --git a/src/Cookie.php b/src/Cookie.php index 1d33403e..7dd03717 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -37,13 +37,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. * * @codeCoverageIgnore */ diff --git a/src/EncryptionKeyPair.php b/src/EncryptionKeyPair.php index a4f8f29c..2db3f95b 100644 --- a/src/EncryptionKeyPair.php +++ b/src/EncryptionKeyPair.php @@ -17,13 +17,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionKeyPair extends KeyPair { diff --git a/src/File.php b/src/File.php index daaac1fd..f0dad8c7 100644 --- a/src/File.php +++ b/src/File.php @@ -60,13 +60,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class File { diff --git a/src/Halite.php b/src/Halite.php index f2b8e3d1..a66c180d 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -33,13 +33,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Halite { diff --git a/src/Key.php b/src/Key.php index 4900bc99..c29b86ba 100644 --- a/src/Key.php +++ b/src/Key.php @@ -17,13 +17,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class Key { diff --git a/src/KeyFactory.php b/src/KeyFactory.php index e32728bf..93f29c43 100644 --- a/src/KeyFactory.php +++ b/src/KeyFactory.php @@ -64,13 +64,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class KeyFactory { diff --git a/src/KeyPair.php b/src/KeyPair.php index 72bdd0ca..4fab3308 100644 --- a/src/KeyPair.php +++ b/src/KeyPair.php @@ -15,13 +15,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class KeyPair { diff --git a/src/Password.php b/src/Password.php index ba8dda97..eef03d11 100644 --- a/src/Password.php +++ b/src/Password.php @@ -36,13 +36,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Password { diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index dd37dadf..b51d5ae7 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -26,13 +26,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class SignatureKeyPair extends KeyPair { diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index 66b4fc4b..1566b2c5 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -38,13 +38,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Stream * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class MutableFile implements StreamInterface { diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 33c63992..290723f2 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -40,13 +40,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Stream * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class ReadOnlyFile implements StreamInterface { diff --git a/src/Structure/MerkleTree.php b/src/Structure/MerkleTree.php index 9e58fa05..ec535d1a 100644 --- a/src/Structure/MerkleTree.php +++ b/src/Structure/MerkleTree.php @@ -27,13 +27,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class MerkleTree { diff --git a/src/Structure/Node.php b/src/Structure/Node.php index 8bc083df..0f048a7e 100644 --- a/src/Structure/Node.php +++ b/src/Structure/Node.php @@ -14,13 +14,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class Node { diff --git a/src/Structure/TrimmedMerkleTree.php b/src/Structure/TrimmedMerkleTree.php index d52172e0..2f213b5c 100644 --- a/src/Structure/TrimmedMerkleTree.php +++ b/src/Structure/TrimmedMerkleTree.php @@ -23,13 +23,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Structure * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class TrimmedMerkleTree extends MerkleTree { diff --git a/src/Symmetric/AuthenticationKey.php b/src/Symmetric/AuthenticationKey.php index a53a69b6..1c065a0e 100644 --- a/src/Symmetric/AuthenticationKey.php +++ b/src/Symmetric/AuthenticationKey.php @@ -15,7 +15,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class AuthenticationKey extends SecretKey { diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index d7dec55e..3a1f7bef 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -20,13 +20,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Symmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Config extends BaseConfig { diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index dcd035b0..04fb97a9 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -44,13 +44,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite\Symmetric * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Crypto { diff --git a/src/Symmetric/EncryptionKey.php b/src/Symmetric/EncryptionKey.php index ea8fee2d..ce48f4bc 100644 --- a/src/Symmetric/EncryptionKey.php +++ b/src/Symmetric/EncryptionKey.php @@ -15,7 +15,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class EncryptionKey extends SecretKey { diff --git a/src/Symmetric/SecretKey.php b/src/Symmetric/SecretKey.php index 42588e72..22012839 100644 --- a/src/Symmetric/SecretKey.php +++ b/src/Symmetric/SecretKey.php @@ -10,7 +10,7 @@ * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ class SecretKey extends Key { diff --git a/src/Util.php b/src/Util.php index 4094028b..076cf646 100644 --- a/src/Util.php +++ b/src/Util.php @@ -41,13 +41,13 @@ * This library makes heavy use of return-type declarations, * which are a PHP 7 only feature. Read more about them here: * - * @ref http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration + * @ref https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration * * @package ParagonIE\Halite * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * file, You can obtain one at https://www.mozilla.org/en-US/MPL/2.0/. */ final class Util { @@ -124,7 +124,7 @@ public static function raw_hash( /** * Use a derivative of HKDF to derive multiple keys from one. - * http://tools.ietf.org/html/rfc5869 + * https://datatracker.ietf.org/doc/html/rfc5869 * * This is a variant from hash_hkdf() and instead uses BLAKE2b provided by * libsodium. diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index c13a672b..0a1e3085 100644 --- a/test/unit/AsymmetricTest.php +++ b/test/unit/AsymmetricTest.php @@ -331,7 +331,7 @@ public function testSignEncrypt() $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $message = new HiddenString( 'When I think of civil liberties I think of the founding principles of the country. ' . 'The freedoms that are in the First Amendment. But also the fundamental right to privacy.' @@ -374,7 +374,7 @@ public function testSignEncryptFail() $alice = KeyFactory::generateSignatureKeyPair(); $bob = KeyFactory::generateEncryptionKeyPair(); - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $junk = new HiddenString( // Instead of a signature, it's 64 random bytes random_bytes(SODIUM_CRYPTO_SIGN_BYTES) . @@ -397,7 +397,7 @@ public function testSignEncryptFail() $this->assertInstanceOf(CryptoException\InvalidSignature::class, $ex); } - // http://time.com/4261796/tim-cook-transcript/ + // https://time.com/4261796/tim-cook-transcript/ $message = new HiddenString( 'When I think of civil liberties I think of the founding principles of the country. ' . 'The freedoms that are in the First Amendment. But also the fundamental right to privacy.' diff --git a/test/unit/UtilTest.php b/test/unit/UtilTest.php index 685fe4f0..33479e96 100644 --- a/test/unit/UtilTest.php +++ b/test/unit/UtilTest.php @@ -13,7 +13,7 @@ * @category HaliteTest * @package Halite * @author Stefanie Schmidt - * @license http://opensource.org/licenses/GPL-3.0 GPL 3 + * @license https://opensource.org/license/GPL-3.0 GPL 3 * @link https://paragonie.com/project/halite */ final class UtilTest extends TestCase From d150e828e8c1366df9214d3bb9703a0a08e52f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 4 Mar 2025 05:41:49 +0100 Subject: [PATCH 103/134] Require the latest Psalm 6.8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 092d93ea..642d846d 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "require-dev": { "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9", - "vimeo/psalm": "^4" + "vimeo/psalm": "^6.8" }, "scripts": { "test": "phpunit && phpstan && psalm" From 76f3820ce6beef3317756aec9d8dfe74f3da0225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 4 Mar 2025 05:43:15 +0100 Subject: [PATCH 104/134] Ignore errors for now The ones downgraded to `errorLevel="info"` could and possibly should be fixed one day, but today is not the day, I'd like a minimal change for now. --- psalm.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/psalm.xml b/psalm.xml index 132ad1c5..7fed6175 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,10 +10,24 @@ + + + + + + + + + + + + + + From f187c7cb246cf308010c50d96c6ac094a554b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 4 Mar 2025 05:43:47 +0100 Subject: [PATCH 105/134] Remove redundant docblocks --- src/Cookie.php | 1 - src/Password.php | 2 -- src/Stream/MutableFile.php | 1 - src/Stream/ReadOnlyFile.php | 1 - 4 files changed, 5 deletions(-) diff --git a/src/Cookie.php b/src/Cookie.php index 7dd03717..01a776ee 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -100,7 +100,6 @@ public function fetch( throw new InvalidType('Cookie value is not a string'); } $config = self::getConfig($stored); - /** @var string|bool $encoding */ $encoding = $config->ENCODING; $decrypted = Crypto::decrypt( $stored, diff --git a/src/Password.php b/src/Password.php index eef03d11..0989f0a0 100644 --- a/src/Password.php +++ b/src/Password.php @@ -120,7 +120,6 @@ public static function needsRehash( if (Binary::safeStrlen($stored) < ((int) $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3)) { throw new InvalidMessage('Encrypted password hash is too short.'); } - /** @var string|bool $encoding */ $encoding = $config->ENCODING; // First let's decrypt the hash @@ -225,7 +224,6 @@ public static function verify( 'Encrypted password hash is too short.' ); } - /** @var string|bool $encoding */ $encoding = $config->ENCODING; // First let's decrypt the hash diff --git a/src/Stream/MutableFile.php b/src/Stream/MutableFile.php index 1566b2c5..60fbd542 100644 --- a/src/Stream/MutableFile.php +++ b/src/Stream/MutableFile.php @@ -217,7 +217,6 @@ public function readBytes(int $num, bool $skipTests = false): string // @codeCoverageIgnoreEnd } $bufSize = min($remaining, self::CHUNK); - /** @var string|false $read */ $read = fread($this->fp, $bufSize); if (!is_string($read)) { // @codeCoverageIgnoreStart diff --git a/src/Stream/ReadOnlyFile.php b/src/Stream/ReadOnlyFile.php index 290723f2..c30f1955 100644 --- a/src/Stream/ReadOnlyFile.php +++ b/src/Stream/ReadOnlyFile.php @@ -86,7 +86,6 @@ public function __construct($file, ?Key $key = null) 'Could not open file for reading' ); } - /** @var resource|false $fp */ $fp = fopen($file, 'rb'); // @codeCoverageIgnoreStart if (!is_resource($fp)) { From cb289fe95417f19c5fa4b91f94065ea44684b7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 4 Mar 2025 05:46:53 +0100 Subject: [PATCH 106/134] Run Psalm on all supported PHPs --- .github/workflows/psalm.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 489eb959..7935458f 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,7 +13,8 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.3'] + php-versions: ['8.1', '8.2', '8.3', '8.4'] + steps: - name: Checkout uses: actions/checkout@v4 @@ -22,13 +23,10 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - tools: psalm:4 coverage: none - name: Install Composer dependencies uses: "ramsey/composer-install@v3" - with: - composer-options: --no-dev - - name: Static Analysis - run: psalm + - name: Psalm static analysis + run: vendor/bin/psalm From 490f184b7a12c0859ef93aa9be2634aab6868f2e Mon Sep 17 00:00:00 2001 From: junaid farooq Date: Wed, 20 Aug 2025 15:48:10 +0530 Subject: [PATCH 107/134] feat: Remove access modifier `final` from private methods as such methods are never overridden by other classes --- src/Asymmetric/Crypto.php | 2 +- src/Halite.php | 2 +- src/Symmetric/Crypto.php | 2 +- src/Util.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 7b3fb1f3..2740ff0e 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -60,7 +60,7 @@ final class Crypto * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { throw new Error('Do not instantiate'); } diff --git a/src/Halite.php b/src/Halite.php index a66c180d..9ec561e8 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -66,7 +66,7 @@ final class Halite * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { throw new Error('Do not instantiate'); } diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 04fb97a9..b95c70b2 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -60,7 +60,7 @@ final class Crypto * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { throw new Error('Do not instantiate'); } diff --git a/src/Util.php b/src/Util.php index 076cf646..3edf049b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -56,7 +56,7 @@ final class Util * @throws Error * @codeCoverageIgnore */ - final private function __construct() + private function __construct() { throw new Error('Do not instantiate'); } From 3439bf70842568a3628378dbcc1aa9341d9fe46b Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 16 Sep 2025 17:48:32 -0500 Subject: [PATCH 108/134] Ignore tests, workflows and .MD docs with "export-ignore" on .gitattributes --- .gitattributes | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f66a55a1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto + +/.github export-ignore +/doc export-ignore +/tests export-ignore +/.coveralls.yml export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpstan* export-ignore +/phpunit* export-ignore +/psalm* export-ignore From a69c65aedacc5cc5ed82c132709df93554fd6584 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 16 Sep 2025 17:53:28 -0500 Subject: [PATCH 109/134] Fix typo --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index f66a55a1..4f695283 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,7 @@ /.github export-ignore /doc export-ignore -/tests export-ignore +/test export-ignore /.coveralls.yml export-ignore /.gitattributes export-ignore /.gitignore export-ignore From de32b40983f4dccc5786c423e0a0f82273ef1778 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 20:23:48 -0400 Subject: [PATCH 110/134] Expand test coverage --- test/unit/AsymmetricTest.php | 48 +++++++++++++++++++++++++++++++++++- test/unit/FileTest.php | 44 +++++++++++++++++++++++++++++++++ test/unit/StreamTest.php | 47 +++++++++++++++++++++++++++++++++++ test/unit/SymmetricTest.php | 32 ++++++++++++++++++++++++ test/unit/UtilTest.php | 3 +++ 5 files changed, 173 insertions(+), 1 deletion(-) diff --git a/test/unit/AsymmetricTest.php b/test/unit/AsymmetricTest.php index 0a1e3085..a473ac1e 100644 --- a/test/unit/AsymmetricTest.php +++ b/test/unit/AsymmetricTest.php @@ -4,9 +4,11 @@ use ParagonIE\Halite\Alerts as CryptoException; use ParagonIE\Halite\KeyFactory; use ParagonIE\Halite\Asymmetric\{ + Config, Crypto as Asymmetric, EncryptionPublicKey, - EncryptionSecretKey + EncryptionSecretKey, + SignatureSecretKey }; use ParagonIE\Halite\Halite; use ParagonIE\HiddenString\HiddenString; @@ -489,4 +491,48 @@ public function testSignFail() } } } + + /** + * @return void + * @throws CryptoException\InvalidMessage + */ + public function testGetConfigInvalidMode(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + $this->expectExceptionMessage('Invalid configuration mode: decrypt'); + Config::getConfig(str_repeat('A', 4), 'decrypt'); + } + + /** + * @throws CryptoException\InvalidMessage + */ + public function testGetConfigEncryptInvalidVersion(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + $this->expectExceptionMessage('Invalid version tag'); + Config::getConfigEncrypt(1, 0); + } + + /** + * @throws CryptoException\InvalidKey + */ + public function testInvalidSignatureSecretKey(): void + { + $this->expectException(CryptoException\InvalidKey::class); + new SignatureSecretKey(new HiddenString('invalid')); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\InvalidKey + * @throws SodiumException + */ + public function testCachedPublicKey(): void + { + $keypair = KeyFactory::generateSignatureKeyPair(); + $secretKey = $keypair->getSecretKey(); + $secretKey->derivePublicKey(); + $encryptionSecretKey = $secretKey->getEncryptionSecretKey(); + $this->assertInstanceOf(\ParagonIE\Halite\Asymmetric\EncryptionSecretKey::class, $encryptionSecretKey); + } } diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index 1d1772b8..91fdb36b 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -925,4 +925,48 @@ public function testOutputToOutputbuffer() ); unlink(__DIR__.'/tmp/paragon_avatar.encrypted.png'); } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidChecksumKey(): void + { + $this->expectException(CryptoException\InvalidKey::class); + File::checksum(__DIR__.'/tmp/paragon_avatar.png', KeyFactory::generateEncryptionKey()); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\FileModified + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidConfigHeader(): void + { + touch(__DIR__.'/tmp/invalid.txt'); + chmod(__DIR__.'/tmp/invalid.txt', 0777); + file_put_contents(__DIR__.'/tmp/invalid.txt', 'invalid'); + touch(__DIR__.'/tmp/invalid-out.txt'); + chmod(__DIR__.'/tmp/invalid-out.txt', 0777); + $this->expectException(CryptoException\InvalidMessage::class); + File::decrypt( + __DIR__.'/tmp/invalid.txt', + __DIR__.'/tmp/invalid-out.txt', + KeyFactory::generateEncryptionKey() + ); + unlink(__DIR__.'/tmp/invalid.txt'); + unlink(__DIR__.'/tmp/invalid-out.txt'); + } + } diff --git a/test/unit/StreamTest.php b/test/unit/StreamTest.php index ec662279..499e7b31 100644 --- a/test/unit/StreamTest.php +++ b/test/unit/StreamTest.php @@ -210,4 +210,51 @@ public function testFileRead() $this->assertSame(bin2hex($buffer), bin2hex($mStream->readBytes($size))); } } + + + public function testMutableFileResource() + { + $fp = fopen('php://temp', 'w+b'); + $mStream = new MutableFile($fp); + $mStream->writeBytes('test'); + $mStream->reset(); + $this->assertSame('test', $mStream->readBytes(4)); + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\InvalidType + */ + public function testWriteBytesNull(): void + { + $mStream = new MutableFile(fopen('php://temp', 'w+b')); + $this->assertSame(4, $mStream->writeBytes('test', null)); + } + + /** + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testReadOnlyFileResource(): void + { + $fp = fopen('php://temp', 'rb'); + $rStream = new ReadOnlyFile($fp); + $this->assertInstanceOf(ReadOnlyFile::class, $rStream); + } + + /** + * @throws CryptoException\FileAccessDenied + * @throws CryptoException\FileError + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testReadOnlyFileWriteBytes(): void + { + $this->expectException(CryptoException\FileAccessDenied::class); + $rStream = new ReadOnlyFile(fopen('php://temp', 'rb')); + $rStream->writeBytes('test'); + } } diff --git a/test/unit/SymmetricTest.php b/test/unit/SymmetricTest.php index b110875f..1b33278b 100644 --- a/test/unit/SymmetricTest.php +++ b/test/unit/SymmetricTest.php @@ -275,4 +275,36 @@ public function testUnpack() ); $this->assertSame(Binary::safeStrlen($unpacked[5]), $config->MAC_SIZE); } + + /** + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidType + * @throws CryptoException\InvalidMessage + * @throws SodiumException + */ + public function testInvalidMac(): void + { + $key = new AuthenticationKey(new HiddenString(str_repeat('A', 32))); + try { + Symmetric::verify('test', $key, 'invalid'); + $this->fail('Invalid MAC was accepted'); + } catch (CryptoException\InvalidSignature $ex) { + $this->assertInstanceOf(CryptoException\InvalidSignature::class, $ex); + } + } + + /** + * @throws CryptoException\CannotPerformOperation + * @throws CryptoException\InvalidDigestLength + * @throws CryptoException\InvalidKey + * @throws CryptoException\InvalidMessage + * @throws CryptoException\InvalidSignature + * @throws CryptoException\InvalidType + * @throws SodiumException + */ + public function testInvalidEncodedCiphertext(): void + { + $this->expectException(CryptoException\InvalidMessage::class); + Symmetric::decrypt('invalid', new EncryptionKey(new HiddenString(str_repeat('A', 32)))); + } } diff --git a/test/unit/UtilTest.php b/test/unit/UtilTest.php index 33479e96..90bcf4f9 100644 --- a/test/unit/UtilTest.php +++ b/test/unit/UtilTest.php @@ -34,6 +34,9 @@ public function testChrToInt() $random, Util::chrToInt(Util::intToChr($random)) ); + + $this->expectException(\RangeException::class); + Util::chrToInt("ab"); } public function testIntArrayToString() From c37099f980b656ebbd35fd50f29cbd266a28eb95 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:45:23 -0400 Subject: [PATCH 111/134] Attempt to fix coverage badge --- .coveralls.yml | 3 --- .github/workflows/coverage.yml | 36 ++++++++++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) delete mode 100644 .coveralls.yml create mode 100644 .github/workflows/coverage.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 3b85ca4f..00000000 --- a/.coveralls.yml +++ /dev/null @@ -1,3 +0,0 @@ -service_name: travis-ci -coverage_clover: build/logs/clover.xml -json_path: build/logs/coveralls-upload.json diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..3091a9ee --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,36 @@ +name: Coverage Check + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + coverage: + name: PHPUnit Coverage Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, intl, sodium + ini-values: error_reporting=-1, display_errors=On + coverage: xdebug + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: PHPUnit tests with coverage + run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml + + - name: Check for 100% coverage + uses: michaelpetri/phpunit-coverage-check@0.3.1 + with: + clover-file: build/logs/clover.xml + min-coverage: 100 diff --git a/README.md b/README.md index 78f99156..fe9bb553 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://coveralls.io/repos/github/paragonie/halite/badge.svg?branch=master)](https://coveralls.io/github/paragonie/halite?branch=master) +[![Coverage Status](https://github.com/paragonie/halite/actions/workflows/coverage.yml/badge.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From 298308de19490e91ef3e1eaa5bbb2e5df9a6c18a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:46:23 -0400 Subject: [PATCH 112/134] Remove coverage badge for now --- .github/workflows/coverage.yml | 36 ---------------------------------- README.md | 1 - 2 files changed, 37 deletions(-) delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 3091a9ee..00000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Coverage Check - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - coverage: - name: PHPUnit Coverage Check - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: mbstring, intl, sodium - ini-values: error_reporting=-1, display_errors=On - coverage: xdebug - - - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" - - - name: PHPUnit tests with coverage - run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml - - - name: Check for 100% coverage - uses: michaelpetri/phpunit-coverage-check@0.3.1 - with: - clover-file: build/logs/clover.xml - min-coverage: 100 diff --git a/README.md b/README.md index fe9bb553..a7b00e3e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://github.com/paragonie/halite/actions/workflows/coverage.yml/badge.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From 0b0cbe7e31d8563fec18bc180d6e28e117e0ffd0 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:49:11 -0400 Subject: [PATCH 113/134] Use phpunit-coverage-badge --- .github/workflows/ci.yml | 3 +++ phpunit.xml.dist | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbda4e64..70288071 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,5 +33,8 @@ jobs: - name: PHPUnit tests run: vendor/bin/phpunit + - name: phpunit-coverage-badge + uses: timkrase/phpunit-coverage-badge@v1.2.1 + - name: PHPStan analysis run: vendor/bin/phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b78565c..b6054b74 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + ./src @@ -13,7 +13,7 @@ ./doc - + From c57bf0ed9647e5da9429607d07afe073dd47e0cd Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:53:09 -0400 Subject: [PATCH 114/134] Push coverage to .github --- .github/workflows/ci.yml | 12 ++++++++++++ README.md | 1 + 2 files changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70288071..7605819f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,18 @@ jobs: - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 + with: + coverage_badge_path: .github/coverage.svg + push_badge: false + + - name: Git push to image-data branch + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: .github + publish_branch: image-data + github_token: ${{ secrets.GITHUB_TOKEN }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' - name: PHPStan analysis run: vendor/bin/phpstan diff --git a/README.md b/README.md index a7b00e3e..e45e4f77 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) +[![Coverage Status](https://github.com/paragonie/halite/.github/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/ci.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From d9dceddeaf35530a1305e2ba38b43ae67e141fc5 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:54:53 -0400 Subject: [PATCH 115/134] Prevent Action cascading --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7605819f..99e1241e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,11 @@ jobs: phpunit: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} + if: "!contains(github.event.head_commit.message, '[ci]')" strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1', '8.2', '8.3', '8.4'] + php-versions: ['8.4'] steps: - name: Checkout @@ -50,3 +51,33 @@ jobs: - name: PHPStan analysis run: vendor/bin/phpstan + + older-php: + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + if: "!contains(github.event.head_commit.message, '[ci]')" + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.1', '8.2', '8.3'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium + ini-values: error_reporting=-1, display_errors=On + coverage: none + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: PHPUnit tests + run: vendor/bin/phpunit + + - name: PHPStan analysis + run: vendor/bin/phpstan From 909b840bd2c55ce1049d33c7fd2986a57583ccec Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 22:58:53 -0400 Subject: [PATCH 116/134] Does enabling pcov fix this? --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e1241e..3f5ec049 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, sodium + extensions: mbstring, intl, pcov, sodium ini-values: error_reporting=-1, display_errors=On coverage: none From d6dd9ec67cbc45231c908fe29e0468f77e876410 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:00:29 -0400 Subject: [PATCH 117/134] Make clover.xml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f5ec049..0f534625 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: [ master ] jobs: - phpunit: + latest: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} if: "!contains(github.event.head_commit.message, '[ci]')" @@ -32,7 +32,7 @@ jobs: uses: "ramsey/composer-install@v3" - name: PHPUnit tests - run: vendor/bin/phpunit + run: vendor/bin/phpunit --coverage-clover clover.xml - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 From f556c7579585cf901ea9ebd0b5afca5ce1fc27e4 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:02:40 -0400 Subject: [PATCH 118/134] Use PECL to install PCOV --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f534625..78ceed30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,13 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, pcov, sodium + extensions: mbstring, intl, sodium ini-values: error_reporting=-1, display_errors=On coverage: none + - name: Install PCOV + run: pecl install pcov + - name: Install Composer dependencies uses: "ramsey/composer-install@v3" From e6f2c581c6ed4b6a6b6051107b84855581f36fc1 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:04:12 -0400 Subject: [PATCH 119/134] Make coverage its own file --- .github/workflows/ci.yml | 52 ++----------------------------- .github/workflows/coverage.yml | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78ceed30..cf09ca5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,62 +7,14 @@ on: branches: [ master ] jobs: - latest: + phpunit: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} if: "!contains(github.event.head_commit.message, '[ci]')" strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.4'] - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, sodium - ini-values: error_reporting=-1, display_errors=On - coverage: none - - - name: Install PCOV - run: pecl install pcov - - - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" - - - name: PHPUnit tests - run: vendor/bin/phpunit --coverage-clover clover.xml - - - name: phpunit-coverage-badge - uses: timkrase/phpunit-coverage-badge@v1.2.1 - with: - coverage_badge_path: .github/coverage.svg - push_badge: false - - - name: Git push to image-data branch - uses: peaceiris/actions-gh-pages@v3 - with: - publish_dir: .github - publish_branch: image-data - github_token: ${{ secrets.GITHUB_TOKEN }} - user_name: 'github-actions[bot]' - user_email: 'github-actions[bot]@users.noreply.github.com' - - - name: PHPStan analysis - run: vendor/bin/phpstan - - older-php: - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - runs-on: ${{ matrix.operating-system }} - if: "!contains(github.event.head_commit.message, '[ci]')" - strategy: - matrix: - operating-system: ['ubuntu-latest'] - php-versions: ['8.1', '8.2', '8.3'] + php-versions: ['8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..dd028b19 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + coverage: + name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + if: "!contains(github.event.head_commit.message, '[ci]')" + strategy: + matrix: + operating-system: ['ubuntu-latest'] + php-versions: ['8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, intl, sodium + ini-values: error_reporting=-1, display_errors=On + coverage: none + + - name: Install PCOV + run: pecl install pcov + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + + - name: PHPUnit tests + run: vendor/bin/phpunit --coverage-clover clover.xml + + - name: phpunit-coverage-badge + uses: timkrase/phpunit-coverage-badge@v1.2.1 + with: + coverage_badge_path: .github/coverage.svg + push_badge: false + + - name: Git push to image-data branch + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: .github + publish_branch: image-data + github_token: ${{ secrets.GITHUB_TOKEN }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + + - name: PHPStan analysis + run: vendor/bin/phpstan From 30584ef169bbbb48fdb6c5a9713ccfbc5b7b7898 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:06:07 -0400 Subject: [PATCH 120/134] Try xdebug --- .github/workflows/coverage.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index dd028b19..5826407c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,13 +24,10 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: mbstring, intl, sodium + extensions: mbstring, intl, sodium, xdebug ini-values: error_reporting=-1, display_errors=On coverage: none - - name: Install PCOV - run: pecl install pcov - - name: Install Composer dependencies uses: "ramsey/composer-install@v3" @@ -51,6 +48,3 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' - - - name: PHPStan analysis - run: vendor/bin/phpstan From 6dcf6333f16c066d9e89f399b9c0458117646080 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:06:48 -0400 Subject: [PATCH 121/134] Rename coverage test --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5826407c..b97bd8ea 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -8,7 +8,7 @@ on: jobs: coverage: - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} + name: Code Coverage - PHPP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} if: "!contains(github.event.head_commit.message, '[ci]')" strategy: From 76d4ff305b2b470b48402fae0e2cd62189a92d5c Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:10:48 -0400 Subject: [PATCH 122/134] Let's try this --- .github/workflows/coverage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b97bd8ea..fb4cdad3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -31,14 +31,14 @@ jobs: - name: Install Composer dependencies uses: "ramsey/composer-install@v3" - - name: PHPUnit tests - run: vendor/bin/phpunit --coverage-clover clover.xml + - name: PHPUnit tests with coverage + run: vendor/bin/phpunit --coverage-clover clover.xml --coverage-html build/coverage-report - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 with: coverage_badge_path: .github/coverage.svg - push_badge: false + push_badge: true - name: Git push to image-data branch uses: peaceiris/actions-gh-pages@v3 From 8ce0918ab4b7454a744c7f4e4513f2b67c850a19 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:13:34 -0400 Subject: [PATCH 123/134] Maybe it's a XML issue --- .github/workflows/coverage.yml | 3 +++ phpunit.xml.dist | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fb4cdad3..dcc68eb9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -34,6 +34,9 @@ jobs: - name: PHPUnit tests with coverage run: vendor/bin/phpunit --coverage-clover clover.xml --coverage-html build/coverage-report + - name: Copy clover.xml + run: cp clover.xml /github/workspace/clover.xml + - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 with: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b6054b74..18f41084 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,7 +13,7 @@ ./doc - + From 2867c39cf59cdd996c11bead84f3bb0d42b7442a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:15:32 -0400 Subject: [PATCH 124/134] Could it be this stupid? --- .github/workflows/coverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index dcc68eb9..737056c9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -31,6 +31,9 @@ jobs: - name: Install Composer dependencies uses: "ramsey/composer-install@v3" + - name: Ensure XML file is being loaded + run: cp phpunit.xml.dist phpunit.xml + - name: PHPUnit tests with coverage run: vendor/bin/phpunit --coverage-clover clover.xml --coverage-html build/coverage-report From e06e47914196636cbfa0a864c1ff06b060fc0252 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:16:09 -0400 Subject: [PATCH 125/134] Clean up --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 737056c9..88b227d2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: CI +name: Coverage on: push: @@ -8,7 +8,7 @@ on: jobs: coverage: - name: Code Coverage - PHPP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} + name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }} if: "!contains(github.event.head_commit.message, '[ci]')" strategy: From d6e39446174fe04eac128e9533219627511fa595 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:16:44 -0400 Subject: [PATCH 126/134] This doesn't work --- .github/workflows/coverage.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 88b227d2..811654de 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -37,9 +37,6 @@ jobs: - name: PHPUnit tests with coverage run: vendor/bin/phpunit --coverage-clover clover.xml --coverage-html build/coverage-report - - name: Copy clover.xml - run: cp clover.xml /github/workspace/clover.xml - - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 with: From 6335fcff4c36a2279d6dc7ba33ec9778052c6d49 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:19:33 -0400 Subject: [PATCH 127/134] How silly of me --- .github/workflows/coverage.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 811654de..a0d00b6d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,7 +26,7 @@ jobs: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl, sodium, xdebug ini-values: error_reporting=-1, display_errors=On - coverage: none + coverage: xdebug - name: Install Composer dependencies uses: "ramsey/composer-install@v3" diff --git a/README.md b/README.md index e45e4f77..88a8275c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://github.com/paragonie/halite/.github/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/ci.yml) +[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/image-data/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From 1b0f88b63b7994a3c8c4178667ed5f614af142f9 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:21:43 -0400 Subject: [PATCH 128/134] Coverage can write --- .github/workflows/coverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a0d00b6d..f7f4efc6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,5 +1,8 @@ name: Coverage +permissions: + contents: write + on: push: branches: [ master ] From 7307f268a088ffeb4c40cbaeb5ba7895e2ac46ff Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:28:17 -0400 Subject: [PATCH 129/134] Add access token --- .github/workflows/coverage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f7f4efc6..ee7768ba 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -45,12 +45,13 @@ jobs: with: coverage_badge_path: .github/coverage.svg push_badge: true + github_token: ${{ secrets.TOKEN }} - name: Git push to image-data branch uses: peaceiris/actions-gh-pages@v3 with: publish_dir: .github publish_branch: image-data - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.TOKEN }} user_name: 'github-actions[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' From f80e7df59aaec05b13bc5eeab04b7121b71d6d9b Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:29:47 -0400 Subject: [PATCH 130/134] Attempt number aleph null --- .github/workflows/coverage.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ee7768ba..e08f4c7b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -44,8 +44,7 @@ jobs: uses: timkrase/phpunit-coverage-badge@v1.2.1 with: coverage_badge_path: .github/coverage.svg - push_badge: true - github_token: ${{ secrets.TOKEN }} + push_badge: false - name: Git push to image-data branch uses: peaceiris/actions-gh-pages@v3 From e6fde47f2b201ba535835399fc61a565a5d99a8d Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:33:09 -0400 Subject: [PATCH 131/134] Fix image-data scope --- .github/output/.gitkeep | 0 .github/workflows/coverage.yml | 4 ++-- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .github/output/.gitkeep diff --git a/.github/output/.gitkeep b/.github/output/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e08f4c7b..8abe0e64 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -43,13 +43,13 @@ jobs: - name: phpunit-coverage-badge uses: timkrase/phpunit-coverage-badge@v1.2.1 with: - coverage_badge_path: .github/coverage.svg + coverage_badge_path: .github/output/coverage.svg push_badge: false - name: Git push to image-data branch uses: peaceiris/actions-gh-pages@v3 with: - publish_dir: .github + publish_dir: .github/output publish_branch: image-data github_token: ${{ secrets.TOKEN }} user_name: 'github-actions[bot]' diff --git a/README.md b/README.md index 88a8275c..82e09030 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/image-data/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) +[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/image-data/.github/output/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From 2cafe1aa15c861f98b9bf57eb926501a2bcdb3da Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:34:24 -0400 Subject: [PATCH 132/134] Fix image path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82e09030..bd95e4c1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Latest Unstable Version](https://poser.pugx.org/paragonie/halite/v/unstable)](https://packagist.org/packages/paragonie/halite) [![License](https://poser.pugx.org/paragonie/halite/license)](https://packagist.org/packages/paragonie/halite) [![Downloads](https://img.shields.io/packagist/dt/paragonie/halite.svg)](https://packagist.org/packages/paragonie/halite) -[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/image-data/.github/output/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) +[![Coverage Status](https://raw.githubusercontent.com/paragonie/halite/refs/heads/image-data/coverage.svg)](https://github.com/paragonie/halite/actions/workflows/coverage.yml) **Halite** is a high-level cryptography interface that relies on [libsodium](https://pecl.php.net/package/libsodium) for all of its underlying cryptography operations. From d6ac9dcc69880723bbc69b192c86dff90a07108a Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:38:17 -0400 Subject: [PATCH 133/134] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2fbb0c..132cee21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Version 5.1.4 (2025-09-18) + +* Add PHPStan analysis, level 5 by @spaze in https://github.com/paragonie/halite/pull/195 +* Replace all `http://` links with the `https://` URL they redirect to by @GrahamCampbell in https://github.com/paragonie/halite/pull/196 +* Use Psalm 6 by @spaze in https://github.com/paragonie/halite/pull/198 +* Remove access modifier `final` from private methods by @junaidbinfarooq in https://github.com/paragonie/halite/pull/204 +* Ignore tests, workflows and .MD docs with "export-ignore" on .gitattr… by @erikn69 in https://github.com/paragonie/halite/pull/205 +* Expand test coverage by @paragonie-security in https://github.com/paragonie/halite/pull/206 +* Fixed the broken test coverage badge (https://github.com/paragonie/halite/pull/207 and https://github.com/paragonie/halite/pull/208) + ## Version 5.1.3 (2025-01-23) * Merged [#184](https://github.com/paragonie/halite/pull/194), which fixes PHP 8.4 deprecations with nullable types. From 12e7d1ab50ef3cacc27b59140e75d2cf59e85f71 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Thu, 18 Sep 2025 23:39:32 -0400 Subject: [PATCH 134/134] Add readme to output directory for coverage badge --- .github/output/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/output/README.md diff --git a/.github/output/README.md b/.github/output/README.md new file mode 100644 index 00000000..d67317d4 --- /dev/null +++ b/.github/output/README.md @@ -0,0 +1,3 @@ +# Coverage Output + +If you are looking at the `image-data` branch, please know that this is just a hack to get the coverage badge working.