I am working on a project which needs a very advanced security system when it comes to saving passwords etc. So my question is, is this way to save passwords safe enough?
This is the way I programmed:
When a user registers, a salt will be created out of the following details:
An unique user ID (primary key in mySQL)
The users emailaddress
The current (micro)timestamp
Some random key defined in the configuration of the website
This salt is being hashed into a sha512 key.
After the salt has been created, the following string is being hashed using Bcrypt: password + sha512 salt (worklevel 10, ($2a$10...)).
Then I skip the first 5 characters of the Bcrypt output ($2a$10) and I will save the remaining string into the database.
When a user tries to log in, I first check if the username exists. If it does, I check if the password is correct using the check() function.
I use this Bcrypt class to encrypt and check.
Can you guys tell me if this way of encrypting and verifying is well enough for a big project?
Regards,
Jan Willem
There's absolutely no point in deriving the salt from any particular input. In fact, this can only serve to weaken it. A salt derived from any value that has a relation with the value to be hashed is not a salt, it's just an altered hashing algorithm. Salts are entirely (pseudo) random noise, period. Their only point is to make the input unique; and the most unique value is random noise.
If you derive the salt from a good pseudo random number generator, there's no need to hash it. This can only serve to reduce its entropy.
You should store the entire result in the database (including $2a$10). If you're doing it properly, that hash is virtually impossible to brute force as is. Omitting that piece of information only makes your job more difficult, it doesn't make it meaningfully more difficult for a potential attacker.
Storing that value lets you see what algorithm the hash was created with and upgrade it over time. As hardware becomes faster you should increase the work factor of the algorithm and as better algorithms become available (hello scrypt!) you should upgrade the algorithm used. The way that works is that you check whether the hash was created using the latest and greatest when a user logs in. At that point is your opportunity to rehash the cleartext password and upgrade it.
Use PHP's new password_hash API or the password_compat shim for older versions, those are well tested and perfectly "advanced".
First, don't use bcrypt. Use PBKDF2 instead. This will allow you to increase the difficulty of doing an offline brute force attack. [Not saying that bcrypt won't, they are essentially the same, but PBKDF2 supports any hashing function, not just Blowfish]
Second, here is how you should do this.
Randomly Generate (don't base it on anything) a 128-bit random number for the Salt
Store the Salt as it's own parameter in the database. Remember the purpose of the salt is to make it so that the same password hashed twice doesn't have the same result.
Pick a good Hashing Algorithm. My recommendation, use SHA-256 or SHA-512. This is what you will use as part of the PBKDF2 function.
Do some research, figure out what is a good number of rounds to make the hashing of the password take 1 second. Remember if we have a password keyspace of 96 characters and a minimum of 8 characters wide, and each permutation takes a required 1 second to calculate, then an attacker will cry. The nice part of this is that over time as computers become faster, you can reevaluate this value and hash the hash a few more times to bring it up to the new 1 second requirement. Example: Say that 12 rounds of SHA-256 takes 1 second on modern computers. In 5 years, the computers are now so fast that 12 rounds takes 20ms instead. But 16 rounds takes 1 second on the hardware then. Just hashing the hash 4 additional times now keeps it at 1 second. The user never knew this happened because you didn't have to ask them to change their password.
You could store your password in the Shadow format, sure, but if you are using a database, just use database fields. You parsing a string is slower than the database just knowing what to give you.
Now I want to point out a timing attack in your system. If you tell an attacker that a username doesn't exist or the amount of time it takes to return an error is different (and shorter) than it takes to return a success, then you have a possibility for a side channel attack. Additional information can make it easier for an attacker to enumerate user accounts on your system (and if the username is based on Email, then they can now do a phishing attack against the user directly).
At first I want to thank you guys for the quick response, it is very important to me to make the system as hard to hack as possible.
Based on your recommendations I made the following system:
When a user registers, an unique hash is being created:
$unique_hash = hash('sha512', some defined security key (in the config) . openssl_random_pseudo_bytes(50));
At second, a salt is created:
$salt = strtr(base64_encode(openssl_random_pseudo_bytes(50)), '+', '.');
In another table, located in another database, this two variables are being combined and only the $unique_hash is stored in the users row in the users table.
Then we create the encrypted password, I use this function:
<?php
const COUNT = 8192;
const KEY_LENGTH = 254;
const ALGORITHM = 'sha512';
public static function encrypt($password, $salt, $algorithm = PBKDF2::ALGORITHM, $count = PBKDF2::COUNT, $key_length = PBKDF2::KEY_LENGTH, $raw_output = false) {
$algorithm = strtolower($algorithm);
if(!in_array($algorithm, hash_algos(), true))
die('PBKDF2 ERROR: Invalid hash algorithm.');
if($count <= 0 || $key_length <= 0)
die('PBKDF2 ERROR: Invalid parameters.');
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++)
{
$last = $salt . pack("N", $i);
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
for ($j = 1; $j < $count; $j++)
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
$output .= $xorsum;
}
if($raw_output)
return substr($output, 0, $key_length);
else
return bin2hex(substr($output, 0, $key_length));
}
?>
Then we store this password in the default user table.
Last step; logging in. When a user tries to log in, at first I check if the given username exists in the database. When it does, I call the $unique_hash, located in the same row. Since we need the salt, I search in the other database, where the hashes and salts are stored. Then the salt will be returned and we are able to validate the given password with the same function (encrypt()).
This will be it. Is this safe enough?
Jan Willem
Related
im currently trying to understand hashes and salts. As i understand it, it should not be possible to verify a password, if i only have the password and the generated hash(that was generated with a random salt ofc).
So how can the password_verify function in PHP verify my password, if i dont give it the salt? Is there a hidden variable in the background, that stores it for the php hashing functions?
And if that is the case, how can
doveadm pw -t '{SHA512-CRYPT}$6$myhash...' -p "qwertz"
verify it too, even if i run it on a complety different computer? Thats a tool, that comes with Dovecot(a MDA).
Here is my PHP code, that creates a random salt with 64 chars, combines it with a password, creates a hash and verifies the hash via password_verify().
I just started working on the whole hash/salt/pepper thing today, so there could be a huge flaw in my whole train of thought.
<?php
$password = "qwertz";
$salt = createSalt(64);
$hash = crypt($password, "$6$$salt");
if (password_verify($password, $hash)) {
echo 'Password is valid!';
} else {
echo 'Invalid password.';
}
function createSalt($length){
$chars = "IrhsYyLofUKj4caz0FDBCe2W9NRunTgQvp7qOXmS5GM3EJV6i8tAHdkPbxwl1Z";
$salt="";
for($i=0; $i < $length; $i++){
$newchar = substr($chars, rand(0,strlen($chars)-1),1);
$salt .= $newchar;
}
return $salt;
}
?>
The hash contains several pieces of information. This article explains the format used by Unix but I believe PHP password functions use a similar format (if not the same):
The hash field itself is comprised of three different fields. They are
separated by '$' and represent:
Some characters which represents the cryptographic hashing mechanism used to generate the actual hash
A randomly generated salt to safeguard against rainbow table attacks
The hash which results from joining the users password with the stored salt and running it through the hashing mechanism specified in
the first field
It can also include the exact per-algorithm options used to generate the hash, such us the algorithmic cost:
var_dump(password_hash('foo', PASSWORD_BCRYPT, [
'cost' => 8,
]));
string(60) "$2y$08$7Z5bTz7xXnom8QsrbZ7uQetMLxOZ7WjuDkUYRIh73Ffa17GV1Tb7q"
Here $2y$08$ means that Bcrypt with cost 8 was used.
If we use the newer Argon2 available in PHP/7.2 there're even more params:
$argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0
Some backgrounds to the answer from #Álvaro González :
PHP manual suggests using "password_hash" instead of "crypt" function through "password_hash" is a "crypt()" wrapper ( Because, it uses a strong hash, generates a strong salt, and applies proper rounds automatically. )
"password_hash()" returns the algorithm, cost, and salt as part of the returned hash. Therefore, all information that's needed to verify the hash is included in it. This allows the "password_verify" function to verify the hash without needing separate storage for the salt or algorithm information. : http://php.net/manual/en/function.password-verify.php
Since, "password_hash" is a wrapper for "crypt", "crypt" also does the same, ie., returns the algorithm, cost, and salt as part of the returned hash. and thus "password_verify" can verify the hash.
Now, please check the answer given by #Álvaro González
At the moment I have a database with md5 passwords stored, a few years back this was considered a little more secure than it is now and it's got to the point where the passwords need to be more secure.
I've read a lot of posts on here about crypt, md5, hash, bcrypt, etc and have come to consider using something along the lines of the following to 'secure' the passwords better than they are now.
I will use a combination of hash("sha512" and two salts, the first salt will be a site wide salt stored in a file such as .htaccess and the second salt will be created for each user.
Here's an example along the lines of what I'm testing at the moment:
.htaccess
SetEnv SITEWIDE_SALT NeZa5Edabex?26Y#j5pr7VASpu$8UheVaREj$yA*59t*A$EdRUqer_prazepreTr
example.php
$currentpassword = //get password
$pepper = getenv('SITEWIDE_SALT');
$salt = microtime().ip2long($_SERVER['REMOTE_ADDR']);
$saltpepper = $salt.$pepper;
$password = hash("sha512", md5($currentpassword).$saltpepper);
The salt would obviously need to be stored in a separate table to allow checking of future inserted login passwords but it would never be possible for a user to see. Do you think this is a sufficient way to go about this?
Ok, let's go over a few points here
What you have in $salt is not a salt. It's deterministic (meaning that there is no randomness in there at all). If you want a salt, use either mcrypt_create_iv($size, MCRYPT_DEV_URANDOM) or some other source of actual random entropy. The point is that it should be both unique and random. Note that it doesn't need to be cryptographically secure random... At absolute worst, I'd do something like this:
function getRandomBytes($length) {
$bytes = '';
for ($i = 0; $i < $length; $i++) {
$bytes .= chr(mt_rand(0, 255));
}
return $bytes;
}
As #Anony-Mousse indicated, never feed the output of one hash function into another without re-appending the original data back to it. Instead, use a proper iterative algorithm such as PBKDF2, PHPASS or CRYPT_BLOWFISH ($2a$).
My suggestion would be to use crypt with blowfish, as it's the best available for PHP at this time:
function createBlowfishHash($password) {
$salt = to64(getRandomBytes(16));
$salt = '$2a$10$' . $salt;
$result = crypt($password, $salt);
}
And then verify using a method like this:
function verifyBlowfishHash($password, $hash) {
return $hash == crypt($password, $hash);
}
(note that to64 is a good method defined here). You could also use str_replace('+', '.', base64_encode($salt));...
I'd also suggest you read the following two:
Fundamental difference between hashing and encrypting
Many hash iterations, append salt every time?
Edit: To Answer the Migration Question
Ok, so I realize that my answer did not address the migration aspect of the original question. So here's how I would solve it.
First, build a temporary function to create a new blowfish hash from the original md5 hash, with a random salt and a prefix so that we can detect this later:
function migrateMD5Password($md5Hash) {
$salt = to64(getRandomBytes(16));
$salt = '$2a$10$' . $salt;
$hash = crypt($md5Hash, $salt);
return '$md5' . $hash;
}
Now, run all the existing md5 hashes through this function and save the result in the database. We put our own prefix in so that we can detect the original password and add the additional md5 step. So now we're all migrated.
Next, create another function to verify passwords, and if necessary update the database with a new hash:
function checkAndMigrateHash($password, $hash) {
if (substr($hash, 0, 4) == '$md5') {
// Migrate!
$hash = substr($hash, 4);
if (!verifyBlowfishHash(md5($password), $hash) {
return false;
}
// valid hash, so let's generate a new one
$newHash = createBlowfishHash($password);
saveUpdatedPasswordHash($newHash);
return true;
} else {
return verifyBlowfishHash($password, $hash);
}
}
This is what I would suggest for a few reasons:
It gets the md5() hashes out of your database immediately.
It eventually (next login for each user) updates the hash to a better alternative (one that's well understood).
It's pretty easy to follow in code.
To answer the comments:
A salt doesn't need to be random - I direct you to RFC 2898 - Password Based Cryptography. Namely, Section 4.1. And I quote:
If there is no concern about interactions between multiple uses
of the same key (or a prefix of that key) with the password-
based encryption and authentication techniques supported for a
given password, then the salt may be generated at random and
need not be checked for a particular format by the party
receiving the salt. It should be at least eight octets (64
bits) long.
Additionally,
Note. If a random number generator or pseudorandom generator is not
available, a deterministic alternative for generating the salt (or
the random part of it) is to apply a password-based key derivation
function to the password and the message M to be processed.
A PseudoRandom Generator is available, so why not use it?
Is your solution the same as bcrypt? I can't find much documentation on what bcrypt actually is? - I'll assume that you already read the bcrypt Wikipedia Article, and try to explain it better.
BCrypt is based off the Blowfish block cipher. It takes the key schedule setup algorithm from the cipher, and uses that to hash the passwords. The reason that it is good, is that the setup algorithm for Blowfish is designed to be very expensive (which is part of what makes blowfish so strong of a cypher). The basic process is as follows:
A 18 element array (called P boxes, 32 bits in size) and 4 2-dimensional arrays (called S boxes, each with 256 entries of 8 bits each) are used to setup the schedule by initializing the arrays with predetermined static values. Additionally, a 64 bit state is initialized to all 0's.
The key passed in is XOred with all 18 P boxes in order (rotating the key if it's too short).
The P boxes are then used to encrypt the state that was previously initialized.
The ciphertext produced by step 3 is used to replace P1 and P2 (the first 2 elements of the P array).
Step 3 is repeated, and the result is put in P3 and P4. This continues until P17 and P18 are populated.
That's the key derivation from the Blowfish Cipher. BCrypt modifies that to this:
The 64 bit state is initialized to an encrypted version of the salt.
Same
The P boxes are then used to encrypt the (state xor part of the salt) that was previously initialized.
Same
Same
The resulting setup is then used to encrypt the password 64 times. That's what's returned by BCrypt.
The point is simple: It's a very expensive algorithm that takes a lot of CPU time. That's the real reason that it should be used.
I hope that clears things up.
Implementation of your new, more secure, password storage should use bcrypt or PBKDF2, as that's really the best solution out there right now.
Don't nest things, as you don't get any real security out of this due to collisions as #Anony-Mousse describes.
What you may want to do it implement a "transition routine" where your app transitions users over from the old MD5-based system to the new more secure system as they log in. When a login request comes in, see if the user is in the new, more secure, system. If so, bcrypt/PBKDF2 the password, compare, and you're good to go. If they are not (no one will be at first), check them using the older MD5-based system. If it matches (password is correct), perform the bcrypt/PBKDF2 transformation of the password (since you now have it), store it in the new system, and delete the old MD5 record. Next time they log in, they have an entry in the new system so you're good to go. Once all of the users have logged in once you implement this, you can remove this transition functionality and just authenticate against the new system.
Do not nest md5 inside your sha512 hash. An md5 collision then implies a hash collision in the outer hash, too (because you are hashing the same values!)
The common way of storing passwords is to use a scheme such as
<method><separator><salt><separator><hash>
When validating the password, you read <method> and <salt> from this field, reapply them to the password, and then check that it produces the same <hash>.
Check the crypt functions you have available. On a modern Linux system, crypt should be able to use sha512 password hashing in a sane way: PHP crypt manual. Do not reinvent the wheel, you probably just screw up more badly than md5, unless you are an expert on cryptographic hashing. It will even take care of above scheme: the Linux standard is to use $ as separator, and $6$ is the method ID for sha512, while $2a$ indicates you want to use blowfish. So you can even have multiple hashes in use in your database. md5 hashes are prefixed with $1$<salt>$ (unless you reinvented md5 hashing, then your hashes may be incompatible).
Seriously, reuse the existing crypt function. It is well checked by experts, extensible, and compatible across many applications.
I looked into this subject a while back and found the following link of great use:
Secure hash and salt for PHP passwords
I also use the following to create a random salt:
public static function getRandomString($length = 20) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$string = '';
for ($i = 0; $i < $length; $i++) {
$string .= substr($characters, (mt_rand() % strlen($characters)), 1);
}
return $string;
}
i'm testing several combinations of sha1 and md5:
<?php
$test = 'fail test';
echo nl2br ("Text: $test\n");
echo nl2br ("md5: ".md5($test)."\nsha1: ".sha1($test)."\nsha1(md5): ".sha1(md5($test))."\nmd5(sha1): ".md5(sha1($test)));
?>
Output:
Text: fail test
md5: 748410d0085967c496d54dd8fcbecc96
sha1: d730125e8cb8576459173655148fb6896ef44c09
sha1(md5): faa3ebeecfec45e509e93e6b245a69e2a78785ea
md5(sha1): b48e89b85c350c91eb302c1de96d4249
Which one better, or maybe user something else ? If yes, what then ?
Both of them are cryptographic hash functions that operate 1-way only, the main difference being that MD5 output size is 128 bits whereas SHA-1 is 160 bits. In brief, I don't see they are much different to use despite MD5 is more common these days.
Curiously, I can't really see how md5($text) is different from md5(sha($text)) when they all encrypted to a 32 character-long string, what about md5($text."token") for example?
And, what do you mean by better? Is it more good looking or more security? See bcrypt if you prefer security :) Wikipedia: http://en.wikipedia.org/wiki/Bcrypt
Hashing a hash adds no extra security. (In fact, it might make it worse if the person has a hash-of-hash lookup table.)
The best hash will be the one that is computationally the most expensive to perform without any vulnerabilities. I would hash passwords with at least sha-256.
Always hash your passwords with a salted key. This key should be unique per password. It doesn't need to be stored privately. The purpose of a salted password is that the hacker who gained access to your database cannot simply compare the hash with a known list of hashes that correspond to common passwords. Instead, he must try to brute force the password by trying every possible password.
By using a unique salt per password, you guarantee that each hash in the database is different, even if they use the same password.
To salt a password, simply create a random string of characters and append it to the password. Here's a sample hash with a 48-bit salt and sha-256:
function make_password($password)
{
# random 48-bit salt (8 chars when base64 encoded)
$salt = base64_encode(pack('S3', mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0, 0xffff)));
return $salt.hash('sha256', $salt.$password);
}
function check_password($password, $hash)
{
$salt = substr($hash, 0, 8);
return hash('sha256', $salt.$password) == substr($hash, 8);
}
$password = 'password';
$hash = make_password('password');
echo $hash."\n";
var_dump(check_password('password', $hash));
var_dump(check_password('wrong', $hash));
Every time you run it, the hash will be different. To validate a password, you select the row where the username matches, and then call check_password($password_from_user, $hash_from_db).
Here's a sample output:
AzrD1jZzc693714a43ad5dfd4106c0a620ef23ff9915070711fa170a6670b8164862b496
bool(true)
bool(false)
You can use a larger salt or a stronger hashing algorithm if you prefer. But at minimum, I would use something like the above.
You should salt your passwords, ALWAYS. This doesn't stop brute force through a login form but if someone managed to get the details, it would be much harder to crack (rainbow tables would be useless unless they manage to get your salt too)
Essentially, if you adding onto the original data or mangling in a controlled way, it will make security a little better. No-one can ever reverse a hash but they can find other inputs thats match the hash. Mangling the user input will make it harder to login for the hackers.
for example, if a user's pass is 123456, if you add a salt of "salt" to it so it becomes 123456salt, the MD5 of this would be 207acd61a3c1bd506d7e9a4535359f8a. A hacker could crack this to become 123456salt but when it comes to using that on your login form, your code will add salt again and the login will fail.
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
}
I'm using the Auth Module in Kohana v 2.3.4.
In terms of authenticating users, there's a two step process. The entry point is the function login. It's first task is to retrieve the password stored in the database and retrieve the password and determine the salt value. The salt is supposedly determined by an array of values, each corresponding to a point in the $salt.$password hashed value to introduce yet another part of the salt. In my case, I'm using md5.
Problems:
I can't find a configuration for this SALT value. It seems to be relying on one already present within the password stored in the database. Is there one or do I need to configure AUTH to do so since this login needs to be portable and reproducible? If it can't detect the salt, in the hash_password routine, it defaults to using uniqid(), which I don't believe is portable at all.
In terms of adding users, does it make sense to modify the Auth library to add this feature? ie, introduce my own customized SALT that I can say, do an MD5 hash on that and then use that md5 generated by the salt to seed the password at given points in the md5sum?
I'm no security expert, but is this overkill? Granted, it prevents someone who were to get access to the md5 password list from using a md5 lookup of predetermined hashes.
If you have used the Kohana PHP framework, if you have any lessons learned or experiences after using it that might give insight as to the right approach for this problem, let me know. I'm reading numerous forums and wiki's about it, and there isn't a real concrete opinion yet that I've seen. I'm essentially trying to get a reproducible approach for authenticating someone in this site, both using PHP and eventually from a mobile device, like an iPhone. I'm also thinking of eventually adding support for google friend connect for openID support and integration.
Below are snippets from the Auth module in Kohana concerning the functions of interest. They have some debugging in them as I'm trying to better understand what's going on.
public function login($username, $password, $remember = FALSE)
{
if (empty($password))
return FALSE;
if (is_string($password))
{
// Get the salt from the stored password
$salt = $this->find_salt($this->driver->password($username));
Kohana::log('debug', "--- Auth_Core login salt = $salt ");
Kohana::log('debug', "--- Auth_Core login pass = $password ");
// Create a hashed password using the salt from the stored password
$password = $this->hash_password($password, $salt);
}
Kohana::log('debug', "--- Auth_Core login pass_hash = $password ");
return $this->driver->login($username, $password, $remember);
}
public function find_salt($password)
{
$salt = '';
foreach ($this->config['salt_pattern'] as $i => $offset)
{
// Find salt characters, take a good long look...
//$salt .= $password[$offset + $i];
$salt .= substr($password, $offset + $i, 0);
}
return $salt;
}
public function hash_password($password, $salt = FALSE)
{
Kohana::log('debug', "--- Auth_Core Original Pass = $password ");
if ($salt === FALSE)
{
// Create a salt seed, same length as the number of offsets in the pattern
$salt = substr($this->hash(uniqid(NULL, TRUE)), 0, count($this->config['salt_pattern']));
Kohana::log('debug', "--- Auth_Core salt created = $salt ");
}
// Password hash that the salt will be inserted into
$hash = $this->hash($salt.$password);
// Change salt to an array
$salt = str_split($salt, 1);
// Returned password
$password = '';
// Used to calculate the length of splits
$last_offset = 0;
foreach ($this->config['salt_pattern'] as $offset)
{
// Split a new part of the hash off
$part = substr($hash, 0, $offset - $last_offset);
// Cut the current part out of the hash
$hash = substr($hash, $offset - $last_offset);
// Add the part to the password, appending the salt character
$password .= $part.array_shift($salt);
// Set the last offset to the current offset
$last_offset = $offset;
}
Kohana::log('debug', "--- Auth_Core hashpw = $password + $hash ");
// Return the password, with the remaining hash appended
return $password.$hash;
}
Problem 1. The salt configuration is stored in config/auth.php. Find that file in modules/auth/config, then in your app/config folder (as you might have already known, Kohana uses cascading file system mechanism). The default file, which you are encouraged to customize into app/config/ folder, looks like below:
<?php defined('SYSPATH') OR die('No direct access allowed.');
return array
(
'driver' => 'ORM',
'hash_method' => 'sha1',
'salt_pattern' => '1, 3, 5, 9, 14, 15, 20, 21, 28, 30',
'lifetime' => 1209600,
'session_key' => 'auth_user',
'users' => array
(
// 'admin' => 'b3154acf3a344170077d11bdb5fff31532f679a1919e716a02',
),
);
Problem 2. In my opinion, the password hashing mechanism used by Auth, which is SHA1 with salt insertion, is quite secure provided you keep your salts, i.e. your auth.php file, secure.
Problem 3. Auth built-in hashing mechanism uses SHA1, which is relatively more crack-proof than MD5, so I would say don't do the MD5 way, no matter how complicated your scheme might look. A security expert Thomas Ptacek in his blog wrote:
No, really. Use someone else’s
password system. Don’t build your own.
Most of the industry’s worst security
problems (like the famously bad LANMAN
hash) happened because smart
developers approached security code
the same way they did the rest of
their code.
Problem 4. Yup I'm using Kohana to build my small company website and some of our clients' website and so far I don't find any problem with the Auth module, although I can't say much since I haven't really used it for real security-concerned website. But in general, I'd say Kohana is an excellent framework especially with the cascading filesystem mechanism.
Regarding point 1, the hash_password() function is used both to generate the password hash (against the salt and including the salt) that is stored in the database (e.g. at signup-time), as well as to recreate that hash when the password needs to be verified (e.g. at login-time). The hash_password() function will encode any salt that is given (or uniqid() if none is given) in the password-hash itself; that's a form of encryption where the salt_pattern is the key; if the salt_pattern can be kept secret, then that provides additional security since an adversary will not be able to do offline brute-forcing of the hash since the method of hashing is not reproducible (if the salt_pattern can be kept secret):
// Signup time; forget about uniqid(); you can use any salt that
// you please; once the password hash is stored in the database there
// is no need to know where your salt came from since it will be
// included in the password hash.
$password_hash = hash_password($password, FALSE);
// Login time; note that the salt is taken from the password hash itself.
$reproduced = hash_password($password, find_salt($password_hash));
$verifies = $password_hash == $reproduced;
The hash_password() function will first hash the password against the salt, and then insert each char of the salt into the password hash at the corresponding salt_pattern offset. find_salt() will extract these salt chars so that the hash can be reproduced. You can see it as hash_password() encrypting the salt and find_salt() decrypting it. Although you can also see it has hash_password() hiding the salt and find_salt() finding it, this method of encryption can't be called steganography, I think, because it is clear from the code that there is a salt stored with the password hash (the existence of the salt is not secret).
Regarding point 2, using your own salt is straightforward and fully compatible with the Auth module and an already existing database.
Regarding point 3, using a per user salt (uniqid() by default) is not overkill. Especially with MD5 which is broken for security purposes and where finding collisions is already practical with today's technology. Even better would be to use bcrypt() which uses a purposefully slower hashing algorithm to thwart brute-forcing attempts.
Regarding point 4, I haven't used the Kohana framework before, but reproducing or porting the Auth module is straightforward. Care must be taken that the salt_pattern is not forgotten or lost since it is an essential part of the hashing algorithm. The salt_pattern should also be kept secret since it is the only thing that keeps a determined adversary from brute-forcing the password hashes. uniqid() is just a reasonable default and can be replaced with whatever you want (as long as it is per-user and not a constant site-wide value.)
Also, there is a very good answer here on stackoverflow regarding portable bcrypt() and PHP. Naturally that will not be compatible with the Auth module, but I'd like to mention it anyway since it's just best practice to use a slow hash and not to rely on secrets that are difficult to keep, like the salt_patten.