@livingdocs/secure-password
Version:
Making Password storage safer for all
177 lines (151 loc) • 5.62 kB
JavaScript
const {randomBytes, timingSafeEqual} = require('crypto')
const {hashRaw} = require('@node-rs/argon2')
const generateSalt = require('util').promisify(randomBytes)
const VERSION = 0x13
// @node-rs/argon2 Version enum: V0x10 = 0 (argon2 version 16), V0x13 = 1 (argon2 version 19)
const versions = Object.freeze({0x10: 0, 0x13: 1})
const types = Object.freeze({
'0': 0,
'1': 1,
'2': 2,
argon2d: 0,
argon2i: 1,
argon2id: 2
})
const names = Object.freeze({
[types.argon2d]: 'argon2d',
[types.argon2i]: 'argon2i',
[types.argon2id]: 'argon2id',
})
const limits = Object.freeze({
hashLength: { max: 4294967295, min: 4 },
memoryCost: { max: 4294967295, min: 2048 },
timeCost: { max: 4294967295, min: 2 },
parallelism: { max: 16777215, min: 1 },
passwordLength: { min: 0, max: 4294967295 }
})
// Attention, the old secure-password had different options
// timeCost=2, hashLength=32, memoryCost=65536
// memoryCost is now also in kilobytes instead of bytes
const defaults = Object.freeze({
hashLength: 32,
saltLength: 16,
timeCost: 3,
memoryCost: 65536,
parallelism: 1,
type: types.argon2id,
version: VERSION
})
const VALID = Symbol('VALID')
const INVALID = Symbol('INVALID')
const VALID_NEEDS_REHASH = Symbol('VALID_NEEDS_REHASH')
const INVALID_UNRECOGNIZED_HASH = Symbol('INVALID_UNRECOGNIZED_HASH')
securePassword.limits = limits
securePassword.defaults = defaults
securePassword.INVALID_UNRECOGNIZED_HASH = INVALID_UNRECOGNIZED_HASH
securePassword.INVALID = INVALID
securePassword.VALID = VALID
securePassword.VALID_NEEDS_REHASH = VALID_NEEDS_REHASH
securePassword.securePassword = securePassword
class AssertionError extends Error {}
AssertionError.prototype.name = 'AssertionError'
function assert (t, m) {
if (t) return
const err = new AssertionError(m)
Error.captureStackTrace(err, assert)
throw err
}
function assertBetween (value, {min, max}, key) {
if (value >= min && value <= max) return
const err = new AssertionError(`${key} (${value}), must be between ${min} and ${max}`)
Error.captureStackTrace(err, assertBetween)
throw err
}
const {serialize, deserialize: _phcDeserialize} = require('@phc/format')
// Removes trailing null bytes from a buffer and deserializes it
function deserialize (hashBuf) {
try {
const i = hashBuf.indexOf(0x00)
if (i !== -1) hashBuf = hashBuf.slice(0, i)
return _phcDeserialize(hashBuf.toString())
} catch (err) {
return
}
}
function needsRehash (deserializedHash, {version, memoryCost, timeCost}) {
const {version: v, params: {m, t}} = deserializedHash
return +v !== version || +m !== memoryCost || +t !== timeCost
}
function recognizedAlgorithm (deserializedHash) {
if (!deserializedHash) return false
return types[deserializedHash.id] !== undefined
}
async function argon2Verify (deserializedHash, passwordBuf, secret) {
const {id, version = 0x10, params: {m, t, p}, salt, hash} = deserializedHash
return timingSafeEqual(
await hashRaw(passwordBuf, {
salt,
outputLen: hash.byteLength,
memoryCost: +m,
timeCost: +t,
parallelism: +p,
version: versions[+version] ?? 1,
algorithm: types[id],
secret: secret && secret.length ? secret : undefined,
}),
hash
)
}
function securePassword (opts = {}) {
const options = Object.freeze({...defaults, ...opts})
const secret = options.secret ? Buffer.from(options.secret) : Buffer.alloc(0)
const type = opts.type !== undefined ? types[opts.type] : defaults.type
assert(type, 'Invalid type, must be one of argon2d, argon2i or argon2id')
const serializeOpts = Object.freeze({
id: names[type],
version: VERSION,
params: {
m: options.memoryCost,
t: options.timeCost,
p: options.parallelism
}
})
assertBetween(options.hashLength, limits.hashLength, 'Invalid options.hashLength')
assertBetween(options.memoryCost, limits.memoryCost, 'Invalid options.memoryCost')
assertBetween(options.timeCost, limits.timeCost, 'Invalid options.timeCost')
assertBetween(options.parallelism, limits.parallelism, 'Invalid options.parallelism')
async function hash (passwordBuf) {
assert(passwordBuf instanceof Uint8Array, 'Invalid passwordBuf, must be Buffer or Uint8Array')
assertBetween(passwordBuf.length, limits.passwordLength, 'Invalid passwordBuf length')
const salt = await generateSalt(options.saltLength)
const hash = await hashRaw(passwordBuf, {
salt,
outputLen: options.hashLength,
memoryCost: options.memoryCost,
timeCost: options.timeCost,
parallelism: options.parallelism,
version: versions[options.version],
algorithm: options.type,
secret: secret.length ? secret : undefined,
})
return Buffer.from(serialize({
id: serializeOpts.id,
version: serializeOpts.version,
params: serializeOpts.params,
salt,
hash
}))
}
async function verify (passwordBuf, hashBuf) {
assert(passwordBuf instanceof Uint8Array, 'Invalid passwordBuf, must be Buffer or Uint8Array')
assert(hashBuf instanceof Uint8Array, 'Invalid hashBuf, must be Buffer or Uint8Array')
assertBetween(passwordBuf.length, limits.passwordLength, 'Invalid passwordBuf')
const deserializedHash = deserialize(hashBuf)
if (recognizedAlgorithm(deserializedHash) === false) return INVALID_UNRECOGNIZED_HASH
if (await argon2Verify(deserializedHash, passwordBuf, secret) === false) return INVALID
if (needsRehash(deserializedHash, options)) return VALID_NEEDS_REHASH
return VALID
}
return {hash, verify}
}
module.exports = securePassword