From 9557bb230248b0960cc760ef40365674f566567c Mon Sep 17 00:00:00 2001 From: Dominique Fournier Date: Sun, 3 May 2020 19:10:53 +0000 Subject: [PATCH] Password : manage all the existing PHP hash types. Allow more salt methods. Add more OOP with the list of the allowed hashes git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5978 bf3deb0d-5f1a-0410-827f-c0cc1f45334c --- Tests/passwordTest.php | 181 ++++++++++++++++++++++++++++++++- password.php | 225 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 385 insertions(+), 21 deletions(-) diff --git a/Tests/passwordTest.php b/Tests/passwordTest.php index 10a4161..1f16bfb 100644 --- a/Tests/passwordTest.php +++ b/Tests/passwordTest.php @@ -6,7 +6,7 @@ class passwordTest extends PHPUnit_Framework_TestCase public function test_cryptPasswd_1 () { $res = \password::cryptPasswd ("AAA"); - $this->assertSame (substr ($res, 0, 4), "$2y$"); + $this->assertSame ($res[0] == "$" && strlen ($res) > 8, true); } public function test_cryptPasswd_2 () @@ -22,6 +22,101 @@ class passwordTest extends PHPUnit_Framework_TestCase // Three passwords : each must have a different result } + public function test_cryptPasswd_3 () + { + $this->expectException (); + $res = \password::cryptPasswd (false); + } + + public function test_cryptPassword_MYSQL () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "MYSQL"); + $this->assertSame ($res, "*5AF9D0EA5F6406FB0EDD0507F81C1D5CEBE8AC9C"); + } + + public function test_cryptPassword_CRYPT_STD_DES () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_STD_DES"); + $this->assertSame (strlen ($res), 13); + } + + public function test_cryptPassword_CRYPT_EXT_DES () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_EXT_DES"); + $this->assertSame (strlen ($res), 13); + } + + public function test_cryptPassword_CRYPT_MD5 () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_MD5"); + $this->assertSame (substr ($res, 0, 3) === "$1$" && strlen ($res) === 34, + true); + } + + public function test_cryptPassword_CRYPT_BLOWFISH () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_BLOWFISH"); + $this->assertSame (substr ($res, 0, 4) === "$2y$" && strlen ($res) === 24, + true); + } + + public function test_cryptPassword_CRYPT_SHA256 () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_SHA256"); + $this->assertSame ( + substr ($res, 0, 10) === "$5\$rounds=" && strlen ($res) === 75, + true); + } + + public function test_cryptPassword_CRYPT_SHA512 () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "CRYPT_SHA512"); + $this->assertSame ( + substr ($res, 0, 10) === "$6\$rounds=" && strlen ($res) === 118, + true); + } + + public function test_cryptPassword_PASSWORD_BCRYPT () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "PASSWORD_BCRYPT"); + $this->assertSame ( + substr ($res, 0, 7) === "$2y\$10\$" && strlen ($res) === 60, + true); + } + + public function test_cryptPassword_PASSWORD_ARGON2I () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "PASSWORD_ARGON2I"); + $this->assertSame ( + substr ($res, 0, 11) === "\$argon2i\$v=" && strlen ($res) === 96, + true); + } + + public function test_cryptPassword_PASSWORD_ARGON2ID () + { + $password = new password (); + $res = $password->cryptPassword ("AAA", "PASSWORD_ARGON2ID"); + $this->assertSame ( + substr ($res, 0, 12) === "\$argon2id\$v=" && strlen ($res) === 97, + true); + } + + public function test_cryptPassword_UNKNOWN () + { + $this->expectException (); + $password = new password (); + $res = $password->cryptPassword ("AAA", "UNKNOWN"); + } + public function test_checkPassword_1 () { $res = \password::checkPassword ("AAA", "AAA"); @@ -30,7 +125,8 @@ class passwordTest extends PHPUnit_Framework_TestCase public function test_checkPassword_2 () { - $res = \password::checkPassword ("AAA", \password::cryptPasswd ("AAA")); + $crypt = \password::cryptPasswd ("AAA"); + $res = \password::checkPassword ("AAA", $crypt); $this->assertSame ($res, true); } @@ -46,4 +142,85 @@ class passwordTest extends PHPUnit_Framework_TestCase '$2y$11$Y.E98jbjgDpV61eK..9MT.klzTeg7ulO4WH/B5yA8cAGMIh.zoNXq'); $this->assertSame ($res, true); } + + public function test_checkPassword_invalid1 () + { + $this->expectException (); + $res = \password::checkPassword (false, + '$2y$11$Y.E98jbjgDpV61eK..9MT.klzTeg7ulO4WH/B5yA8cAGMIh.zoNXq'); + } + + public function test_checkPassword_invalid2 () + { + $this->expectException (); + $res = \password::checkPassword ("AAA", false); + } + + public function test_generateASCII_defaultsize () + { + $res = \password::generateASCII (); + $this->assertSame (strlen ($res), 12); + } + + public function test_generateASCII_toobig () + { + $this->expectException (); + $res = \password::generateASCII (112); + } + + public function test_generateASCII_toosmall () + { + $this->expectException (); + $res = \password::generateASCII (0); + } + + public function test_generateAlphanum_defaultsize () + { + $res = \password::generateAlphanum (); + $this->assertSame (strlen ($res), 12); + } + + public function test_generateAlphanum_toobig () + { + $this->expectException (); + $res = \password::generateAlphanum (112); + } + + public function test_generateAlphanum_toosmall () + { + $this->expectException (); + $res = \password::generateAlphanum (0); + } + + public function test_generateAlphabetical_defaultsize () + { + $res = \password::generateAlphabetical (); + $this->assertSame (strlen ($res), 12); + } + + public function test_generateAlphabetical_toobig () + { + $this->expectException (); + $res = \password::generateAlphabetical (112); + } + + public function test_generateAlphabetical_toosmall () + { + $this->expectException (); + $res = \password::generateAlphabetical (0); + } + + public function test_listMethods_1 () + { + $password = new password (); + $res = $password->listMethods(); + $this->assertSame (count ($res) > 7, true); + } + + public function test_salt_1 () + { + $password = new password (); + $res = $password->salt(); + $this->assertSame (strlen ($res) > 20, true); + } } diff --git a/password.php b/password.php index 129d753..0c57065 100644 --- a/password.php +++ b/password.php @@ -1,32 +1,133 @@ array ( + "hash" => "", "size" => "", "pre" => "", "post" => ""), + "CRYPT_STD_DES" => array ( + "hash" => CRYPT_STD_DES, "size" => 2, "pre" => "", "post" => "" ), + "CRYPT_EXT_DES" => array ( + "hash" => CRYPT_EXT_DES, "size" => 9, "pre" => "", "post" => "" ), + "CRYPT_MD5" => array ( + "hash" => CRYPT_MD5, "size" => 12, "pre" => "$1$", "post" => "$" ), + "CRYPT_BLOWFISH" => array ( + "hash" => CRYPT_BLOWFISH, "size" => 16, "pre" => "$2y$11$", + "post" => "$" ), + "CRYPT_SHA256" => array ( + "hash" => CRYPT_SHA256, "size" => 16, "pre" => "$5\$rounds=5000$", + "post" => "$" ), + "CRYPT_SHA512" => array ( + "hash" => CRYPT_SHA512, "size" => 16, "pre" => "$6\$rounds=5000$", + "post" => "$" ), + "PASSWORD_BCRYPT" => array ( + "hash" => PASSWORD_BCRYPT, "size" => "", "pre" => "", "post" => ""), + "PASSWORD_ARGON2I" => array ( + "hash" => PASSWORD_ARGON2I, "size" => "", "pre" => "", "post" => ""), + "PASSWORD_ARGON2ID" => array ( + "hash" => PASSWORD_ARGON2ID, "size" => "", "pre" => "", "post" => ""), + ); + // }}} + + ///////////////// + // METHODS // + ///////////////// + /** List all the allowed password encrytion methods, sorted from weak to + * strong encryption + * @return array ("PASSWORD_ARGON2ID" => PASSWORD_ARGON2ID); + */ + public function listMethods () + // {{{ + { + $res = array (); + foreach ($this->methods as $key => $params) + { + if ($key !== "MYSQL" && (! defined ($key) || $params["hash"] === 0)) + continue; + if (substr ($key, 0, 9) === "PASSWORD_" && + ! function_exists ("password_hash")) + continue; + $res[$key] = $params["hash"]; + } + return $res; + } + // }}} + + /** Create a salt, based on openssl_random_pseudo_bytes function + * @return a string salt + */ + public function salt () + // {{{ + { + if (function_exists ("openssl_random_pseudo_bytes")) + $salt = substr (base64_encode (openssl_random_pseudo_bytes (17)), 0, 22); + elseif (function_exists ("random_bytes")) + $salt = substr (base64_encode(random_bytes (22)), 0, 22); + else + throw new \Exception (dgettext ("domframework", + "Password : no PHP support for random numbers (OpenSSL...)"), 500); + $salt = str_replace ("+",".",$salt); + return $salt; + } + // }}} + + /** Crypt the provided password with the wanted crypt method + * @param string $password The password to crypt + * @param string|null $method The method to use. If null, use the strongest + * one available + * @return string The hashed password + */ + public function cryptPassword ($password, $method = null) + // {{{ + { + if (! is_string ($password) && ! is_integer ($password)) + throw new \Exception (dgettext ("domframework", + "Invalid clear password provided to be crypted : not a string"), 403); + $methods = $this->listMethods (); + if ($method === null) + { + end ($methods); + $method = key ($methods); + } + if (! key_exists ($method, $methods)) + throw new \Exception (sprintf (dgettext ("domframework", + "Password : cryptPassword method not allowed : %s"), $method), 500); + $params = $this->methods[$method]; + if (substr ($method, 0, 9) === "PASSWORD_") + return password_hash ($password, $params["hash"]); + if ($method === "MYSQL") + return "*" . strtoupper (sha1 (sha1 ($password, true))); + if (substr ($method, 0, 6) === "CRYPT_") + { + $salt = $this->salt (); + $tmpSalt = $params["pre"].substr ($salt, 0, $params["size"]). + $params["post"]; + return crypt ($password, $tmpSalt); + } + // Will never match here + throw new \Exception (sprintf (dgettext ("domframework", + "Password : Unknown method to crypt requested : %s"), $method), 500); + } + // }}} + /** Crypt the password with the best algorithm available * @param string $password The password to crypt * @return string The hashed password */ static public function cryptPasswd ($password) + // {{{ { - if (! function_exists ("openssl_random_pseudo_bytes")) - throw new \Exception (dgettext ("domframework", - "No PHP support for openssl_random_pseudo_bytes"), - 500); - if (! is_string ($password) && ! is_integer ($password)) - throw new \Exception (dgettext ("domframework", - "Invalid clear password provided to be crypted : not a string"), 403); - $cost = 11; - $salt = substr (base64_encode (openssl_random_pseudo_bytes (17)), 0, 22); - $salt = str_replace ("+", ".", $salt); - $param = '$'.implode ('$', array ( - "2y", //select the most secure version of blowfish (>=PHP 5.3.7) - str_pad ($cost, 2, "0", STR_PAD_LEFT), //add the cost in two digits - $salt //add the salt - )); - //now do the actual hashing - return crypt ($password, $param); + $passwd = new password (); + return $passwd->cryptPassword ($password); } + // }}} /** Check if the clear password is valid against the hashed one * @param string $clear The clear password @@ -34,15 +135,101 @@ class password * @return boolean true if the password correspond to the hash */ static public function checkPassword ($clear, $hashed) + // {{{ { if (! is_string ($clear)) throw new \Exception (dgettext ("domframework", "Invalid clear password provided to be checked : not a string"), 403); - if (! is_string ($clear)) + if (! is_string ($hashed)) throw new \Exception (dgettext ("domframework", "Invalid hashed password provided to be checked : not a string"), 403); + if (function_exists ("password_verify")) + return password_verify ($clear, $hashed); + // Crypt do not work with Argon2. But If Argon2 exists, password_verify + // should exists... if (crypt ($clear, $hashed) === $hashed) return true; return false; } + // }}} + + /** Create a random password with $nbChars chars, ASCII chars (with special + * chars). + * A maximum of 20% for special chars in the size + * @param integer|null $nbChar The number of chars (12 by default) + * @return The random password + */ + static public function generateASCII ($nbChars = 12) + // {{{ + { + if (! is_int ($nbChars) || $nbChars < 1) + throw new \Exception (dgettext ("domframework", + "Password : generateASCII : invalid nbChars provided : not an int or ". + "negative or null")); + if ($nbChars > 72) + throw new \Exception (dgettext ("domframework", + "Password : generateASCII : Can't generate more than 72 chars")); + $password = array (); + $chars = array_merge (range (97, 122), range (65, 90), range (48, 57)); + $special = range (33, 47); + $rand1 = array_rand ($chars, floor ($nbChars * 0.80)); + foreach ($rand1 as $chr) + $password[] = chr($chars[$chr]); + $rand2 = array_rand ($special, ceil ($nbChars * 0.2)); + foreach ($rand2 as $chr) + $password[] = chr($special[$chr]); + shuffle ($password); + return implode ($password); + } + // }}} + + /** Create a random password with $nbChars chars, Alphanumericals chars + * (without special chars) + * @param integer|null $nbChar The number of chars (12 by default) + * @return The random password + */ + static public function generateAlphanum ($nbChars = 12) + // {{{ + { + if (! is_int ($nbChars) || $nbChars < 1) + throw new \Exception (dgettext ("domframework", + "Password : generateAlphanum : invalid nbChars provided : not an int or ". + "negative or null")); + if ($nbChars > 72) + throw new \Exception (dgettext ("domframework", + "Password : generateASCII : Can't generate more than 72 chars")); + $password = array (); + $chars = array_merge (range (97, 122), range (65, 90), range (48, 57)); + $rand1 = array_rand ($chars, $nbChars); + foreach ($rand1 as $chr) + $password[] = chr($chars[$chr]); + shuffle ($password); + return implode ($password); + } + // }}} + + /** Create a random password with $nbChars chars, Alphabeticals chars + * (without special chars, neither numbers) + * @param integer|null $nbChar The number of chars (12 by default) + * @return The random password + */ + static public function generateAlphabetical ($nbChars = 12) + // {{{ + { + if (! is_int ($nbChars) || $nbChars < 1) + throw new \Exception (dgettext ("domframework", + "Password : generateAlphanum : invalid nbChars provided : not an int or ". + "negative or null")); + if ($nbChars > 72) + throw new \Exception (dgettext ("domframework", + "Password : generateASCII : Can't generate more than 72 chars")); + $password = array (); + $chars = array_merge (range (97, 122), range (65, 90)); + $rand1 = array_rand ($chars, $nbChars); + foreach ($rand1 as $chr) + $password[] = chr($chars[$chr]); + shuffle ($password); + return implode ($password); + } + // }}} }