General description
Password security is one of the most important and critical issues in the planning and development of backend services. To protect user accounts, passwords should never be stored in plain text in a database.
Why hashing?
The Internet has indisputably become an extremely relevant medium for communication. In addition to normal surfing on news portals, the use of platforms such as Facebook in particular is now an everyday activity. In addition to email services, social media platforms are used, invoices are processed online in booking management and much more. The protection of the user accounts used in this process, and thus also of the personal data of a potential user, should be at the forefront of the design of such an application.
One parameter that contributes to application security is the hashing of passwords. The following illustration will explain how hashing is used in practice.
An illustration
Have you ever forgotten your password for an online service? Have you perhaps already been told by an online platform that no one can know your password except you? If yes, then good. If the developers of this platform have done everything right, then no one but yourself actually knows the password you have chosen. Hashing makes it unrecognizable, without any possibility to undo the process. undo the process.
You might be wondering how you can log in to your account at all, if no one can trace which password you have chosen?
The process is quite simple, as the following example will show:
Suppose you create an account at https://intyme.io and choose the password "123456".
As soon as you press the "Create account" button, the following happens on the server:
The server uses a chosen hashing algorithm, for example "Bcrypt" or "Argon2", which converts your password into a cryptic string by applying mathematical functions.
If you use Bcrypt with a cost factor (the explanation follows) of 12, then you get the following result for the password "123456":
$2y$12$.Y5cUcilrbXvIkdKGsphyeicTX047Pe2qLqUUZCztjX0PMGSbE7Le
This is not a coincidence at all. Hashing algorithms are usually deterministic, meaning that they always generate the same output for the same input. This string is now stored in the database. The next time you log in and then enter your password, the string stored in the database is compared with the hash of the password you just entered. If the two strings match, then you have entered the correct password and will be logged into the application.
Of course, you now have the legitimate question:
Correct - this is exactly what can be done and is also common practice in attacks. These tables are called rainbow tables and contain a resulting hash of all possible input values."If I can simply map the resulting strings to an input value, couldn't I simply create a list of input values and their associated output values that would help me find the correct password for each hash? Then the whole thing would ultimately be useless against attacks."
Salting & cost factor
Fortunately, algorithms like Bcrypt, Argon2 or PBKDF integrate an intelligent "salting" mechanism to prevent this kind of attack. In this mechanism, a randomly generated string is attached to the password to be hashed before the actual hashing is performed. With this additional randomly generated string, these Rainbow Tables would have to become unimaginably large to be effective.
In order to also be protected against dictionary attacks or brute force attacks, the "cost factor" already mentioned comes into play.
Basically, the higher the cost factor is chosen, the more computing power must be used for the calculation. A cost factor of 12 means 212 iterations per hash, i.e. 4096 iterations. The slower the generation of hashes is accordingly.
Calculation example:
- As a basis serves a 8 character password: r3Dcr0W5.
- Each character position results in 62 possibilities: [a-z,A-Z,0-9]
- Therefore a total of 628 possibilities.
- With a cost factor of 10, an i7 2700K processor can compute circa 12 hashes per second.
- So to generate all iterations it takes (628) / ( 60 * 60 * 24 * 365 * 12 ) years - That's a total of about 580,000 years - so enough time
With increasing hardware power, much higher frequencies can be achieved in the generation of individual hashes. Nevertheless, this example is representative of the effectiveness of slow hashing algorithms like Bcrypt.
Example implementation of Bcrypt on Node.js
The following implementation should show how simple the actual use of a hashing algorithm can be. The Bcryptjs library for Node.js is used.
/* Example implementation of a pre store action
* which hashed the provided password field
* before it is stored in the database
*/
UserSchema.pre("save", async function (next) {
// cost factor
const SALT_ROUNDS = 12;
try {
let user = this;
// generate a salt
let salt = await bcrypt.genSalt(SALT_ROUNDS);
// hash the provided password
let hash = await bcrypt.hash(user.password, salt);
// replace user password
user.password = hash;
// ...store in db or other actions
next();
} catch (error) {
// ... error handling
}
});
Conclusion:
In any case, it is worth protecting passwords against conventional attacks by hashing and salting. However, the responsibility lies equally with the developer and the user to treat passwords with the appropriate relevance. Good hashing algorithms are no excuse for weak passwords or careless password handling. Strong passwords, on the other hand, are no excuse for negligence in hashing/encryption. Only the combination of both achieves the desired effect.