From 4148a77434d844e5996555afa302bf83822a947b Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Fri, 21 Feb 2025 22:03:23 +0100 Subject: [PATCH] Forward compatibility with v5 --- psalm.xml | 4 +- src/Asymmetric/Config.php | 102 +++++++++++++++++++ src/Asymmetric/Crypto.php | 82 ++++++++++++--- src/Cookie.php | 2 - src/File.php | 199 ++++++++++++++++++++++++++++--------- src/Halite.php | 1 + src/Password.php | 2 + src/Symmetric/Config.php | 26 ++++- src/Symmetric/Crypto.php | 100 ++++++++++++------- src/Util.php | 80 +++++++++++++++ test/unit/RemoteStream.php | 1 + 11 files changed, 496 insertions(+), 103 deletions(-) create mode 100644 src/Asymmetric/Config.php diff --git a/psalm.xml b/psalm.xml index 69c5b316..132ad1c5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,7 @@ +> @@ -11,6 +11,8 @@ + + diff --git a/src/Asymmetric/Config.php b/src/Asymmetric/Config.php new file mode 100644 index 00000000..a03dd497 --- /dev/null +++ b/src/Asymmetric/Config.php @@ -0,0 +1,102 @@ + Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => 'HaliteVersion5X25519SharedSecret', + 'HASH_SCALARMULT' => true, + ]; + } + } + if ($major === 4 || $major === 3) { + switch ($minor) { + case 0: + return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, + 'HASH_DOMAIN_SEPARATION' => '', + 'HASH_SCALARMULT' => false, + ]; + } + } + throw new InvalidMessage( + 'Invalid version tag' + ); + } +} diff --git a/src/Asymmetric/Crypto.php b/src/Asymmetric/Crypto.php index 5f05d73f..1e0be0e8 100644 --- a/src/Asymmetric/Crypto.php +++ b/src/Asymmetric/Crypto.php @@ -110,7 +110,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( @@ -186,7 +188,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( @@ -208,6 +212,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 @@ -217,24 +222,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 ): object { - 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( - \sodium_crypto_scalarmult( + + $hiddenString = new HiddenString( + sodium_crypto_scalarmult( $privateKey->getRawKeyMaterial(), $publicKey->getRawKeyMaterial() ) ); + if ($get_as_object) { + return new EncryptionKey($hiddenString); + } + return $hiddenString; } /** @@ -478,4 +497,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/Cookie.php b/src/Cookie.php index f3a0dbce..97991f15 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -147,8 +147,6 @@ protected static function getConfig(string $stored): SymmetricConfig * @throws InvalidMessage * @throws InvalidType * @throws \TypeError - * - * @psalm-suppress InvalidArgument PHP version incompatibilities * @psalm-suppress MixedArgument */ public function store( diff --git a/src/File.php b/src/File.php index 9e8b5cd5..8903ee66 100644 --- a/src/File.php +++ b/src/File.php @@ -25,6 +25,7 @@ Symmetric\AuthenticationKey, Symmetric\EncryptionKey }; +use ParagonIE\ConstantTime\Binary; use ParagonIE\HiddenString\HiddenString; /** @@ -603,10 +604,26 @@ protected static function encryptData( ); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + $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', 4)); + + // 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', $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); + } /** @var string $mac */ Util::memzero($authKey); @@ -677,10 +694,26 @@ protected static function decryptData( list ($encKey, $authKey) = self::splitKeys($key, $hkdfSalt, $config); // VERSION 2+ uses BMAC - $mac = \sodium_crypto_generichash_init($authKey); - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $firstNonce); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + $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', 4)); + + // 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', $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); + } /** @var string $mac */ $old_macs = self::streamVerify($input, Util::safeStrcpy($mac), $config); @@ -740,7 +773,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.'); @@ -790,9 +828,24 @@ protected static function sealData( // We no longer need $authKey after we set up the hash context Util::memzero($authKey); - \sodium_crypto_generichash_update($mac, Halite::HALITE_VERSION_FILE); - \sodium_crypto_generichash_update($mac, $ephPublic->getRawKeyMaterial()); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', 4)); + + // 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', $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); + } unset($ephPublic); Util::memzero($hkdfSalt); @@ -876,7 +929,8 @@ protected static function unsealData( $key = AsymmetricCrypto::getSharedSecret( $secretKey, $ephemeral, - true + true, + AsymmetricCrypto::getAsymmetricConfig($header, true) ); // @codeCoverageIgnoreStart if (!($key instanceof EncryptionKey)) { @@ -893,11 +947,29 @@ protected static function unsealData( // We no longer need the original key after we split it unset($key); - $mac = \sodium_crypto_generichash_init($authKey); - - \sodium_crypto_generichash_update($mac, $header); - \sodium_crypto_generichash_update($mac, $ephPublic); - \sodium_crypto_generichash_update($mac, $hkdfSalt); + $mac = sodium_crypto_generichash_init($authKey); + + if ($config->USE_PAE) { + // Number of pieces: + sodium_crypto_generichash_update($mac, pack('P', 4)); + + // 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', $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); + } /** @var string $mac */ $oldMACs = self::streamVerify($input, Util::safeStrcpy($mac), $config); @@ -1046,14 +1118,29 @@ 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_USE_INFO' => 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_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1065,7 +1152,10 @@ protected static function getConfigEncrypt(int $major, int $minor): array 'BUFFER' => 1048576, 'NONCE_BYTES' => \SODIUM_CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, + 'ENC_ALGO' => 'XSalsa20', 'MAC_SIZE' => 32, + 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1074,7 +1164,7 @@ protected static function getConfigEncrypt(int $major, int $minor): array // If we reach here, we've got an invalid version tag: // @codeCoverageIgnoreStart throw new InvalidMessage( - 'Invalid version tag' + 'Invalid version tag g' ); // @codeCoverageIgnoreEnd } @@ -1089,7 +1179,23 @@ 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 [ + 'SHORTEST_CIPHERTEXT_LENGTH' => 100, + 'BUFFER' => 1048576, + 'HKDF_SALT_LEN' => 32, + 'MAC_SIZE' => 32, + 'PUBLICKEY_BYTES' => SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XChaCha20', + 'USE_PAE' => true, + 'HKDF_USE_INFO' => true, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } + } elseif ($major === 4) { switch ($minor) { case 0: return [ @@ -1098,6 +1204,9 @@ protected static function getConfigSeal(int $major, int $minor): array 'HKDF_SALT_LEN' => 32, 'MAC_SIZE' => 32, 'PUBLICKEY_BYTES' => \SODIUM_CRYPTO_BOX_PUBLICKEYBYTES, + 'ENC_ALGO' => 'XSalsa20', + 'USE_PAE' => false, + 'HKDF_USE_INFO' => false, 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -1133,7 +1242,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 [ @@ -1153,7 +1262,7 @@ protected static function getConfigChecksum(int $major, int $minor): array /** * Split a key using HKDF-BLAKE2b * - * @param Key $master + * @param EncryptionKey $master * @param string $salt * @param Config $config * @return array @@ -1163,25 +1272,11 @@ protected static function getConfigChecksum(int $major, int $minor): array * @throws \TypeError */ protected static function splitKeys( - Key $master, + EncryptionKey $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 - ) - ]; + return Util::splitKeys($master, $salt, $config); } /** @@ -1310,7 +1405,7 @@ private static function streamDecrypt( // @codeCoverageIgnoreStart // Someone attempted to add a chunk at the end. throw new InvalidMessage( - 'Invalid message authentication code' + 'Invalid message authentication code b' ); // @codeCoverageIgnoreEnd } else { @@ -1320,18 +1415,26 @@ private static function streamDecrypt( // This chunk was altered after the original MAC was verified // @codeCoverageIgnoreStart throw new InvalidMessage( - 'Invalid message authentication code' + 'Invalid message authentication code c' ); // @codeCoverageIgnoreEnd } } // 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, + $nonce, + $encKey->getRawKeyMaterial() + ); + } else { + $decrypted = sodium_crypto_stream_xor( + $read, + $nonce, + $encKey->getRawKeyMaterial() + ); + } $output->writeBytes($decrypted); \sodium_increment($nonce); } @@ -1418,7 +1521,7 @@ private static function streamVerify( */ if (!\hash_equals($finalHMAC, $stored_mac)) { throw new InvalidMessage( - 'Invalid message authentication code' + 'Invalid message authentication code d' ); } $input->reset($start); diff --git a/src/Halite.php b/src/Halite.php index 9bb37afe..f7be91fd 100644 --- a/src/Halite.php +++ b/src/Halite.php @@ -44,6 +44,7 @@ final class Halite /* Raw bytes (decoded) of the underlying ciphertext */ const VERSION_TAG_LEN = 4; + const VERSION_5_PREFIX = 'MUIFA'; const VERSION_PREFIX = 'MUIEA'; const VERSION_OLD_PREFIX = 'MUIDA'; diff --git a/src/Password.php b/src/Password.php index 6c0b0b70..fa87368b 100644 --- a/src/Password.php +++ b/src/Password.php @@ -161,6 +161,8 @@ protected static function getConfig(string $stored): SymmetricConfig ); } if ( + \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_5_PREFIX) + || \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX) || \hash_equals(Binary::safeSubstr($stored, 0, 5), Halite::VERSION_OLD_PREFIX) diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 2952ca66..f376652a 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -76,6 +76,25 @@ public static function getConfig( */ public static function getConfigEncrypt(int $major, int $minor): array { + 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_USE_INFO' => true, + 'HKDF_SBOX' => 'Halite|EncryptionKey', + 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' + ]; + } + } + if ($major === 3 || $major === 4) { switch ($minor) { case 0: @@ -84,8 +103,11 @@ 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_USE_INFO' => false, // ? 'HKDF_SBOX' => 'Halite|EncryptionKey', 'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite' ]; @@ -106,14 +128,16 @@ 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 === 3 || $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, '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 241f3cb0..a1f76328 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -113,10 +113,15 @@ 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 + * * @param string $ciphertext * @param EncryptionKey $secretKey * @param string $additionalData * @param mixed $encoding + * * @return HiddenString * * @throws CannotPerformOperation @@ -178,31 +183,43 @@ public static function decryptWithAd( $authKey = $split[1]; // Check the MAC first - if (!self::verifyMAC( + if ($config->USE_PAE) { + $verified = self::verifyMAC( + $auth, + CryptoUtil::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' ); } + CryptoUtil::memzero($salt); 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); @@ -281,22 +298,43 @@ 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( + CryptoUtil::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 */ $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory @@ -331,21 +369,7 @@ public static function splitKeys( string $salt, BaseConfig $config ): array { - $binary = $master->getRawKeyMaterial(); - return [ - CryptoUtil::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, - (string) $config->HKDF_SBOX, - $salt - ), - CryptoUtil::hkdfBlake2b( - $binary, - \SODIUM_CRYPTO_AUTH_KEYBYTES, - (string) $config->HKDF_AUTH, - $salt - ) - ]; + return CryptoUtil::splitKeys($master, $salt, $config); } /** diff --git a/src/Util.php b/src/Util.php index 16edcb91..3d47ecf0 100644 --- a/src/Util.php +++ b/src/Util.php @@ -11,6 +11,9 @@ InvalidDigestLength, InvalidType }; +use ParagonIE\Halite\Symmetric\EncryptionKey; +use SodiumException; +use TypeError; /** * Class Util @@ -218,6 +221,23 @@ 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() * @@ -255,6 +275,66 @@ public static function raw_keyed_hash( return \sodium_crypto_generichash($input, $key, $length); } + /** + * 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 + ) + ]; + } + /** * PHP 7 uses interned strings. We don't want altering this one to alter * the original string. diff --git a/test/unit/RemoteStream.php b/test/unit/RemoteStream.php index 00b18278..724abbab 100644 --- a/test/unit/RemoteStream.php +++ b/test/unit/RemoteStream.php @@ -8,6 +8,7 @@ final class RemoteStream { private $contents; private $position = 0; + public $context; function stream_open($path, $mode, $options, &$opened_path) {