The following code snippet should not emit 'MATCHED' because the password 'testtest' does not match 'testtesttest', but does on PHP 7.4.3 for me. Am I doing something wrong?
<?php
$sPass = 'testtesttest';
$sSalt = hash('sha256','this is my salt');
$sShadow = password_hash($sSalt . $sPass,PASSWORD_BCRYPT);
echo (password_verify($sSalt . 'testtest',$sShadow) ? 'MATCHED' : 'nomatch');
Note, if you remove the salt references above, the code works fine. It's like the password_hash and password_verify functions of PHP have a size limitation where they no longer become accurate if the string is longer than so many characters.
So, I'm thinking this is a bug.
BCrypt can only handle 72 characters, your salt takes up 64 characters, so only 8 characters of your password are considered.
The input to the bcrypt function is the password string (up to 72 bytes), a numeric cost, and a 16-byte (128-bit) salt value.
Use the binary form of your salt to not "waste" as many characters or just don't use one at all, as password_hash will generate one anyway.
Related
Before we begin: Yes I am aware that I should use PHP's password_hash function when actually storing passwords. This is a question about the internals of PHP's hashing system.
So I was messing around with PHP's crypt function the other day, and I noticed some odd behavior with bcrypt.
$password = "totallyagoodpassword";
$salt = "hereisa22charactersalt";
$parameter = '$2y$10$' . $salt;
echo $parameter . PHP_EOL;
echo crypt($password, $parameter);
According to PHP's manual, this code should hash "totallyagoodpassword" using bcrypt, salting it with "hereisa22charactersalt." The output of this hash should be the scheme ("$2y$10$"), followed by the 22 characters of the salt, then followed by 31 characters of hash. Therefore, I should expect "$2y$10$hereisa22charactersalt" and then 31 characters of random base64 characters.
So I run the code:
$2y$10$hereisa22charactersalt
$2y$10$hereisa22charactersalev7uylkfHc.RuyCP9EG4my7WwDMKGRvG
And I can't help but notice how the salt I passed into crypt and the salt that came out aren't the same; specifically, the very last character magically became an "e." After running this with different salts, I still get this same quirk where the last and only last character of the output hash is different.
I'm not a developer for PHP, so I'm sure there is some logic behind this behaviour. But I'm curious.
The docs do not state that the output will include the entire 22 bytes of salt. Also the example on the crypt documentation shows a final "$" on the salt.
crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$')
Producing:
$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi
Using the password "testtest" and the hash "KFtIFW1vulG5nUH3a0Mv" with the following code results in different hashes depending on the PHP version (as shown in the image below):
$salt = "KFtIFW1vulG5nUH3a0Mv";
$password = "testtest";
$key = '$2a$07$';
$key = $key.$salt."$";
echo crypt($password, $key);
Output 1 (PHP v.5.3.0 - 5.4.41, 5.5.21 - 5.5.25, 5.6.5 - 5.6.9): $2a$07$KFtIFW1vulG5nUH3a0Mv$.0imhrNa/laTsN0Ioj5m357/a8AxxF2q
Output 2 (PHP v.5.5.0 - 5.5.20, 5.6.0 - 5.6.4): $2a$07$KFtIFW1vulG5nUH3a0Mv$e0imhrNa/laTsN0Ioj5m357/a8AxxF2q
Here is an example of the problem:
http://3v4l.org/dikci
This is a massive issue if crypt is being used to hash passwords for a login, as depending on PHP version the hash will be different. Anybody understand what this issue is from and how to deal with it?
This is not so much an issue as you may think.
First you should note, that your code is not absolutely correct, BCrypt requires a 22 character salt, but you provided a 20 character salt. This means that the terminating '$' (which is not necessary btw) will be seen as part of the salt, as well as the first letter of your password. The $ is not a valid character for a BCrypt salt though.
Another thing to consider is that not all bits of character 22 are used, this is due to the encoding, ircmaxell gave a good explanation about this. So different salts can result in the same hash, you can see this well in this answer. How different implementations handle this last bits of the character 22 could theoretically change. As long as the crypt function can verify the password with both hashes there is no problem.
The generation of the salt with its pitfalls is one of the reasons, why the functions password_hash() and password_verify() where written, they make the password handling much easier.
I understand that PHP's crypt() function works like so:
crypt($password,$salt);
And that to create a bcrypt hash(which is considered very secure) the format is:
crypt("$2y$15$password",$salt);
Where $2y specifies the use of bcrypt, and 15 is the number of rounds, and it should be above 10.
Here's my question. Running:
crypt("$2y$15$password");
in my understanding, generates a nice big random salt and adds it to the hash, and when comparing passwords, this is extracted automatically. This would mean that I wouldn't have to have a salt field in my table, or generate a salt independently. Is this secure and correct?
For example, when I run:
$test = "Hello";
echo crypt("$2y$15$test") . "\n";
I get:
$6$R59d/nemygl0$/Gk6s57K2eFAkH4BWDGYhfdhbYGcqz.GRbD7qGDKOlhE5Lk.kgCoGQo/sDCCf1VDffdh7jtXPn/9rsexwrpFk1
Where the first 6 refers to some algorithm number, the next bit between the two $ is the salt, and the bit after that is the hash. Is this correct?
Also, what is the syntax for comparing this hash to another for verification?
Thanks.
I think crypt as you're using it uses SHA-512.
You probably forgot to pass $test as parameter in your crypt().
According to php docs you should pass the password as first argument (without any prefix) and then pass the salt as second argument with the $2y$15 prefix and a 22 characters salt. E.g.
crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$');
The function crypt() should be used like this:
$hashvalue = crypt($password, $cryptParams);
whereas the salt must be generated by yourself and is part of $cryptParams.
$bcryptAlgo = '$2y';
$cost = '$15';
$salt = '$' . functionThatGenerates22CharRandomSalt();
$cryptParams = $bcryptAlgo . $cost . $salt;
One of the difficulties is to generate a valid, unique and unpredictable salt. The best you can do is to read from the operating systems random source.
PHP 5.5 will have it's own functions password_hash() and password_verify() ready, to simplify this task. I strongly recommend to use this excellent api, there is also a compatibility pack available for earlier PHP versions. If you want to know more about how to use crypt, have a look at this article.
Your cost parameter of 15 is quite high, use it if you can afford this much of time, but today a number of 10 is ok.
There is an application written in PHP which I am converting to Ruby. When encrypting passwords the PHP app uses the following code:
if($method == 2 && CRYPT_BLOWFISH) return crypt($pass, '$2a$07$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxx$');
I'm assuming this is using a Blowfish implementation. The x's here are all a-zA-Z0-9 characters.
The Blowfish implementation in Ruby uses the following syntax (taken from http://crypt.rubyforge.org/blowfish.html):
blowfish = Crypt::Blowfish.new("A key up to 56 bytes long")
plainBlock = "ABCD1234"
encryptedBlock = blowfish.encrypt_block(plainBlock)
I don't have a 56 or fewer byte long string and it's not clear what that should be from the PHP version. So how can I write a Ruby function which will encrypt passwords to give the same result as the PHP one?
The PHP code is hashing $pass with the salt $2a$07$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxx$ if CRYPT_BLOWFISH is set (CRYPT_BLOWFISH == 1). The salt has to follow the format indicated in the PHP documentation ("$2a$", a two digit cost parameter, "$", and 22 digits from the alphabet "./0-9A-Za-z").
I'm not sure if you can do it with the library you are referring, but you can use bcrypt-ruby instead.
For your code it would be something like this, I'm using the same data from the PHP example ( http://php.net/manual/en/function.crypt.php ), I only take the 29 first characters of the salt because beyond that PHP ignores it:
require 'bcrypt-ruby'
pass = "rasmuslerdorf" # Here you should put the $pass from your PHP code
salt = '$2a$07$usesomesillystringfors' # Notice no $ at the end. Here goes your salt
hashed_password = BCrypt::Engine.hash_secret(pass,salt) # => "$2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi"
This gives you the same output as on the PHP example. If your salt is too long take the first 29 characters ($2a$07$ plus the next 22 extra characters).
I tested the behavior of PHP, if the salt is too long (beyond 29 characters in total) the rest is ignored, if the salt is too short it will return 0. E.g in PHP:
<?php
crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$')
// returns $2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi
crypt('rasmuslerdorf', '$2a$07$usesomesillystringfors')
// returns $2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi
crypt('rasmuslerdorf', '$2a$07$usesomesilly')
// returns 0 because the salt is not long enough
?>
I need to get the basics of this function. The php.net documentation states, for the blowfish algorithm, that:
Blowfish hashing with a salt as follows: "$2a$", a two digit cost parameter, "$", and 22 base 64 digits from the alphabet "./0-9A-Za-z". Using characters outside of this range in the salt will cause crypt() to return a zero-length string
So this, by definition, should not work:
echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');
However, it spits out:
$2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e
Where it seems that crypt() has cut the salt itself to a length of 22. Could somebody please explain this?
Another aspect of this function I can't get my head around is when they use crypt() to compare passwords. http://php.net/manual/en/function.crypt.php (look at ex. #1). Does this mean that if I use the same salt for all encrypting all my passwords, I have to crypt it first? ie:
$salt = "usesomadasdsadsadsadae";
$salt_crypt = crypt($salt);
if (crypt($user_input, $salt) == $password) {
// FAIL WONT WORK
}
if (crypt($user_input, $salt_crypt) == $password) {
// I HAVE TO DO THIS?
}
Thanks for your time
Following code example may answer your questions.
To generate hashed password using Blowfish, you first need to generate a salt, which starts with $2a$ followed by iteration count and 22 characters of Base64 string.
$salt = '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringfors';
$digest = crypt('rasmuslerdorf', $salt);
Store the whole $digest in database, it has both the salt and digest.
When comparing password, just do this,
if (crypt($user_input, $digest) == $digest)
You are reusing the digest as salt. crypt knows how long is the salt from the algorithm identifier.
New salt for every password
$password = 'p#ssw0rd';
$salt = uniqid('', true);
$algo = '6'; // CRYPT_SHA512
$rounds = '5042';
$cryptSalt = '$'.$algo.'$rounds='.$rounds.'$'.$salt;
$hashedPassword = crypt($password, $cryptSalt);
// Store complete $hashedPassword in DB
echo "<hr>$password<hr>$algo<hr>$rounds<hr>$cryptSalt<hr>$hashedPassword";
Authentication
if (crypt($passwordFromPost, $hashedPasswordInDb) == $hashedPasswordInDb) {
// Authenticated
Quoting from the manual
CRYPT_BLOWFISH - Blowfish hashing with
a salt as follows: "$2a$", a two digit
cost parameter, "$", and 22 base 64
digits from the alphabet
Note: 22 base 64 digits
BCrypt uses 128 bits for salt, so 22 bytes Base64, with only two bits of the last byte being used.
The hash is computed using the salt and the password. When you pass the crypted password, the algorithm reads the strength, the salt (ignoring everything beyond it), and the password you gave, and computes the hash, appending it. If you have PostgreSQL and pg_crypto handy, SELECT gen_salt('bf'); will show you what of $salt is being read.
Here's a code sample for salt generation, from my .NET implementation's test-vector-gen.php, alternatively:
$salt = sprintf('$2a$%02d$%s', [strength goes here],
strtr(str_replace(
'=', '', base64_encode(openssl_random_pseudo_bytes(16))
),
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'));
There is no reason to use the same salt for all of your passwords. The salt is part of the output anyway so you gain nothing in convenience... though I grant PHP ought to have a built-in gen_salt function.
First question:
So this, by definition, should not work:
echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');
Where it seems that crypt() has cut
the salt itself to a length of 22.
Could somebody please explain this?
There isn't a problem with having too many characters... the phrase Using characters outside of this range in the salt will cause crypt() to return a zero-length string referse to outside the range of base 64 not the range of 22 characters. Try putting an illegal character in the salt string, and you should find that you get an empty output (or if you put < 22 characters in, resulting in illegal empty bytes).
Second question:
You pass in the encrypted stored password as salt because the salt string always appears (by design) in the encrypted string, and this way you ensure that you have the same salt for both encryption of stored and user-entered password.
This question is in relation to my response to ZZ Coder's answer. Basically my question is regarding storing the crypt() result in the database. Am I supposed to store the entire output in the database, so that my database looks like this:
--------------------------------------------------------------------------------
| ID | Username | Password |
--------------------------------------------------------------------------------
| 32 | testuser | $2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e |
--------------------------------------------------------------------------------
If yes, then doesn't this kind of defy the purpose of using a salt in the first place? If someone gains access to the db, they can clearly see the salt used for the encryption?
Bonus question: Is it secure to use the same salt for every password?