'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
var protobuf = require('protocol-buffers');
/**
* The password module provides utilities for dealing with passwords. This
* includes hashing and verifying passwords as well as derive secure
* cryptographic keys from passwords.
* @module easy-crypto/password
*/
/**
* The message format for encoding and decoding data using protobuf.
* @constant {Object}
* @inner
*/
const messages = protobuf(
fs.readFileSync(path.resolve(__dirname, './password.proto'))
);
/**
* An error that hints that the hashing algorithm used is no longer valid and a
* rehash is required.
* @constant {Error}
* @since 0.2.0
* @static
*/
const InvalidHashError = new Error('Invalid algorithm, rehash required.');
/**
* Derive a cryptographically secure key using a password and a salt.
* @since 0.2.0
* @async
* @function deriveKey
* @param {Data} password The password to be used
* for key derivation.
* @param {Data} salt The salt to be applied to the
* password. The salt should be as unique as possible. It is recommended that a
* salt is random and at least 16 bytes long. See NIST SP 800-132 for details.
* @param {number} iterations The number of iterations to be performed. The
* value must be a number set as high as possible. The higher the number of
* iterations, the more secure the derived key will be, but will take a longer
* amount of time to complete.
* @param {number} keylen The length of the key to be produced.
* @param {string} digest The HMAC digest algorithm to be used.
* @returns {Promise<Buffer>} The derived key.
* @static
*/
function deriveKey(password, salt, iterations, keylen, digest) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, keylen, digest, (err, key) => {
if (err) reject(err);
resolve(key);
});
});
}
/**
* Derive a cryptographically secure key synchronously using a password and a salt.
* @since 0.2.0
* @function deriveKeySync
* @param {Data} password The password to be used
* for key derivation.
* @param {Data} salt The salt to be applied to the
* password. The salt should be as unique as possible. It is recommended that a
* salt is random and at least 16 bytes long. See NIST SP 800-132 for details.
* @param {number} iterations The number of iterations to be performed. The
* value must be a number set as high as possible. The higher the number of
* iterations, the more secure the derived key will be, but will take a longer
* amount of time to complete.
* @param {number} keylen The length of the key to be produced.
* @param {string} digest The HMAC digest algorithm to be used.
* @returns {Buffer} The derived key.
* @static
*/
function deriveKeySync(password, salt, iterations, keylen, digest) {
return crypto.pbkdf2Sync(password, salt, iterations, keylen, digest);
}
/**
* Hash a password for storage.
* @since 0.2.0
* @async
* @function hashPassword
* @param {Data} password The password to be hashed.
* @returns {Promise<Buffer>} The hashed password optimized for storage.
* @static
*/
function hashPassword(password) {
const salt = crypto.randomBytes(32);
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, hashedPassword) => {
if (err) return reject(err);
resolve(
messages.Password.encode({
algorithm: messages.Algorithm.SCRYPT,
salt,
length: 64,
hash: hashedPassword
})
);
});
});
}
/**
* Hash a password synchronously for storage.
* @since 0.2.0
* @function hashPasswordSync
* @param {Data} password The password to be hashed.
* @returns {Buffer} The hashed password optimized for storage.
* @static
*/
function hashPasswordSync(password) {
const salt = crypto.randomBytes(32);
const hashedPassword = crypto.scryptSync(password, salt, 64);
return messages.Password.encode({
algorithm: messages.Algorithm.SCRYPT,
salt,
length: 64,
hash: hashedPassword
});
}
/**
* Verify a previously hashed and stored password.
* @since 0.2.0
* @async
* @function verifyHash
* @param {Buffer} hashed The hashed password to be verified.
* @param {Data} password The actual password.
* @returns {Promise<boolean>} Wether the hash was valid for the given password.
* @static
*/
function verifyHash(hashed, password) {
return new Promise((resolve, reject) => {
const { algorithm, salt, length, hash } = messages.Password.decode(hashed);
if (algorithm !== messages.Algorithm.SCRYPT || hash.length !== length)
return reject(InvalidHashError);
crypto.scrypt(password, salt, 64, (err, recomputed) => {
if (err) return reject(err);
resolve(crypto.timingSafeEqual(recomputed, hash));
});
});
}
/**
* Verify a previously hashed and stored password synchronously.
* @since 0.2.0
* @function verifyHashSync
* @param {Buffer} hashed The hashed password to be verified.
* @param {Data} password The actual password.
* @returns {Promise<boolean>} Wether the hash was valid for the given password.
* @throws {module:easy-crypto/password.InvalidHashError} The hash was produced
* using an invalid algorithm.
* A rehash with the currently valid algorithm is required.
* @static
*/
function verifyHashSync(hashed, password) {
const { algorithm, salt, length, hash } = messages.Password.decode(hashed);
if (algorithm !== messages.Algorithm.SCRYPT || hash.length !== length)
throw InvalidHashError;
const recomputed = crypto.scryptSync(password, salt, 64);
return crypto.timingSafeEqual(recomputed, hash);
}
module.exports = {
deriveKey,
deriveKeySync,
hashPassword,
hashPasswordSync,
verifyHash,
verifyHashSync,
InvalidHashError
};