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
This commit is contained in:
2020-05-03 19:10:53 +00:00
parent a9d4f3deb3
commit 9557bb2302
2 changed files with 385 additions and 21 deletions

View File

@@ -6,7 +6,7 @@ class passwordTest extends PHPUnit_Framework_TestCase
public function test_cryptPasswd_1 () public function test_cryptPasswd_1 ()
{ {
$res = \password::cryptPasswd ("AAA"); $res = \password::cryptPasswd ("AAA");
$this->assertSame (substr ($res, 0, 4), "$2y$"); $this->assertSame ($res[0] == "$" && strlen ($res) > 8, true);
} }
public function test_cryptPasswd_2 () public function test_cryptPasswd_2 ()
@@ -22,6 +22,101 @@ class passwordTest extends PHPUnit_Framework_TestCase
// Three passwords : each must have a different result // 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 () public function test_checkPassword_1 ()
{ {
$res = \password::checkPassword ("AAA", "AAA"); $res = \password::checkPassword ("AAA", "AAA");
@@ -30,7 +125,8 @@ class passwordTest extends PHPUnit_Framework_TestCase
public function test_checkPassword_2 () 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); $this->assertSame ($res, true);
} }
@@ -46,4 +142,85 @@ class passwordTest extends PHPUnit_Framework_TestCase
'$2y$11$Y.E98jbjgDpV61eK..9MT.klzTeg7ulO4WH/B5yA8cAGMIh.zoNXq'); '$2y$11$Y.E98jbjgDpV61eK..9MT.klzTeg7ulO4WH/B5yA8cAGMIh.zoNXq');
$this->assertSame ($res, true); $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);
}
} }

View File

@@ -1,32 +1,133 @@
<?php <?php
/** A class to manage the password hashing /** A class to manage the password hashing, password generation
*/ */
class password class password
{ {
////////////////////
// PROPERTIES //
////////////////////
/** List all the allowed hashing methods, sort from weak to strong encryption
*/
private $methods = array (
// {{{
"MYSQL" => 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 /** Crypt the password with the best algorithm available
* @param string $password The password to crypt * @param string $password The password to crypt
* @return string The hashed password * @return string The hashed password
*/ */
static public function cryptPasswd ($password) static public function cryptPasswd ($password)
// {{{
{ {
if (! function_exists ("openssl_random_pseudo_bytes")) $passwd = new password ();
throw new \Exception (dgettext ("domframework", return $passwd->cryptPassword ($password);
"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);
} }
// }}}
/** Check if the clear password is valid against the hashed one /** Check if the clear password is valid against the hashed one
* @param string $clear The clear password * @param string $clear The clear password
@@ -34,15 +135,101 @@ class password
* @return boolean true if the password correspond to the hash * @return boolean true if the password correspond to the hash
*/ */
static public function checkPassword ($clear, $hashed) static public function checkPassword ($clear, $hashed)
// {{{
{ {
if (! is_string ($clear)) if (! is_string ($clear))
throw new \Exception (dgettext ("domframework", throw new \Exception (dgettext ("domframework",
"Invalid clear password provided to be checked : not a string"), 403); "Invalid clear password provided to be checked : not a string"), 403);
if (! is_string ($clear)) if (! is_string ($hashed))
throw new \Exception (dgettext ("domframework", throw new \Exception (dgettext ("domframework",
"Invalid hashed password provided to be checked : not a string"), 403); "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) if (crypt ($clear, $hashed) === $hashed)
return true; return true;
return false; 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);
}
// }}}
} }