diff --git a/src/Cookie.php b/src/Cookie.php index db32c11..cf3816b 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -2,10 +2,12 @@ declare(strict_types=1); namespace ParagonIE\Halite; +use ParagonIE\ConstantTime\Base64UrlSafe; use ParagonIE\Halite\{ Alerts\InvalidMessage, - Symmetric\EncryptionKey, - Symmetric\Crypto + Symmetric\Config as SymmetricConfig, + Symmetric\Crypto, + Symmetric\EncryptionKey }; /** @@ -54,8 +56,13 @@ public function fetch(string $name) return null; } try { - $decrypted = Crypto::decrypt($_COOKIE[$name], $this->key) - ->getString(); + $stored = new HiddenString($_COOKIE[$name]); + $config = self::getConfig($stored); + $decrypted = Crypto::decrypt( + $stored, + $this->key, + $config->ENCODING + )->getString(); if (empty($decrypted)) { return null; } @@ -64,6 +71,33 @@ public function fetch(string $name) return null; } } + + /** + * Get the configuration for this version of halite + * + * @param HiddenString $stored A stored password hash + * @return SymmetricConfig + * @throws InvalidMessage + */ + protected static function getConfig(HiddenString $stored): SymmetricConfig + { + $stored = $stored->getString(); + $length = Util::safeStrlen($stored); + // This doesn't even have a header. + if ($length < 8) { + throw new InvalidMessage( + 'Encrypted password hash is way too short.' + ); + } + if (\hash_equals(Util::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { + return SymmetricConfig::getConfig( + Base64UrlSafe::decode($stored), + 'encrypt' + ); + } + $v = \Sodium\hex2bin(Util::safeSubstr($stored, 0, 8)); + return SymmetricConfig::getConfig($v, 'encrypt'); + } /** * Store a value in an encrypted cookie diff --git a/src/Password.php b/src/Password.php index e874e95..ca309f5 100644 --- a/src/Password.php +++ b/src/Password.php @@ -23,13 +23,13 @@ final class Password * Hash then encrypt a password * * @param HiddenString $password The user's password - * @param EncryptionKey $secret_key The master key for all passwords + * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @return HiddenString An encrypted hash to store */ public static function hash( HiddenString $password, - EncryptionKey $secret_key, + EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE ): HiddenString { $kdfLimits = KeyFactory::getSecurityLevels($level); @@ -43,7 +43,7 @@ public static function hash( // Now let's encrypt the result return Crypto::encrypt( new HiddenString($hashed), - $secret_key + $secretKey ); } @@ -51,7 +51,7 @@ public static function hash( * Is this password hash stale? * * @param HiddenString $stored Encrypted password hash - * @param EncryptionKey $secret_key The master key for all passwords + * @param EncryptionKey $secretKey The master key for all passwords * @param string $level The security level for this password * @return bool Do we need to regenerate the hash or * ciphertext? @@ -59,21 +59,20 @@ public static function hash( */ public static function needsRehash( HiddenString $stored, - EncryptionKey $secret_key, + EncryptionKey $secretKey, string $level = KeyFactory::INTERACTIVE ): bool { $config = self::getConfig($stored); - $v = \Sodium\hex2bin(Util::safeSubstr($stored->getString(), 0, 8)); - if (!\hash_equals(Halite::HALITE_VERSION, $v)) { - // Outdated version of the library; Always rehash without decrypting - return true; - } if (Util::safeStrlen($stored->getString()) < ($config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3)) { throw new InvalidMessage('Encrypted password hash is too short.'); } // First let's decrypt the hash - $hash_str = Crypto::decrypt($stored, $secret_key)->getString(); + $hash_str = Crypto::decrypt( + $stored, + $secretKey, + $config->ENCODING + )->getString(); // Upon successful decryption, verify that we're using Argon2i if (!\hash_equals( @@ -122,7 +121,7 @@ protected static function getConfig(HiddenString $stored): SymmetricConfig 'Encrypted password hash is way too short.' ); } - if (Util::safeSubstr($stored, 0, 5) === Halite::VERSION_PREFIX) { + if (\hash_equals(Util::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { return SymmetricConfig::getConfig( Base64UrlSafe::decode($stored), 'encrypt' @@ -137,14 +136,14 @@ protected static function getConfig(HiddenString $stored): SymmetricConfig * * @param HiddenString $password The user's password * @param HiddenString $stored The encrypted password hash - * @param EncryptionKey $secret_key The master key for all passwords + * @param EncryptionKey $secretKey The master key for all passwords * @return bool Is this password valid? * @throws InvalidMessage */ public static function verify( HiddenString $password, HiddenString $stored, - EncryptionKey $secret_key + EncryptionKey $secretKey ): bool { $config = self::getConfig($stored); // Hex-encoded, so the minimum ciphertext length is double: @@ -152,7 +151,7 @@ public static function verify( throw new InvalidMessage('Encrypted password hash is too short.'); } // First let's decrypt the hash - $hash_str = Crypto::decrypt($stored, $secret_key); + $hash_str = Crypto::decrypt($stored, $secretKey); // Upon successful decryption, verify the password is correct return \Sodium\crypto_pwhash_str_verify( $hash_str->getString(), diff --git a/src/Symmetric/Config.php b/src/Symmetric/Config.php index 5c719dc..2d2d30d 100644 --- a/src/Symmetric/Config.php +++ b/src/Symmetric/Config.php @@ -68,6 +68,7 @@ public static function getConfigEncrypt(int $major, int $minor): array case 1: case 0: return [ + 'ENCODING' => Halite::ENCODE_HEX, 'SHORTEST_CIPHERTEXT_LENGTH' => 124, 'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, @@ -81,6 +82,7 @@ public static function getConfigEncrypt(int $major, int $minor): array switch ($minor) { case 0: return [ + 'ENCODING' => Halite::ENCODE_BASE64URLSAFE, 'SHORTEST_CIPHERTEXT_LENGTH' => 124, 'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES, 'HKDF_SALT_LEN' => 32, diff --git a/src/Symmetric/Crypto.php b/src/Symmetric/Crypto.php index 9edc8ec..7a34cd9 100644 --- a/src/Symmetric/Crypto.php +++ b/src/Symmetric/Crypto.php @@ -82,7 +82,7 @@ public static function decrypt( // Split our keys list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); - + // Check the MAC first if (!self::verifyMAC( $auth,