Password Hashing with Node JS
2022-12-23
Passwords must be stored in a database. This research investigates the manipulation of plain text passwords for storage purposes, the manner in which the algorithm modifies password messages and how this algorithm ensures password security. The NodeJS runtime has libraries available that can be employed to manipulate password messages. An exploration of how to implement these libraries with algorithms to secure passwords.
NodeJS is a run time javascript engine that allows javascript to be run away from the browser. It can be used for server-sided application. The Crypto library and other third party libraries will be used to show how to utilize the password hashing algorithms.
Encrypting passwords is a way to obscure the text of the passwords, thus preventing them from being stored in plain text and potentially being read by a malicious actor. The issue with password encryption is that, should a hacker manage to ascertain the encryption key, they would be able to decrypt all of the passwords rapidly. Hern (n.d.) covered in The Guaradian about Adobe encrypting all the passwords with the same key. Upon the breaching of Adobe's database by hackers, it was revealed that Adobe had utilized the same encryption for storing all passwords. Hackers were able to deduce the key to the passwords and decrypt the millions of passwords from users.
To implement encrypting with NodeJS, the Crypto library can be used. The algorithm used is ase-256-cbc. The key and iv must be the same in order to result in the same encryption for the same password. The issue that can be noticed from encryption is when the key, 7c6d943cfbe0b24d74c829b2965c4fffe39cca30dd34b11af53cc313d5009a41, is compromised or cracked, all of the passwords can be decrypted.
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key =
Buffer.from("7c6d943cfbe0b24d74c829b2965c4fffe39cca30dd34b11af53cc313d5009a41", "hex");
const iv = Buffer.from("a396af7cd1c31c059b80b0fdebdf3af6", "hex");
const message = 'Password1';
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key, "hex"), iv);
const encryptedMessage = cipher.update(message, 'utf8', 'hex')
cipher.final('hex');
console.log("Encrypted Message: " + encryptedMessage);
A better way to manipulate the password is to not be able to go backwards from the scrambled text. A hash function is a function that maps data to a fixed-sized data. When it comes to hash functions and passwords, a password message is hashed to another message. Important features of a hash function with passwords are the one-way function, whereby a message cannot be reversed, small changes to the input create varying digested messages, and there is a low probability of multiple hash inputs creating the same output. Owing to these features, it is preferable to store passwords with hashing instead of encryption or plain text (Mayoral, 2013).
The message digest 5 hashing algorithm was designed by Ronald Rivest in 1991 (Mayoral, 2013). The MD5 algorithm computes incredibily fast and it takes up low resources. However, this is not helpful for hashing a password as a hacker can hash millions of possible values in a quick amount of time to try to figure out the password. In addition to the speed of hashing, there is chance of hash collision with two different values being the same hash value (Mayoral, 2013).
Before the MD5 hashing algorithm goes through the hashing function, the message is changed to be a multiple of 512 bits. First, the message is padded with a one, then it is padded with zeros until it is 64 bits short of being a multiple of 512 bits. Afterward, the length of the message is appended to result in a message that has a multiple of 512 bits. If the length was greater than two to the power of sixty-four, then only its lower 64 bits are appended to the message (Rivest, 1992).
Once the message has been created to be a multiple of 512 bits, the hashing function begins to hash the message. There is an array of 64 fixed constant values, which is generated by doing floor 2^32 * abs (sin(i + 1))). An additional array is created of 64 integers, which is broken into 4 subblocks that consist of the per round bit shift amounts. Four words, 32 bits variables are initialized, which can be represented as A, B, C, D. The message is broken down into sixteen words and goes through the function steps (Rivest, 1992).
Each function will have the result of the function added to A, an element of constant value, and the 32-bit block of the broken down word. That result is bit shifted by the bit shift array amount and added to B. The A, C, and D words are rotated by A becoming D, D becoming C, and C becoming B. The four function iterations are (B AND C) OR ((NOT B) AND D), (D AND B) OR ((NOT D) AND C), B XOR C XOR D, and C XOR (B OR (NOT D)) (Rivest, 1992).
The Node.js Crypto library does have the MD5 algorithm, however, it is not recommended to use. The following JavaScript code can be run on Node.js and uses the MD5 algorithm from the Crypto library. The 'str' variable holds the value of the plain text password. The 'crypto' imports the library, then 'hash' uses MD5 to process a plain text password string; finally, after hashing, 'digest' will encode the return value into a hex string.
const str = "password";
const crypto = require('crypto');
const hash = crypto.createHash('md5').update(str).digest('hex');
console.log(hash);
The Secure Hash Algorithm (SHA) is part of a group of hashing functions published by the U.S. Federal Information Processing Standard. The reason for SHA being secure is the impossibility of getting the original message from its digested message and the highly improbable chance of two messages having the same hash value, termed as a hash collision. In addition to being secure, SHA makes even small changes to a message result in very different digest messages (Secure Hash Standard, 2002).
SHA preprocessing is similar to the MD5 algorithm. For all SHA, padding will add the first bit with a one and the rest of the bits with zeros. Padding for zero bits will go up to 64 less than a multiple of 512 bits for SHA-1 and SHA-256, and up to 128 bits before a multiple of 1024 for SHA-384 and SHA-512. The SHA-1 and SHA-256 will parse 512 bit blocks into 16 32-bit words. Meanwhile,SHA-384 and SHA-512 will parse 1024 bit blocks into 16 64 word blocks. Constants for SHA 1 are 80 32 bit words; while those of SHA 256 are 64 32bit words with first 32bits as fractional parts of cube roots plus 64 prime numbers; lastly, those constants for SHA 384 and SHA512 are 80 64 bitwords where the first 64 are fractional cube roots plus 80 prime numbers (Secure Hash Standard, 2002).
SHA-1, SHA-256, SHA-384, and SHA-512 can be used with Node.js. The SHA-1 is not recommended by the Node.js library and links back to the NIST Special Publication 800-131A Revision 1 report saying that SHA-1 is too weak to be used for digital signatures and has a high chance for hash collisions. Below is JavaScript code that will hash the "password" string through the different SHAs: The higher theSHA number, the greater message hash is outputted (Secure Hash Standard, 2002).
const str = "password";
const crypto = require('crypto');
const hash1 = crypto.createHash('sha1').update(str).digest("hex");
const hash256 = crypto.createHash('sha256').update(str).digest("hex");
const hash384 = crypto.createHash('sha384').update(str).digest("hex");
const hash512 = crypto.createHash('sha512').update(str).digest("hex");
console.log(hash1); // SHA 1
console.log(hash256); // SHA 256
console.log(hash384); // SHA 384
console.log(hash512); // SHA 512
When it comes to users entering passwords, it is common for the same person to have multiple passwords. When there are millions of accounts with passwords stored in a database, there is a high probability that some of those passwords are the same. To protect the passwords from a rainbow table or dictionary attack, a message (salt) is concatenated to the password before it has been hashed. The salt must be different for each password, resulting in different hash outcomes even when two passwords are identical. The salt may be stored in plain text, as knowledge of the salt does not enable an attacker to determine the message (Mayoral, 2013).
The issue with SHA and MD5 algorithms is the speed of computation, which renders them vulnerable to brute force attacks. To combat this, one method that can be implemented is key stretching, which would increase the time complexity of the hash. However, the time needed to hash each individual password must be kept to a maximum, but also to avoid disrupting user convenience. Some examples of key stretching algorithms is PBKDF2, bcrypt, and Argon2 (Mayoral, 2013).
The Password-Based Key Derivation Function (PBKDF) is an algorithm that creates a cipher key that can be used for hashing a password. The way in which PBKDF extends the key is through an iteration count. For each iteration, the hashed password message is XORed with the newly digested message, which is then used for the subsequent hash iteration, thus augmenting the time complexity of the hashing algorithm employed. Another requirement for PBKDF is a salt, which it is recommended to be generated by a random byte generator from the Computer Security Division Information Technology Laboratory of the United States Department of Commerce (Aliski, 2000).
The PBKDF2 algorithm in the NodeJS Crypto library requires a plain text password, salt, iteration count, the length of output key, and the hashing algorithm to be used after the created key. If the iteration count is increased, so will be the time taken to resolve the hashed value. In addition, the algorithm that newly generate key should be hashed with is needed.
const crypto = require('crypto');\\
const password = "password";\\
const iterations = 100000;\\
const keyLength = 64;\\
const hashDigest = "sha512";\\
let salt = crypto.randomBytes(16).toString('hex');
let hashedPassword = crypto
.pbkdf2Sync(password, salt, iterations, keyLength, hashDigest)
.toString('hex');
console.log(hashedPassword);
Another way to do key stretching is with the Blowfish cipher. The Blowfish algorithm will XOR the left half of data with an index of the 64-bit array. That XOR value is used as input for the Blowfish's F-function. The F-function will split the 32 bits into four 8-bit blocks. The first block is added to the second and modulo by 2^32, and that value is then XORed with the third 8-bit block. Lastly, that value is added to fourth block and modulo by 2^32. The output from this F-function is XORed with right half of data; then, left side and right side data are swapped. This output value is reiterated 16 more times for producing a final Blowfish cipher key (Schneier, n.d.).
The bcrypt algorithm utilizes the Blowfish cypher to increase the computation time of the hash. The "b" stands for Blowfish and "crypt" is derived from the UNIX password system. The crypt algorithm takes a two-character salt and password to hash the password. The issue with crypt was that computers became faster over the years, so crypt became too fast to hash passwords effectively. With the Blowfish cypher key on the crypt algorithm, it will increase the computation time for later years. The cost factor in bcrypt exponentially increases its computation time, making it secure for future years to come; as technology gets faster, this cost factor can be increased to stay ahead of any technological advances (Arias, n.d.).
How the bcrypt hashing algorithm works is broken up into two phases. The first phase is the EksBlowfishSetup function, which takes the cost factor, salt, and password message as inputs and returns the subkeys used for hashing the password. The EksBlowfishSetup adds the salt to the password and does a key expansion to increase computation time. The subkeys keep reiterating itself for 2\textsuperscript{cost} times. The second phase is OrpheanBeholderScryDoubt; this 192-bit value is hashed with blowfish in block cipher mode, resulting in a hashed password (Arias, n.d.).
Bcrypt is not part of the Crypto library, so another library has to be installed with "npm i bcrypt". The bcrypt library will generate the salt. When the hash is outputted, it will include the salt at the beginning of the hash return value with a dollar sign format to indicate the version number, salt rounds (cost factor), and salt value.
const bcrypt = require("bcrypt"); \\
const saltRounds = 10; \\
const plainText = "PasswordToBeHashed";\\
let hash = "";\\
bcrypt.genSalt(saltRounds).then(salt => {
console.log(`Salt: \${salt}`);
return bcrypt.hash(plainText, salt);
})
.then(hash => {
hash = hash;
console.log(`Hash: \${hash}`);
}
.catch(err => console.error(err.message));
bcrypt.compare(plainTextCompare, hash).then(result => {
console.log(result);
})
.catch(err => console.error(err.message));
One drawback to bcrypt and pbkdf2 is that the memory used during computation of the hash is not increased. Alex Biryukov, Danial Dinu, and Dmitry Khovratovich designed Argon2 hashing algorithm that can vary the time complexity and memory complexity of the hash. The memory-hard function goal is to have the highest memory filling and defend from tradeoff attacks. There are two different variants of Argon2 consist of Argon2d and Argon2i. Argon2d is for cryptocurrencies and appplication with no timing attacks from others because it is fast and uses data-depending memory access. Argon2i is for password hashing because Argon2i is slower and makes more checks on the memory to protect from timing attacks (Biryukov, Dinu, & Khovratovich, 2017).
For the memory-hard function, there is a compression function that fills up the memory array. The compression function will take two 1024-byte blocks, A and B. First, A and B are XORed with each other to produce X. X is split into an 8 by 8 matrix upon which the Blake2b round function computes the rows, followed by the columns. The final value after Blake2b is then XORed with the original X file (Biryukov, Dinu, & Khovratovich, 2017).
Each memory index possesses an indexing function, which is either independent of the password and salt, or dependent on the password. The dependent indexing function helps to thwart attempts to ascertain the missing data for timing attacks in computing with memory. Data-dependent addressing takes the block from two indexes prior to being input into the index function, thus making it difficult for malicious actors to tamper with memory and precompute a hash (Biryukov, Dinu, & Khovratovich, 2017).
Below is the Argon2 library on Node.js that binds to the C Argon2 library. There are more variables than in other hashing algorithms, such as hashLength, which is the number of bytes; timeCost, which is how much time should be taken up; memoryCost, which is the amount of memory Argon2 should take up; parallelism, which is the number of processes to execute memory hashing; and type, whether to use 0 for Argon2di, 1 for Argon2d or 2 for Argon2i.
const argon2 = require('argon2');
const crypto = require('crypto');
const hashLength = 4294967295;
const timeCost = 10;
const memoryCost = 1024;
const parallelism = 1;
const type = 2;
let salt = crypto.randomBytes(16);
const password = 'This is my super secret password';
argon2.hash(password, {
salt: salt,
hashLength: hashLength,
timeCost: timeCost,
memoryCost: memoryCost,
parallelism: parallelism,
type: type\})
.then(hash => {
console.log('Hash:', hash);
});
Aliski, B. (2000). Pkcs #5: Password-based cryptography specification version 2.0 (Tech. Rep.). RSA Laboratories.
Arias, D. (n.d.). Hashing in action: Understanding bcrypt. Retrieved 2021-02-25, from https://auth0.com/blog/ hashing-in-action-understanding-bcrypt/
Biryukov, A., Dinu, D., & Khovratovich, D. (2017). Argon2: the memory-hard function for password hashing and other applications (Tech. Rep.). University of Luxenbourg, Luxembourg.
Hern, A. (n.d.). Did your adobe password leak? now you and 150m others can check. The Guardian. Retrieved 2013-11-07, from https://www.theguardian.com/technology/2013/nov/07/adobe-password-leak-can-check
Mayoral, F. (2013). Instant java password and authentication security (B. Bahrenburg, Ed.). Packt Publishing.
Rivest, R. (1992). The md5 message-digest algorithm (Tech. Rep.). MIT Laboratory for Computer Science and RSA Data.
Schneier, B. (n.d.). Description of a new variable-length key, 64-bit block cipher (blowfish) (Tech. Rep.). Retrieved from https://www.schneier.com/academic/archives/1994/09/description_of_a_new.html
Secure hash standard (Tech. Rep.). (2002). Federal Information Processing Standards Publication.