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 *