From ca1d9a412d2c4d492b07a7bae0f49c60ded08726 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 5 Oct 2021 08:56:42 -0400 Subject: [PATCH 01/86] 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 fa03851..17b754d 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 02/86] Update dependencies --- composer.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 17b754d..f159981 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 03/86] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72579df..7cd6f1a 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 04/86] 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 7cd6f1a..c275ae0 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 5873dbd..bec1e7c 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 0f23dd9..f39f139 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 9e8b5cd..b64e801 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 9bb37af..4495898 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 2952ca6..5af1b32 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 241f3cb..6f13764 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 16edcb9..77790af 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 2a19908..3a13299 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 dfc1eb7..8962d0d 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 05/86] 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 f39f139..3dbeb2b 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 b64e801..43cfe98 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 5af1b32..fee11f3 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 3a13299..b93adb7 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 06/86] 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 07105a9..678f3ec 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 a99703e..8772efe 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 4ebd1c1..c6204ed 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 43cfe98..4a3eee1 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 9d3d72a..e8240fb 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 b93adb7..adf32ee 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 07/86] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c275ae0..5f234bd 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 08/86] 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 e2b3636..ebb19e6 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 09/86] 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 4a3eee1..0a7dbde 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 10/86] Update CHANGELOG, gitignore --- .gitignore | 1 + CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d4fb827..a82c535 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 5f234bd..3754314 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 11/86] 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 78e73b6..a76d08d 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 ab1a6b5..2f9ce8c 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 678f3ec..4ddc344 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 61bba20..0d9ee98 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 8772efe..f85e2f8 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 d536678..141a293 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 c6204ed..eee0917 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 3dbeb2b..4ff8170 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 082b501..15f39f7 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 f3a0dbc..eeaa627 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 6004f5d..4b675d5 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 0a7dbde..97ad23c 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 4495898..a55efa3 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 e8240fb..2591645 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 1cd462a..9a62c33 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 6c0b0b7..a945596 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 d866b15..4dc9403 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 acdcb9b..ba6299e 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 40e48f8..5506f3c 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 65b3e7a..502eef2 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 034044d..622cd1d 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 a394450..398c4de 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 ff990a1..b4b932f 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 fee11f3..5685cbb 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 6f13764..a696871 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 61160b4..c6d00c4 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 77790af..c4efba4 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 12/86] Update README.md --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c935fc0..aabad76 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 13/86] 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 2f9ce8c..ce44766 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 4ddc344..20c1b63 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 141a293..db78293 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 eee0917..38d858e 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 b4b932f..afef733 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 c6d00c4..2443980 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 3cfeea7..c7275ea 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 14/86] 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 b0f83d8..0000000 --- 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 15/86] Clarify HKDF --- src/Util.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Util.php b/src/Util.php index c4efba4..f31a8f0 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 16/86] 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 fe38d3f..8226956 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 4ff8170..5402f7a 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 eeaa627..c905f15 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 97ad23c..1f3fd7d 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 a55efa3..f2b8e3d 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 dbe62ce..4900bc9 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 2591645..e32728b 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 9a62c33..72bdd0c 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 a945596..8acd6d5 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 4dc9403..d36386d 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 a696871..2e3617e 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 f31a8f0..b8ec091 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 17/86] 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 3754314..3e99212 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 0000000..a03dd49 --- /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 8226956..382b916 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 1f3fd7d..0b9a6d0 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 5685cbb..db55d9c 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 18/86] 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 382b916..3f9362c 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 20c1b63..ede9f8b 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 f85e2f8..6da48fa 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 38d858e..f834f6c 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 ba6299e..d1cb7a5 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 5506f3c..a2bac8d 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 502eef2..2a59fbb 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 622cd1d..8bc083d 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 398c4de..8a9a29d 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 afef733..3ef8f51 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 db55d9c..3f19877 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 2e3617e..c62266a 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 19/86] 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 aabad76..fc08b52 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 20/86] 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 3e99212..1dc0bf0 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 a03dd49..2956335 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 5402f7a..f2d90ad 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 0b9a6d0..89d4e36 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 3f19877..d7dec55 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 c62266a..5d623c0 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 b8ec091..4094028 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 21/86] 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 3972487..9527310 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 0000000..1068243 --- /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 6f0263a..feccf3b 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 0000000..f2a5cc5 --- /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 653dc05..c968f7b 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 0d6ef7f..d97456f 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 22/86] 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 88800e0..998df7c 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 23/86] 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 5398e84..63a74bb 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 9b7d560..b9b5ec9 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 24/86] 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 998df7c..04b0bd9 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 25/86] 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 0aa7647..aabac89 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 26/86] 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 bec1e7c..0fabd1d 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 27/86] 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 1dc0bf0..723daeb 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 28/86] 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 ebb19e6..04dffdc 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 723daeb..acd5aa8 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 fc08b52..143543c 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 f159981..c32fea2 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 29/86] 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 04dffdc..c317f98 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 30/86] 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 c317f98..7982793 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 0000000..8106841 --- /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 31/86] Fix psalm.xml --- psalm.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psalm.xml b/psalm.xml index 69c5b31..5d159e6 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 32/86] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 143543c..78f9915 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 33/86] 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 7982793..9ca9460 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 8106841..f5bd5c5 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 acd5aa8..b4a7cc4 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 c32fea2..e9937d5 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 34/86] 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 e9937d5..cae0513 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 35/86] 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 3f9362c..42c8baa 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 ede9f8b..c361ab1 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 6da48fa..ee8d7f6 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 f834f6c..355db43 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 c905f15..beb493b 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 4b675d5..182c855 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 8acd6d5..ba8dda9 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 d36386d..721b9e9 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 3ef8f51..a53a69b 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 2443980..ea8fee2 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 36/86] 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 9ca9460..7eba2b9 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 f5bd5c5..489eb95 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 b4a7cc4..1a3daf2 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 5d159e6..132ad1c 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 37/86] 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 89d4e36..bbb5c03 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 a2bac8d..639e570 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 38/86] 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 04b0bd9..2a94da4 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 39/86] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3daf2..6d2fbb0 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 40/86] 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 cae0513..dac0c8f 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 0000000..dee93f6 --- /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 41/86] 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 f2d90ad..a48862a 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 42/86] 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 bbb5c03..48cff7d 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 c433fa7..823c3df 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 43/86] 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 182c855..a4f8f29 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 721b9e9..667a1dc 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 2a59fbb..3e71fd4 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 8a9a29d..d52172e 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 44/86] 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 48cff7d..daaac1f 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 45/86] Document exceptions --- src/SignatureKeyPair.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index 667a1dc..dd37dad 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 46/86] 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 beb493b..1d33403 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 d1cb7a5..66b4fc4 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 639e570..33c6399 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 3e71fd4..9e58fa0 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 5d623c0..dcd035b 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 21238c7..a78caf5 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 47/86] 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 76c8489..c13a672 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 adf32ee..1d1772b 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 31da70d..d625cac 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 c7275ea..8c24a3a 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 8962d0d..c9edc2f 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 823c3df..ec66227 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 2939b2b..48eb0ef 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 48/86] $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 48eb0ef..b110875 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 49/86] 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 0000000..93e3577 --- /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 dee93f6..0e73c77 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 50/86] 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 a82c535..321fa66 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 51/86] 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 7eba2b9..fbda4e6 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 dac0c8f..092d93e 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 52/86] 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 a612ad9..4f573b7 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 a76d08d..3ecadf6 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 0685f28..3d53a1c 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 116d091..3b088cf 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 d8711b7..11acfbc 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 5ab7ec9..6c78cfb 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 2679a70..b99138e 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 7f7cf32..715d751 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 6fdce62..02297d6 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 6e86b59..23ee19c 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 d567437..2da6e39 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 ff6f68e..81c8db8 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 9306dc9..3fb08d3 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 c75044b..ebc8a83 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 8cb0cd6..bb9f0a9 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 c3bc092..682c580 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 01b3f68..5d597ab 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 2956335..02ec5cb 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 42c8baa..7b3fb1f 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 ce44766..1fde7c9 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 c361ab1..233dfee 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 0d9ee98..3987c4f 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 ee8d7f6..914941b 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 db78293..6d9e4b0 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 355db43..f92d4e9 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 a48862a..b57f02a 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 15f39f7..be60a35 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 1d33403..7dd0371 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 a4f8f29..2db3f95 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 daaac1f..f0dad8c 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 f2b8e3d..a66c180 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 4900bc9..c29b86b 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 e32728b..93f29c4 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 72bdd0c..4fab330 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 ba8dda9..eef03d1 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 dd37dad..b51d5ae 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 66b4fc4..1566b2c 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 33c6399..290723f 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 9e58fa0..ec535d1 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 8bc083d..0f048a7 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 d52172e..2f213b5 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 a53a69b..1c065a0 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 d7dec55..3a1f7be 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 dcd035b..04fb97a 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 ea8fee2..ce48f4b 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 42588e7..2201283 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 4094028..076cf64 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 c13a672..0a1e308 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 685fe4f..33479e9 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 53/86] 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 092d93e..642d846 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 54/86] 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 132ad1c..7fed617 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 55/86] 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 7dd0371..01a776e 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 eef03d1..0989f0a 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 1566b2c..60fbd54 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 290723f..c30f195 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 56/86] 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 489eb95..7935458 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 57/86] 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 7b3fb1f..2740ff0 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 a66c180..9ec561e 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 04fb97a..b95c70b 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 076cf64..3edf049 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 58/86] 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 0000000..f66a55a --- /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 59/86] Fix typo --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index f66a55a..4f69528 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 60/86] 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 0a1e308..a473ac1 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 1d1772b..91fdb36 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 ec66227..499e7b3 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 b110875..1b33278 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 33479e9..90bcf4f 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 61/86] 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 3b85ca4..0000000 --- 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 0000000..3091a9e --- /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 78f9915..fe9bb55 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 62/86] 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 3091a9e..0000000 --- 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 fe9bb55..a7b00e3 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 63/86] 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 fbda4e6..7028807 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 7b78565..b6054b7 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 64/86] 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 7028807..7605819 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 a7b00e3..e45e4f7 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 65/86] 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 7605819..99e1241 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 66/86] 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 99e1241..3f5ec04 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 67/86] 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 3f5ec04..0f53462 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 68/86] 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 0f53462..78ceed3 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 69/86] 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 78ceed3..cf09ca5 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 0000000..dd028b1 --- /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 70/86] 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 dd028b1..5826407 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 71/86] 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 5826407..b97bd8e 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 72/86] 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 b97bd8e..fb4cdad 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 73/86] 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 fb4cdad..dcc68eb 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 b6054b7..18f4108 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 74/86] 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 dcc68eb..737056c 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 75/86] 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 737056c..88b227d 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 76/86] 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 88b227d..811654d 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 77/86] 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 811654d..a0d00b6 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 e45e4f7..88a8275 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 78/86] 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 a0d00b6..f7f4efc 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 79/86] 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 f7f4efc..ee7768b 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 80/86] 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 ee7768b..e08f4c7 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 81/86] 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 0000000..e69de29 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e08f4c7..8abe0e6 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 88a8275..82e0903 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 82/86] Fix image path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82e0903..bd95e4c 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 83/86] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2fbb0..132cee2 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 84/86] 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 0000000..d67317d --- /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. From 68e8e567b138bd0d1105dc9e4c4a739103990eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 22 Dec 2025 21:45:01 +0100 Subject: [PATCH 85/86] Run tests on PHP 8.5 as well --- .github/workflows/ci.yml | 2 +- .github/workflows/psalm.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf09ca5..3081e76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1', '8.2', '8.3', '8.4'] + php-versions: ['8.1', '8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 7935458..1126975 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1', '8.2', '8.3', '8.4'] + php-versions: ['8.1', '8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout From 83a7eef034c9432b4bfb2ac27f9f6cecbb4a8cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 22 Dec 2025 21:55:43 +0100 Subject: [PATCH 86/86] $argc/$argv do not exist anymore in PHP 8.5 Even though they likely should, but most probably won't, see the discussion in https://github.com/php/php-src/issues/20279 --- test/random_audit.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/random_audit.php b/test/random_audit.php index 09ce70a..d1ae371 100644 --- a/test/random_audit.php +++ b/test/random_audit.php @@ -38,8 +38,8 @@ function list_all_files(string $folder, string $extension = '*'): array return $fileList; } -if ($argc > 1) { - $extensions = array_slice($argv, 1); +if ($_SERVER['argc'] > 1) { + $extensions = array_slice($_SERVER['argv'], 1); } else { $extensions = ['php', 'twig']; }