I'm currently doing a very safe login system, but I'm new to the crypt() function and need some quick assistance.
I used crypt() to encrypt the password string during signup and saved it to the database. However, how will I be able to decrypt the key during login? Or how am I supposed to do otherwise? Or would it be possibly to do some magic with the submitted password string to compare it to the encrypted key in the database?
crypt() doesn't encrypt passwords, it hashes them. The fundamental difference is, you can't get hashed passwords back (think of hash browns - if you have hash browns, you can't get the potatoes back).
So you apply the same function to the input and compare its result to the value stored in the database:
$stored_pw = get_hashed_password_from_db($_POST['username']);
crypt($_POST['password'], $stored_pw) == $stored_pw
Read the documentation on crypt() to understand the "magic" behind the code above works.
Do not encrypt the password. Instead, store it with a hash.
Popular SO thread: How should I ethically approach user password storage for later plaintext retrieval?
crypt() the provided password at login. Compare the output to the previous crypt()'s output. If they match, the passwords match.
This is the basic theory of operation of a one-way hash function.
Also this is how you should be doing it. Please note that this code is how I'd do it, and you may want to change a few of the things. And you must define your own unique salt, whether in the config file or elsehwere. It must either a) be in the global scope as I've posted, or you can change it to make it defined in the function. Also you're not encrypting, you're actually hashing. Encryption is both ways, hashing is encryption in one way. Meaning you cannot decrypt a hash. You can only bruteforce guess the original plain text.
/*
* Copyright (c) 2012, Macarthur Inbody
* The following code was posted on http://stackoverflow.com/questions/8195689/encryption-using-crypt
* The license is simply CC-by https://creativecommons.org/licenses/by/3.0/
*
*
*
*/
/*
*
* This is used to hash their password.
*
* #param $password string the users supplied password
* #param $username string the users supplied username
* #param $rand_salt int the secondary salt -2^31-1 to 2^31-1 Must be defined previously.
* #return string the hashed password
*/
function hash_pass($username,$password,$rand_salt){
global $unique_salt;
$main_salt=base64_encode(hash('sha512',$username.$password.$config_salt);
$main_salt=str_replace('+', '.', $salt);
$main_salt=str_replace('=','/',$salt);
$main_salt='$2$06'.$main_salt; //change this here to the cost factor that you want
$hashed=crypt($unique_salt.$username.$password.$rand_salt,$main_salt);
return $hashed;
}
function gen_rand_salt(){
return rand();
}
function rand_str($length,$additional_entropy){
$max_length=ceil($length/28);
if(!is_defined($additional_entropy)){
$additional_entropy='';
}
$str='';
for($i=0;$i<=$max_length;++$i){
$str.=base64_encode(sha1($i.''.microtime().$additional_entropy,true));
}
$str=substr($str,0,$length);
return $str;
}
/*
*
* Generate A temp password/token
*
* This function generates a temporary password and also gives you
* the hashed password too. It is an array, arr[0]=password, arr[1]=
* hashed password. If it fails it'll return -1;
*
* #param $username the username
* #param $rand_salt the random salt value, must be given.
*
* #return array if it is successful array, if it fails it's a number of -1
*/
function generate_temp_password($username,$rand_salt){
global $unique_salt;
if(!is_defined($rand_salt)){
return -1;
}
$pass_len=12; // change this to what you want for password recovery
$pass_arr=Array();
$password=rand_str($pass_len,$unique_salt.rand().$rand_salt);
$password=substr(base64_encode(sha1($rand_str.$rand_salt,true)),0,$pass_len);
$hashed_password=hash_pass($username,$password,$rand_salt);
$pass_arr[0]=$password;
$pass_arr[1]=$hashed_password;
return $pass_arr;
}
As stated in teh code, the license is CC-By as I figure it's good enough for most things. Also please keep the block the same as far as the link to this page as that's what I do with all of my own source code. Also I realize the "random" string isn't really that random, but it's random enough to be useable by you for the purpose that it is going to be.
Edit 2:Also be sure to escape the user's username. I'm not escaping the password, since I'm hashing it, thus escaping it isn't necessary since it's already mitigated and would just waste cycles. But only if you're doing something like this. Make sure to escape the username with mysql_real_escape_string. If you're using php5+ you should look into mysqli(if you're using mysql). If you're using another system, then you'll have to look it up yourself as I only know mysql. I'm going to be away for a couple of days, so I really hope this works for you. I will check it from time to time, but I may forget... so yeah. I hope this helps you, as it's safe, secure, and should work fine for you.
Edit 3: Changed the random string function to make it a bit stronger since I forgot that this is going to be use to generate temporary passwords. That should make it random enough to be used for this purpose since otherwise the generated password might be able to be known by someone knowing the exact time(with the current microtime) though highly unlikely, this still makes it a bit stronger and should make it safe from those kinds of attacks. It should not be completely production ready, and should be secure for your system. Just make sure to set the $unique_salt variable somwhere in the global scope, or set it in every time that it's used in each of those functions.
See
http://php.net/manual/en/function.password-hash.php
and
http://php.net/manual/en/function.password-verify.php
Also never use rand() if you need safe random values. It's the worst source for random values in PHP.
In PHP 7 you should use
http://php.net/manual/en/function.random-bytes.php
instead. For earlier versions see
http://php.net/manual/en/function.openssl-random-pseudo-bytes.php
Related
Does anyone have an idea about how the password_verify() function works? I've searched everywhere in the net about the said function , but I never found a specific answer on how it compares its two parameters. Below is the proper syntax of the said function according to php.net:
bool password_verify ( string $password , string `$hash` )
The question is, does the function hashes the $password then compared it to $hash? or
It dehashed $hash instead then compare it with $password?
Since the entire point of a hash function is that it cannot be reversed, password_verify can't be using option 2.
That leaves option 1.
You can also look at the source code where you can see that …
zend_string *ret = php_crypt(ZSTR_VAL(password), (int)ZSTR_LEN(password), ZSTR_VAL(hash), (int)ZSTR_LEN(hash), 1);
… it crypts the password and then …
/* We're using this method instead of == in order to provide
* resistance towards timing attacks. This is a constant time
* equality check that will always check every byte of both
* values. */
for (i = 0; i < ZSTR_LEN(hash); i++) {
status |= (ZSTR_VAL(ret)[i] ^ ZSTR_VAL(hash)[i]);
}
… compares the hashed password (ret) with the hashed value passed in (hash)
There's no such thing as "dehashing". A hash is a one way function.
What password_verify actually does is, reading the salt and hashing function from the "hash", given by password_hash and then do exactly the same hashing again, with the given parameters.
Because of that, it's important to use password_verify, instead of just do something like $hash == password_hash('...'), as password_hash could use anothere hashing algorithm and creates each time a new random salt value. So calling password_hash with the same input multiple times on the same machine, would never return the same value.
I'm using md5(uniqid()) to generate a unique hash for my token hidden input on my forms (like Log in, Sign up, Settings, etc) for my File Sharing and Hosting service and for the user session, so I can compare those two after the form is submitted.
But I'm wondering if md5(uniqid()) is good enough after I've read that md5 has lots of security flaws.
Are there better or more secure ways of generating tokens for my forms?
Output example
<input type="hidden" name="token" value="4c1dd84d3458964ee6d59c728dc70160">
This token should just be an unpredictable code. The best you can do to get such an unpredictable code with a deterministic computer, is to generate a really random number.
When you use the MD5 function with your uniqid, it does not add any randomness/unpredictability to your token, you (mis)use it as an encoder. The same goal you get with using the bin2hex() function, that's what MD5 does by default after calculating the binary hash. That said, the MD5 function is not unsafe here but has no advantage neither.
The more important point is, that the function uniqid() is not unpredictable, it is based on the current timestamp. This is the unsafe part in your code. To get an unpredictable number you can use the function mcrypt_create_iv() which reads from the random source of the operating system.
I would recommend to let PHP create the session token for you, with the session_start() function. If you really have reasons not to use a normal PHP session, then use mcrypt_create_iv() together with an encoding function like bin2hex() or base64_encode().
EDIT:
From your comments i see that this token is not used to maintain the session, instead to mitigate csrf. In this case of course the session_start function won't help (the session id should not be used as csrf token), but creating an unpredictable token is still important. This is an example of how this can be done:
/**
* Generates a random string of a given length, using the random source of
* the operating system. The string contains only characters of this
* alphabet: +/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
* #param int $length Number of characters the string should have.
* #return string A random base64 encoded string.
*/
protected static function generateRandomBase64String($length)
{
if (!defined('MCRYPT_DEV_URANDOM')) throw new Exception('The MCRYPT_DEV_URANDOM source is required (PHP 5.3).');
// Generate random bytes, using the operating system's random source.
// Since PHP 5.3 this also uses the random source on a Windows server.
// Unlike /dev/random, the /dev/urandom does not block the server, if
// there is not enough entropy available.
$binaryLength = (int)($length * 3 / 4 + 1);
$randomBinaryString = mcrypt_create_iv($binaryLength, MCRYPT_DEV_URANDOM);
$randomBase64String = base64_encode($randomBinaryString);
return substr($randomBase64String, 0, $length);
}
There was a great answer to the first half of this on
https://security.stackexchange.com/a/19710 , the second half god a good answer from the manpage, as posted by rmcfrazier: http://www.php.net/manual/en/function.uniqid.php
quote combinatorics:
"There are devastating collision attacks on MD5. (...) In contrast, SHA1 appears to be much more secure. While there are some known attacks on SHA1, they are much less serious than the attacks on MD5. For this reason, SHA1 is a much better choice than MD5 in many settings." <- MD5
"Warning: This function does not create random nor unpredictable strings. This function must not be used for security purposes. Use a cryptographically secure random function/generator and cryptographically secure hash functions to create unpredictable secure IDs." <- uniqid
Per the man page, this should not be used for cryptographically secure tokens
http://www.php.net/manual/en/function.uniqid.php
You should use openssl_random_pseudo_bytes for your tokens
http://www.php.net/manual/en/function.openssl-random-pseudo-bytes.php
It really depends on how secure you need the token to be. Using the PHP session ID should suffice unless you need to verify the integrity of the key after submission.
Eg. PHP session id:
php > session_start();
php > echo session_id();
ritig5ecgp6ebmnq8p5imbdhl3
However, you can always generate more secure IDs using the hash() function eg:
http://us2.php.net/manual/en/function.hash.php
Don't use RipeMD
echo hash('ripemd160', 'The quick brown fox jumped over the lazy dog.');
ec457d0a974c48d5685a7efa03d137dc8bbde7e3
Unless the token needs to be cryptographically secure, the session key should suffice.
I am keen to migrate my code to the new password_* functions provided natively by PHP.
The existing hashes in the database have been generated as follows:
hash ('sha512', '<A constant defined earlier>' . $email . $password);
I'd like to move these to be hashes created by the now-recommended:
password_hash ($password, PASSWORD_DEFAULT);
Obviously, when a user logs in, I can take the opportunity to create the new hash from the password they just provided, and save that in the database.
However, I'd like to avoid having to have two fields in the database, namely one for the deprecated hash and one for the modern password_hash one. Instead, I'd rather just replace the old ones as each user logs in.
Therefore, is it possible to keep a single database field, and have the userland code determine whether the hash is old, i.e. determine which check to use?
(I'm assuming that the hash('sha512') hashes cannot be automatically upgraded to crypt() ones?)
Hashes created with password_hash will have a very distinctive $2y$ string at the beginning (or similar $..$, as long as you're operating with the current default Blowfish cypher), while SHA256 will simply be all hex values. Therefore, you can simply test whether a value is a legacy hash value or a password_hash value:
function isLegacyHash($hash) {
return !preg_match('/^\$\w{2}\$/', $hash);
}
Using this, you can keep both types of hashes in a single field and upgrade them when the user logs in. Alternatively, you could simply set a flag in a column like hash_version.
You will have to rehash when a user logs in. But there is a function to check already, see password_needs_rehash.
So when a user logs in, run the check and change the password hash if it needs a rehash.
It will be a little more tricky if you decide to completely migrate to bcrypt at some point. Then you will need to think about what to do with the users who have not had a new hash created.
The following code will return true so you know you need to rehash.
$password = 'test';
$oldHash = hash('sha512',); // get old Hash from DB
if (password_needs_rehash($oldHash, PASSWORD_BCRYPT)) {
$newHash = password_hash($password , PASSWORD_BCRYPT);
// save new Hash to DB (IMPORTANT: only if log in was successful...)
}
I modified my old post. I tried the crypt() function and now trying to work with password_hash() and password_verify() to verify the encrypted password coming from database but on each call, password_hash() function retuns a different encrypted string and password_verify() cannot match it.
This is how I am doing this.
//please ignore the syntax error if any
$data = '11';
$dbpass = password_hash($data, PASSWORD_BCRYPT);
echo $dbpass; // displays the random strings on each page refresh.
Once password is saved into database does not get match during the login process. Below is my actual function.
private function process_data($password){
$password = __STR.$password.__STR;
return password_hash($password, PASSWORD_BCRYPT);
}
private function processed($login_password, $dbpassword){
$login_password = __STR.$login_password.__STR;
return password_verify($login_password, $dbpassword);
}
On each function call for creating a hashed string for password, the function returns the different string next time.
Ok, Let's go through this one by one.
First, it's hashing, not encryption. Encryption is two-way, hashing is one way. We want to hash. We never want to encrypt. Yes, terminology matters. Please use the correct terminology.
Next, each call to password_hash is supposed to return a different hash. That's because it's generating a strong random salt. This is how it was designed, and how you really should be using it.
Further, DO NOT do the "pepper" thing of adding __STR before and after the password. You're doing nothing but potentially weakening the users password (which is not good). If you want more information around why that's a bad idea: Read This Answer.
Continuing, I would highly recommend that you do not use crypt directly. It is actually surprisingly easy to screw up and generate extremely weak hashes. This is why the password_* api was designed. crypt is a low level library, you want to use a high level library in your code. For more information on ways to screw up bcrypt, check out my blog: Seven Ways To Screw Up Bcrypt.
The Password API was designed to be a simple, one-stop shop. If it's not working for you check the following things:
Are you using PHP >= 5.5.0? Or are you using PHP >= 5.3.7 with password_compat?
Is your database column wide enough?
It needs to be at least 60 characters long.
Are you checking that the result of the function is a string, and not bool(false)?
If there is an internal error, it will return a non-string from password_hash.
Are you getting any errors?
Have you turned on error_reporting to its maximum setting (I recommend -1 to catch everything) and checked that the code isn't throwing any errors?
Are you sure you are using it correctly?
function saveUser($username, $password) {
$hash = password_hash($password, PASSWORD_BCRYPT);
// save $username and $hash to db
}
function login($username, $password) {
// fetch $hash from db
return password_verify($password, $hash);
}
Note that each one should be called only once.
Are you using PHP < 5.3.7 with password_compat? If so, this is your problem. You are using the compatability library on an unsupported version of PHP. You may get it to work (certain RedHat distributions have backported the necessary fixes), but you are using an unsupported version. Please upgrade to a reasonable release.
If all else fails, please try running this code and reporting back the output:
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
$test = crypt("password", $hash);
$pass = $test == $hash;
echo "Test for functionality of compat library: " . ($pass ? "Pass" : "Fail");
echo "\n";
If that returns Fail, you are running an unsupported version of PHP and should upgrade. If it returns pass, than the error is somewhere in your logic (the library is functioning fine).
The best way to store passwords is to use PHP's function password_hash(). It automatically generates a cryptographically safe salt for each password and includes it in the resulting 60-character string. You won't have to worry about the salt at all!
// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($password, PASSWORD_BCRYPT);
// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($password, $existingHashFromDb);
Your own scheme is very weak, first you are using MD5 which is ways too fast for generating password hashes, then you use a static salt, which defeats the purpose of a salt. Maybe you want to have a look at my tutorial about safely storing passwords.
Edit to answer updated question:
It is not necessary to add the __STR to the password (if you want to add a pepper there are better ways), but your example functions should actually work. The returned value of password_hash() will be different each time because of the random salt. This is correct, the function password_verify() is able to extract this salt for the verification. In your case the database field is probably the problem. Make sure it can hold a 60 character string.
1) How do you create secure Blowfish hashes of passwords with crypt()?
$hash = crypt('somePassword', '$2a$07$nGYCCmhrzjrgdcxjH$');
1a) What is the significance of "$2a"? Does it just indicate that the Blowfish algorithm should be used?
1b) What is the significance of "$07"? Does a higher value imply a more secure hash?
1c) What is the significance of "$nGYCCmhrzjrgdcxjH$"? Is this the salt that will be used? Should this be randomly generated? Hard-coded?
2) How do you store Blowfish hashes?
echo $hash;
//Output: $2a$07$nGYCCmhrzjrgdcxjH$$$$.xLJMTJxaRa12DnhpAJmKQw.NXXZHgyq
2a) What part of this should be stored in the database?
2b) What data type should be used for the column (MySQL)?
3) How should one verify a login attempt?
You should store the entire output of crypt, there's not a lot of point in splitting it up, because you need to generate a new salt for each password you're hashing in any case. Using a fixed hidden salt as mentioned by Matt is wrong - the salt should be different for every hash.
For more information see http://www.openwall.com/articles/PHP-Users-Passwords - I recommend using the phpass library because it handles generating a random salt for you, unlike crypt().
1a) Strength of encryption - requirement in the range of 4..31. See http://php.net/manual/en/function.crypt.php
1b) See 1a
1c) See 1a. 'salt' should not be random, or you would not be able to regenerate the same hash for a given input - see 3.
2a) Strictly speaking, everything except the hash (in case database is compromised). Also, store your salt in a file not accessible beneath the web server's document root and include it. Set it with the strictest permissions possible; ideally read only to web host service (e.g. apache), no write or execute privileges. Less strictly speaking, depends how defensive you wish to be against hackers. Not storing the salt just makes life more difficult; they still have to get the data being input to the algorithm right - but why make it easier?
2b) VARCHAR(32) should be fine for blowfish, if not storing the hash
3) Assuming you've already run the proper injection prevention code, etc.. so please don't just copy the below blindly (and ideally use PDO instead of mysql extension). The below is specific to blowfish, SHA-256 and SHA-512 which all return the salt within the hash. Would need modification for other algorithms...
//store this in another file outside web directory and include it
$salt = '$2a$07$somevalidbutrandomchars$'
...
//combine username + password to give algorithm more chars to work with
$password_hash = crypt($valid_username . $valid_password, $salt)
//Anything less than 13 chars is a failure (see manual)
if (strlen($password_hash) < 13 || $password_hash == $salt)
then die('Invalid blowfish result');
//Drop the salt from beginning of the hash result.
//Note, irrespective of number of chars provided, algorithm will always
//use the number defined in constant CRYPT_SALT_LENGTH
$trimmed_password_hash = substring($password_hash, CRYPT_SALT_LENGTH);
mysql_query("INSERT INTO `users` (username,p assword_hash) VALUES '$valid_username', '$trimmed_password_hash'");
...
$dbRes = mysql_query("SELECT password_hash FROM `users` WHERE username = '$user_input_username' LIMIT 1");
//re-apply salt to output of database and re-run algorithm testing for match
if (substring($salt, CRYPT_SALT_LENGTH) . mysql_result($dbRes, 0, 'password_hash') ) ===
crypt($user_input_username . $user_input_password, $salt) ) {
//... do stuff for validated user
}