From f57277aa96ce91a0e6761b04937447b9a2a9a679 Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Mon, 20 Nov 2017 20:17:06 +0100 Subject: [PATCH] Introduce PasswordHelper for safer passwords refs #2954 --- .../Icinga/Authentication/PasswordHelper.php | 109 ++++++++++++++++++ .../Authentication/PasswordHelperTest.php | 92 +++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 library/Icinga/Authentication/PasswordHelper.php create mode 100644 test/php/library/Icinga/Authentication/PasswordHelperTest.php diff --git a/library/Icinga/Authentication/PasswordHelper.php b/library/Icinga/Authentication/PasswordHelper.php new file mode 100644 index 000000000..0f5448b70 --- /dev/null +++ b/library/Icinga/Authentication/PasswordHelper.php @@ -0,0 +1,109 @@ +='); + } + + /** + * Hash a password with password_hash() or crypt() + * + * @param string $password + * @param int $algo + * + * @return string + * @throws AuthenticationException + */ + public static function hash($password, $algo = null) + { + if (static::supportsModernAPI() and $algo !== self::PASSWORD_ALGO_FALLBACK) { + if ($algo === null) { + $algo = PASSWORD_DEFAULT; + } + $p = password_hash($password, $algo); + if ($p === false) { + throw new AuthenticationException('Could not hash password, password_hash() returned false!'); + } + } else { + $p = crypt($password, self::COMPAT_HASH . static::generateSalt()); + if (strlen($p) < 13) { + throw new AuthenticationException('Hash generated by crypt() seems too small, this suggests an error!'); + } + } + + return $p; + } + + /** + * Verify a password with either password_verify() or crypt() + * + * @param string $password + * @param string $hash + * + * @return bool + */ + public static function verify($password, $hash) + { + if (static::supportsModernAPI()) { + return password_verify($password, $hash); + } else { + return crypt($password, $hash) === $hash; + } + } + + /** + * Shorthand to generate a salt to use with crypt() + * + * @return string + */ + public static function generateSalt() + { + return bin2hex(openssl_random_pseudo_bytes(self::COMPAT_SALT_LENGTH / 2)); + } +} diff --git a/test/php/library/Icinga/Authentication/PasswordHelperTest.php b/test/php/library/Icinga/Authentication/PasswordHelperTest.php new file mode 100644 index 000000000..7c97edda3 --- /dev/null +++ b/test/php/library/Icinga/Authentication/PasswordHelperTest.php @@ -0,0 +1,92 @@ +assertRegExp( + '~^[a-f0-9]{16}$~i', + PasswordHelper::generateSalt(), + 'A hex based salt with 16 chars must be returned' + ); + } + + public function testHash() + { + foreach (array(self::TEST_PASSWORD, self::TEST_PASSWORD_LONG) as $pw) { + $hashed = PasswordHelper::hash($pw); + + $this->assertRegExp( + '~^\$\d\w*\$(?:rounds=\d+\$)?~', + $hashed, + 'Hash output must look like a hash: ' . $hashed + ); + + $this->assertEquals( + crypt($pw, $hashed), + $hashed, + 'New hashed password must validate via crypt: ' . $hashed + ); + } + } + + public function testHashFallback() + { + $hashed = PasswordHelper::hash(self::TEST_PASSWORD, PasswordHelper::PASSWORD_ALGO_FALLBACK); + + $this->assertRegExp( + '~^\$6\$rounds=\d+\$?~', + $hashed, + 'Hash output must look like a SHA-512 hash: ' . $hashed + ); + + $this->assertEquals( + crypt(self::TEST_PASSWORD, $hashed), + $hashed, + 'New hashed password must validate via crypt: ' . $hashed + ); + } + + public function testVerify() + { + $pws = array( + self::TEST_PASSWORD_HASHED_BLOWFISH_1 => self::TEST_PASSWORD, + self::TEST_PASSWORD_HASHED_BLOWFISH_2 => self::TEST_PASSWORD, + self::TEST_PASSWORD_HASHED_BLOWFISH_LONG => self::TEST_PASSWORD_LONG, + self::TEST_PASSWORD_HASHED_SHA256 => self::TEST_PASSWORD, + pack('H*', self::TEST_PASSWORD_OLD_MD5) => self::TEST_PASSWORD, + ); + + foreach ($pws as $hash => $pw) { + $this->assertTrue( + PasswordHelper::verify($pw, $hash), + 'Password must be validated against its hash' + ); + } + } +}