Fohlarbee - BlogHash passwords and compare for similarities

All what you are about to read would have been unnecessary but users do forget their passwords, if not, we can collect old passwords and compare with new one but like I said users do forget their passwords, and that has brought us here.
Let me walk you on how to Hash passwords and still be able to compare for similarities.
There is a difference between checking for exact match and checking for similarities, we will do both, although the latter is why I'm writing this.
We will be working with LSH (Locality sensitive hashing) which allows similar plaintext to have similar hashes hence, no avalanche effect.
Perequisites
packages
- BcryptJS (for Deterministic and avalanche effect hash algo).
- SimHash (for LSH-based hashing)/
- Crypto (for AES-256 encryption and decryption).
- Fast-levenshtein (for similarity comparison).
- Dotenv (for reading env file).
1. Create env file and save secret encryption key and Initialization vector key
.env
AES_KEY=ypurAESSecretKey
IV_KEY=yourIVSecretKey
You can generate random strings to use as your secret key and IV, these should not be exposed.
2. Hash Password
We will be hashing the password twice, using bcrypt and again using simHash for LSH hashing.
JavaScript
const bcrypt = require('bcrypt');
const simHash = require('simHash');
const password = password123;
// Gen salt
const salt = bcrypt.genSalt(10);
// Hash password with bcrypt
const passwordHash = bcrypt.hash(password, salt);
// Hash password with LSH
let passowrdLSH = simHash(password);
code explanation
- Import packages.
- Have your password ready i.e (password123).
- Generate salt using bcrypt.
- Hash password with bcrypt (passwordHash).
- Hash password with simHash (passwordLSH).
3. Encrypt LSH hashed password
JavaScript
const crypto = require('crypto');
cons dotenv = require('dotenv');
// Import secret keys from .env file
const key = process.env.AES_KEY;
const iv = process.env.IV_KEY;
//Encryption function
function encryptLSH(lshHash, secretKey, secretIV){
const cipher = crypto.createCipheriv('aes-256-cbc', secretKey, secretIV);
let encrypted = cipher.update(lsHash, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {encryptedData:encrypted, iv: iv.toString('hex')};
}
// Encrypt LSH hash, pass your LSH hashed password (passwordLSH),
secret key and iv as arguments to the encryption function.
const encryptedPasswordLSH = encryptLSH(passwordLSH, key, iv);
code explanation
- Import packages.
- Import keys from .env file.
- Define and create encryption function. You can tweak as you want.
- Call function and pass arguments.
4. Save Passwords to DB
JavaScript
// You should have both your bcrypt hashed password (passowrdHash) and your LSH hashed and AES-256 encrypted password (encryptedPaswordLSH)
// Save to DB
savePasswordsToDB(passwordHash, encryptedPasswordLSH);
5. Update new Password
When a user tries updating password.
(JavaScript
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const simHash = require('simHash');
const dotenv = require('dotenv');
// Import secret keys from .env file
const key = process.enc.AES_KEY;
const iv = process.env.IV_KEY;
// User's new password
const newPassword = newPassword123;
// Fetch your two passwords, bcrypt hashed and LSH hashed and AES encrypted from DB
const {passwordHash, encryptedPasswordLSH} = await fetchPasswordsFromDB();
// Check if new password is same as passwordHash using bcrypt
const isSame = bcrypt.compare(newPassword, passwordHash);
// Don't allow password change if passwords match
if (isSame) return {mssg: "You can't use your old password as the new one", statusCode: 401}
// Below code will only run if passwords don't match
// Decryption function
function decryptLSH(encryptedPassword, secretKey, secretIV){
const decipher = crypto.createDcipheriv('aes-256-cbc',
secretKey, Buffer.from(secretIV, 'hex'));
let decrypted = decipher.update(encryptedPassword, 'hex',
'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}// Decrypt LSH hash for similarity comparison. Pass the encryptedPasswordLSH, key and IV as arguments to the decryption function.const decryptedLSHPassword = decryptLSH(encryptedPasswordLSH,
key, iv);
// DecryptedLSHPassword should match original LSH hash// To compare new password with our decryptedLSHPassword which is now a LSH Hash, we have to LSH hash newPassword
const newPasswordLSH = simHash(newPassword);
Code explanation
- Import packages.
- Get env secret key and IV.
- Collect user's new password.
- Get user's two hashed passwords from db.
- Compare user's new password with current password (use the bcrypt hashed password) check with bcrypt.compare().
- Don't allow password change if new password matches current password (passwordHash)(exact match). Return a not allowed message.
- If password don't match, decrypt the encrypted password gotten from db (encryptedPasswordLSH).
- LSH hash new password (newPassword) to be able to compare it with decryptedLSHPassword.
6. Comparing decrypted hashed password(decryptedLSHPassword) with new password LSH hash (newPasswordLSH) for similarities.
To achieve this we have to compare the two hashes with a distance (similarities) calculating algorithm.
We will work with Levenshtein distance algo.
The Levenshtein distance measures the number of single-character edits (insertions, deletions, or substitutions) required to transform one string into anotherJavaScript
const levenshtein = require('fast-levenshtein');
const dotenv = require('dotenv');
// Import secret keys from .env file
const key = process.env.AES_KEt key = process.env.AES_KEY;
const iv = process.env.IV_KEY;
// Set a threshold number. This is the number of single character edits we want to check for.
const threshold = 3;
// Similarities check function
function areSimilar(newPasswordLSH, oldPasswordLSH, threshold){
const distance = levenshtein.get(newPasswordLSH, oldPasswordLSH);
return distance <= threshold; // If distance is within the
threshold, it is too similar
}
// Call function and pass newPasswordLSH and decryptedLSHPassword as arguments.
const similar = areSimilar(newPasswordLSH, decryptedLSHPassword);
// If similar, then passwords have similarities within the set threshold, in our case less than or equal to 3, and if similar === false then passwords have similarities that are above the set threshold or have no similarities
// You can handle the outcome as you wish. Look at the example below.
if (similar) {
return {mssg: "Your new password can't be similar to your current password", statusCode: 401}
} else {
// We will first encrypt the new password. You remember our
encryption function right?
const encryptedNewPasswordLSH = encryptLSH(newPasswordLSH,
key, iv);
}
// Update password in DB
await updatePasswordInDB(encryptedPasswordLSH);
return {mssg: "Password updated successfully", statusCode: 201}
Finally, we've securely stored our hashed password and can efficiently compare for similarities. While two password versions are saved in the database, there;s no need for concern--one is bcrypt hashed, and the other is LSH hashed with AES-256 encryption.
What do you think about the hashing and comparing technique? Have you implemented such in any of your programs? Let's hear your thoughts in the comment section.
For more programming contents kindly follow me on Twitter, LinkedIn.