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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
225
password.php
225
password.php
@@ -1,32 +1,133 @@
|
||||
<?php
|
||||
/** A class to manage the password hashing
|
||||
/** A class to manage the password hashing, password generation
|
||||
*/
|
||||
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
|
||||
* @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);
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user