In a previous article, we saw why it was important to store passwords in a database with robust hash functions such as Bcrypt and Argon2. This helps to render brute force or dictionary attacks completely ineffective.
However, a problem is regularly noted on already existing applications: how to use the latest recommendations on password storage on an existing database?
How to securely store passwords?
Before getting to the heart of the matter, a few details on the OWASP recommendations on password storage:
- Use Argon2id with a minimum configuration of 19 MiB of memory, a number of iterations of 2, and 1 degree of parallelism.
- If Argon2id is not available, use Scrypt with a minimum CPU/memory cost setting of (2^17), a minimum block size of 8 (1024 bytes) and a parallelism setting of 1.
- For older systems using Bcrypt, use a work factor of 10 or more and a password limit of 72 bytes.
- If FIPS-140 compliance is required, use PBKDF2 with a work factor of 600,000 or more and an internal hash function of HMAC-SHA-256.
- Use a pepper to provide additional defense in depth (although on its own it does not provide any additional security features).
In the following, we will show how to update passwords with Argon2id. To do this, we will start from 2 common cases:
- Passwords are stored in clear text or with an encryption algorithm.
- Passwords are stored with an unsuitable hash (md5, sha1, sha2, sha3…).
However we will not implement a pepper. In case you want to add a pepper, just hash the pepper + password. As a reminder, the pepper is identical for all users.
Moreover, we will provide PHP code as an example, but the algorithms can be applied to other languages.
Passwords are stored in clear text or with an encryption algorithm
Updating passwords in database with Argon2id
First of all, it is important to remember that storing passwords in clear, readable text is forbidden in certain countries such as France. It is therefore imperative to change your password management.
And if you are starting from scratch (a case we will not see in this article), you can consult the following content which details: How to Securely Store Passwords in Database?
Let us now return to our subject.
In the case presented here, since the passwords are easily identifiable (because they are stored in clear text), it is only required to update the passwords with the following algorithm:
This will result in the user table being transformed from:
ID | Name | Password |
---|---|---|
1 | admin | azerty |
2 | toto | matrix |
3 | billy | yep59f$4txwrr |
4 | tata | matrix |
5 | titi | freepass |
6 | attacker | tesPwndPassword |
To:
ID | Name | Password |
---|---|---|
1 | admin | $argon2id$v=19$m=65536,t=4,p=1$UjYzcS42QmhmLjFsa3lrYQ$7t8mfl Th2JhcVqSTdQ0GwtLtMh6plWECubPEH8NjEUM |
2 | toto | $argon2id$v=19$m=65536,t=4,p=1$SURlcDNVUDZFWEVlQy5UYg$mv m0Iohc9nd/KOLP5kAw6/WB+PAK0Nt6QGPTsdQa8aw |
3 | billy | $argon2id$v=19$m=65536,t=4,p=1$WTlMMDJDeUVMeDNONXRDaQ$u ccT7LUNmJcj8+ZLqtT+U7m+IJePgCuvv8BxCzRYKG4 |
4 | tata | $argon2id$v=19$m=65536,t=4,p=1$c00udFlHMm1LMXhwcDBjVg$NR0I QiLqC0dMSgYHbtEhcAGdrFqhdI4YoOTjtMW1zZA |
5 | titi | $argon2id$v=19$m=65536,t=4,p=1$U1Y2dm44cXlMWHp0SkFhVg$lYj9k Lfx6zUNOTDUXHDhpEFyBdSJXDv9qjxA0+4oEYw |
6 | attacker | $argon2id$v=19$m=65536,t=4,p=1$NmFqZVhOaTRsN2t4L3lWRw$UJbZ AJcm7ujhLv/Y/DcyMDqD42ME95l+Cd5Kh2XJ2G0 |
It should be noted that the application does not impose a strong password policy, which is essential to avoid risks of compromise. It is therefore strongly recommended to impose the use of strong passwords when creating them.
For more information, we refer you to our article: How to secure authentication, session management and access control systems of your web applications?
Thanks to the password_get_info
function, only non-Argon2 passwords are transformed. This allows the script to be run several times without the risk of hashing the passwords several times.
Furthermore, it is important to make a backup of the database to avoid the risk of corrupting users in the event of migration problems.
NB: if an encryption algorithm is used, the clear passwords must be hashed directly, not the encrypted data.
Checking the validity of user passwords
To confirm the presence or not of a user, it is simply necessary to compare the password and the hash with the password_verify
function (or equivalent in other languages).
Even if the operation is invisible to the user, it is still strongly recommended to encourage users to change their password after this modification.
The operation will be as follows:
We add a column to the user table, for example isUpdatePassword
which is false
by default:
sqlite> ALTER TABLE USERS ADD isUpdatePassword bool default false;
sqlite> select * from USERS;
id|name|password|isUpdatePassword
1|admin|$argon2id$v=19$m=65536,t=4,p=1$UjYzcS42QmhmLjFsa3lrYQ$7t8mflTh2JhcVqSTdQ0GwtLtMh6plWECubPEH8NjEUM|0
2|toto|$argon2id$v=19$m=65536,t=4,p=1$SURlcDNVUDZFWEVlQy5UYg$mvm0Iohc9nd/KOLP5kAw6/WB+PAK0Nt6QGPTsdQa8aw|0
3|billy|$argon2id$v=19$m=65536,t=4,p=1$WTlMMDJDeUVMeDNONXRDaQ$uccT7LUNmJcj8+ZLqtT+U7m+IJePgCuvv8BxCzRYKG4|0
4|tata|$argon2id$v=19$m=65536,t=4,p=1$c00udFlHMm1LMXhwcDBjVg$NR0IQiLqC0dMSgYHbtEhcAGdrFqhdI4YoOTjtMW1zZA|0
5|titi|$argon2id$v=19$m=65536,t=4,p=1$U1Y2dm44cXlMWHp0SkFhVg$lYj9kLfx6zUNOTDUXHDhpEFyBdSJXDv9qjxA0+4oEYw|0
6|attacker|$argon2id$v=19$m=65536,t=4,p=1$NmFqZVhOaTRsN2t4L3lWRw$UJbZAJcm7ujhLv/Y/DcyMDqD42ME95l+Cd5Kh2XJ2G0|0
Then in the login form, the user will be redirected to a password reset form if their password has not been changed, otherwise they can access the platform.
Here we save the user’s login or name in the session after validating the connection.
Finally, the code to process the password change form will be as follows:
Note: Here we only check that the old password is different from the new one. Ideally, we should also check that the password follows a strong policy.
Passwords are stored with an unsuitable hash (md5, sha1, sha2, sha3…)
Migrating hash to Argon2id
In this case, the data migration is more complex, because the passwords are not identifiable. Here, what we advise is to do Argon2id directly on the hash stored in the database.
For our example, we place ourselves in a case where the hash md5 is used, with salt and pepper to be in the most complex case possible.
The passwords are therefore stored in the database in this way: md5 (salt + password + pepper).
The connection will thus have this form:
The conversion code will be the same as the previous example. Indeed, it is directly the md5 hash that we want to encode, a value that is already present in the database (password column).
The login form will be more complex, however, as we have several conditions to validate:
- When the user logs in for the first time, ask for a new password and store only the argon2id of that password
- Have to handle 2 cases in the login form:
- If the column isUpdatePassword is false, then the user has not changed their password and the hash will be: argon2id (md5(salt + password + pepper).
- If the column isUpdatePassword is false, then the user has changed their password and the hash will be: argon2id (password + pepper).
And the reset form will be as follows:
If you use a suitable hash
Algorithms considered reliable for storing passwords have a positive point: they can adapt with time and the evolution of hardware by increasing their cost. It can therefore be relevant to anticipate the evolution of algorithms in your login form.
In PHP you can use the password_needs_rehash function.
Author : Thomas DELFINO – Pentester @Vaadata