Prettier+some nitpicks

This commit is contained in:
D. Scott Boggs 2023-09-26 08:45:29 -04:00
parent 8c18fc4f92
commit 1432f43035
5 changed files with 369 additions and 287 deletions

View file

@ -60,7 +60,6 @@ module.exports = (sequelize, DataTypes) => {
sequelize, sequelize,
tableName: 'Users', tableName: 'Users',
modelName: 'User', modelName: 'User',
underscored: true,
}, },
) )
return User return User

279
index.js
View file

@ -1,12 +1,17 @@
/** /**
* Helpers to make using node's built-in scrypt implementation more * Helpers to make using node's built-in scrypt implementation more
* straightforward to use and testable * straightforward to use and testable
* *
* See https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback * See https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback
* https://stackoverflow.com/questions/62908969/password-hashing-in-nodejs-using-built-in-crypto * https://stackoverflow.com/questions/62908969/password-hashing-in-nodejs-using-built-in-crypto
* *
*/ */
const { scrypt, scryptSync, timingSafeEqual, randomBytes } = require('node:crypto') const {
scrypt,
scryptSync,
timingSafeEqual,
randomBytes,
} = require('node:crypto')
/// Node recommends at least 16 at time of writing. 24 is more than that and /// Node recommends at least 16 at time of writing. 24 is more than that and
// also will result in a base64 string which doesn't contain padding, which is // also will result in a base64 string which doesn't contain padding, which is
@ -24,18 +29,20 @@ const DEFAULT_KEY_LENGTH = 64
* @extends {Error} * @extends {Error}
* @classdesc An error thrown when there was a problem encountered while parsing a serialized hash * @classdesc An error thrown when there was a problem encountered while parsing a serialized hash
* @property {?Exception} cause An exception which was thrown while parsing this string. * @property {?Exception} cause An exception which was thrown while parsing this string.
* @property {any} malformedString * @property {any} malformedString
*/ */
class ParseError extends Error { class ParseError extends Error {
/** /**
* @param {any} malformedString - The string which could not be parsed * @param {any} malformedString - The string which could not be parsed
* @param {?Exception} cause - An error which was raised while trying to parse the string * @param {?Exception} cause - An error which was raised while trying to parse the string
*/ */
constructor(malformedString, cause) { constructor(malformedString, cause) {
super(`error parsing ${malformedString}, expected hash encoded by @techworkers/scrypt-wrapper`) super(
this.cause = cause `error parsing ${malformedString}, expected hash encoded by @techworkers/scrypt-wrapper`,
this.malformedString = malformedString )
} this.cause = cause
this.malformedString = malformedString
}
} }
/** /**
@ -45,52 +52,54 @@ class ParseError extends Error {
* @property {HashOptions} options The options passed to hash() or hashSync() * @property {HashOptions} options The options passed to hash() or hashSync()
*/ */
class InvalidOptions extends Error { class InvalidOptions extends Error {
/** /**
* *
* @param {HashOptions} options The options which were received that are invalid * @param {HashOptions} options The options which were received that are invalid
*/ */
constructor(options) { constructor(options) {
super(`invalid options were received:\n\tcost: ${options.cost}\n\tkeyLength: ${options.keyLength}\n\tsaltLength: ${options.saltLength}`) super(
this.options = options `invalid options were received:\n\tcost: ${options.cost}\n\tkeyLength: ${options.keyLength}\n\tsaltLength: ${options.saltLength}`,
} )
this.options = options
}
/** /**
* Check that the given options are actually what they're expected to be * Check that the given options are actually what they're expected to be
* @param {HashOptions} options * @param {HashOptions} options
* @param {?function} reject A function called with the error rather than it being thrown * @param {?function} reject A function called with the error rather than it being thrown
*/ */
static validateOptions(options, reject) { static validateOptions(options, reject) {
const {cost, keyLength, saltLength} = options const { cost, keyLength, saltLength } = options
// Cost validation // Cost validation
if ( if (
!(typeof cost === 'number' || cost instanceof Number) !(typeof cost === 'number' || cost instanceof Number) ||
|| cost < 13 cost < 13 ||
|| !Number.isInteger(cost) !Number.isInteger(cost)
) { ) {
const err = new InvalidOptions(options) const err = new InvalidOptions(options)
if(reject) reject(err) if (reject) reject(err)
else throw err else throw err
}
// keyLength validation
if (
!(typeof keyLength === 'number' || keyLength instanceof Number)
|| !Number.isInteger(keyLength)
) {
const err = new InvalidOptions(options)
if(reject) reject(err)
else throw err
}
// saltLength validation
if (
!(typeof saltLength === 'number' || saltLength instanceof Number)
|| saltLength < 16 /* (recommended minimum) */
|| !Number.isInteger(saltLength)
) {
const err = new InvalidOptions(options)
if(reject) reject(err)
else throw err
}
} }
// keyLength validation
if (
!(typeof keyLength === 'number' || keyLength instanceof Number) ||
!Number.isInteger(keyLength)
) {
const err = new InvalidOptions(options)
if (reject) reject(err)
else throw err
}
// saltLength validation
if (
!(typeof saltLength === 'number' || saltLength instanceof Number) ||
saltLength < 16 /* (recommended minimum) */ ||
!Number.isInteger(saltLength)
) {
const err = new InvalidOptions(options)
if (reject) reject(err)
else throw err
}
}
} }
/** /**
@ -108,54 +117,65 @@ class InvalidOptions extends Error {
* - the URL-safe base64-encoded salt * - the URL-safe base64-encoded salt
* - a dot ('.') * - a dot ('.')
* - the URL-safe base64-encoded derived hash * - the URL-safe base64-encoded derived hash
* *
* @param {string} password the plain-text password * @param {string} password the plain-text password
* @param {?HashOptions} options optional settings * @param {?HashOptions} options optional settings
* @throws {InvalidOptions} if the options aren't within the expected constraints * @throws {InvalidOptions} if the options aren't within the expected constraints
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
function hash(password, options) { function hash(password, options) {
const cost = options?.cost ?? DEFAULT_COST const cost = options?.cost ?? DEFAULT_COST
const keyLength = options?.keyLength ?? DEFAULT_KEY_LENGTH const keyLength = options?.keyLength ?? DEFAULT_KEY_LENGTH
const saltLength = options?.saltLength ?? DEFAULT_SALT_LENGTH const saltLength = options?.saltLength ?? DEFAULT_SALT_LENGTH
const salt = randomBytes(saltLength) const salt = randomBytes(saltLength)
const $cost = 1 << cost const $cost = 1 << cost
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
InvalidOptions.validateOptions({cost, keyLength, saltLength}, reject) InvalidOptions.validateOptions({ cost, keyLength, saltLength }, reject)
let $hash = cost.toString(16) + '.' let $hash = cost.toString(16) + '.'
$hash += salt.toString('base64url') + '.' $hash += salt.toString('base64url') + '.'
scrypt(password.normalize(), salt, keyLength, { cost: $cost }, (err, key) => { scrypt(
if (err) reject(err) password.normalize(),
else resolve($hash + key.toString('base64url')) salt,
}) keyLength,
}) { cost: $cost },
(err, key) => {
if (err) reject(err)
else resolve($hash + key.toString('base64url'))
},
)
})
} }
/** /**
* Like the above hash() function but synchronous. * Like the above hash() function but synchronous.
* *
* @param {string} password the plain-text password * @param {string} password the plain-text password
* @param {?HashOptions} options optional settings * @param {?HashOptions} options optional settings
* @throws {InvalidOptions} if the options aren't within the expected constraints * @throws {InvalidOptions} if the options aren't within the expected constraints
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
function hashSync(password, options) { function hashSync(password, options) {
const cost = options?.cost ?? DEFAULT_COST const cost = options?.cost ?? DEFAULT_COST
const keyLength = options?.keyLength ?? DEFAULT_KEY_LENGTH const keyLength = options?.keyLength ?? DEFAULT_KEY_LENGTH
const saltLength = options?.saltLength ?? DEFAULT_SALT_LENGTH const saltLength = options?.saltLength ?? DEFAULT_SALT_LENGTH
InvalidOptions.validateOptions({cost, keyLength, saltLength}) InvalidOptions.validateOptions({ cost, keyLength, saltLength })
const salt = randomBytes(saltLength) const salt = randomBytes(saltLength)
const $cost = 1 << cost const $cost = 1 << cost
let $hash = cost.toString(16) + '.' let $hash = cost.toString(16) + '.'
$hash += salt.toString('base64url') + '.' $hash += salt.toString('base64url') + '.'
return $hash + scryptSync(password.normalize(), salt, keyLength, { cost: $cost }).toString('base64url') return (
$hash +
scryptSync(password.normalize(), salt, keyLength, { cost: $cost }).toString(
'base64url',
)
)
} }
/** /**
* Verify that the given plaintext password derives the given hash. The hash is * Verify that the given plaintext password derives the given hash. The hash is
* expected to be in the format as encoded by the above hash() function, which * expected to be in the format as encoded by the above hash() function, which
* also encodes the cost and salt used to create it. * also encodes the cost and salt used to create it.
* *
* @param {string} derivedHash The hash as derived and encoded by the above * @param {string} derivedHash The hash as derived and encoded by the above
* hash() function * hash() function
* @param {string} password The plaintext password to compare * @param {string} password The plaintext password to compare
@ -163,34 +183,34 @@ function hashSync(password, options) {
* @returns {Promise<bool>} whether the password matches * @returns {Promise<bool>} whether the password matches
*/ */
function verify(derivedHash, password) { function verify(derivedHash, password) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var $cost, $salt, $key var $cost, $salt, $key
try { try {
[$cost, $salt, $key] = derivedHash.split('.') ;[$cost, $salt, $key] = derivedHash.split('.')
} catch (e) { } catch (e) {
return reject(new ParseError(derivedHash, e)) return reject(new ParseError(derivedHash, e))
} }
if (!$cost || !$salt || !$key) { if (!$cost || !$salt || !$key) {
return reject(new ParseError(derivedHash)) return reject(new ParseError(derivedHash))
} }
var cost, salt, key var cost, salt, key
try { try {
cost = 1 << Number.parseInt($cost, 16) cost = 1 << Number.parseInt($cost, 16)
salt = Buffer.from($salt, 'base64url') salt = Buffer.from($salt, 'base64url')
key = Buffer.from($key, 'base64url') key = Buffer.from($key, 'base64url')
} catch (e) { } catch (e) {
return reject(new ParseError(derivedHash, e)) return reject(new ParseError(derivedHash, e))
} }
scrypt(password.normalize(), salt, key.length, { cost }, (err, newKey) => { scrypt(password.normalize(), salt, key.length, { cost }, (err, newKey) => {
if (err) reject(err) if (err) reject(err)
else resolve(timingSafeEqual(key, newKey)) else resolve(timingSafeEqual(key, newKey))
})
}) })
})
} }
/** /**
* Like `verify()`, but synchronous. * Like `verify()`, but synchronous.
* *
* @param {string} derivedHash The hash as derived and encoded by the above * @param {string} derivedHash The hash as derived and encoded by the above
* hash() function * hash() function
* @param {string} password The plaintext password to compare * @param {string} password The plaintext password to compare
@ -198,27 +218,32 @@ function verify(derivedHash, password) {
* @returns {Promise<bool>} whether the password matches * @returns {Promise<bool>} whether the password matches
*/ */
function verifySync(derivedHash, password) { function verifySync(derivedHash, password) {
var $cost, $salt, $key var $cost, $salt, $key
try { try {
[$cost, $salt, $key] = derivedHash.split('.') ;[$cost, $salt, $key] = derivedHash.split('.')
} catch (e) { } catch (e) {
throw new ParseError(derivedHash, e) throw new ParseError(derivedHash, e)
} }
if (!$cost || !$salt || !$key) { if (!$cost || !$salt || !$key) {
throw new ParseError(derivedHash) throw new ParseError(derivedHash)
} }
var cost, salt, key var cost, salt, key
try { try {
cost = 1 << Number.parseInt($cost, 16) cost = 1 << Number.parseInt($cost, 16)
salt = Buffer.from($salt, 'base64url') salt = Buffer.from($salt, 'base64url')
key = Buffer.from($key, 'base64url') key = Buffer.from($key, 'base64url')
} catch (e) { } catch (e) {
throw new ParseError(derivedHash, e) throw new ParseError(derivedHash, e)
} }
const newKey = scryptSync(password.normalize(), salt, key.length, { cost }) const newKey = scryptSync(password.normalize(), salt, key.length, { cost })
return timingSafeEqual(key, newKey) return timingSafeEqual(key, newKey)
} }
module.exports = { module.exports = {
hash, hashSync, verify, verifySync, ParseError, InvalidOptions hash,
} hashSync,
verify,
verifySync,
ParseError,
InvalidOptions,
}

View file

@ -13,6 +13,12 @@
"devDependencies": { "devDependencies": {
"chai": "^4.3.8", "chai": "^4.3.8",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"mocha": "^10.2.0" "mocha": "^10.2.0",
"prettier": "^3.0.3"
},
"prettier": {
"arrowParens": "avoid",
"singleQuote": true,
"semi": false
} }
} }

View file

@ -5,177 +5,224 @@ const { expect } = chai
const scrypt = require('..') const scrypt = require('..')
describe('hashing and verification', function () { describe('hashing and verification', function () {
it('works', async function () { it('works', async function () {
const pass = 'b4dpa$s' const pass = 'b4dpa$s'
const derived = await scrypt.hash(pass) const derived = await scrypt.hash(pass)
const [cost, salt, key] = derived.split('.') const [cost, salt, key] = derived.split('.')
cost.should.eq('e') cost.should.eq('e')
salt.length.should.eq(32) salt.length.should.eq(32)
key.length.should.eq(86) key.length.should.eq(86)
Buffer.from(key, 'base64url').length.should.eq(64) Buffer.from(key, 'base64url').length.should.eq(64)
scrypt.verify(derived, pass).should.eventually.be.true scrypt.verify(derived, pass).should.eventually.be.true
}) })
}) })
describe('hashSync() and sync verification', function () { describe('hashSync() and sync verification', function () {
it('works', function () { it('works', function () {
const pass = 'b4dpa$s' const pass = 'b4dpa$s'
const derived = scrypt.hashSync(pass) const derived = scrypt.hashSync(pass)
const [cost, salt, key] = derived.split('.') const [cost, salt, key] = derived.split('.')
cost.should.eq('e') cost.should.eq('e')
salt.length.should.eq(32) salt.length.should.eq(32)
key.length.should.eq(86) key.length.should.eq(86)
Buffer.from(key, 'base64url').length.should.eq(64) Buffer.from(key, 'base64url').length.should.eq(64)
scrypt.verifySync(derived, pass).should.be.true scrypt.verifySync(derived, pass).should.be.true
}) })
}) })
describe('hash() error cases', function () { describe('hash() error cases', function () {
it('throws an error when it receives an invalid cost value', async function () { it('throws an error when it receives an invalid cost value', async function () {
scrypt.hash('bad password', { cost: 2.3 }).should.be.rejectedWith(scrypt.InvalidOptions) scrypt
scrypt.hash('bad password', { cost: 2 /* too small */ }).should.be.rejectedWith(scrypt.InvalidOptions) .hash('bad password', { cost: 2.3 })
}) .should.be.rejectedWith(scrypt.InvalidOptions)
it('throws an error when it receives an invalid keyLength value', async function () { scrypt
scrypt.hash('bad password', { keyLength: 2.3 }).should.be.rejectedWith(scrypt.InvalidOptions) .hash('bad password', { cost: 2 /* too small */ })
}) .should.be.rejectedWith(scrypt.InvalidOptions)
it('throws an error when it receives an invalid saltLength value', async function () { })
scrypt.hash('bad password', { saltLength: 2.3 }).should.be.rejectedWith(scrypt.InvalidOptions) it('throws an error when it receives an invalid keyLength value', async function () {
scrypt.hash('bad password', { saltLength: 15 /* too small */ }).should.be.rejectedWith(scrypt.InvalidOptions) scrypt
}) .hash('bad password', { keyLength: 2.3 })
it('throws an error when the password is not the right thing', function () { .should.be.rejectedWith(scrypt.InvalidOptions)
scrypt.hash(undefined).should.eventually.be.rejected })
scrypt.hash(null).should.eventually.be.rejected it('throws an error when it receives an invalid saltLength value', async function () {
scrypt.hash(123.4).should.eventually.be.rejected scrypt
scrypt.hash(NaN).should.eventually.be.rejected .hash('bad password', { saltLength: 2.3 })
scrypt.hash(['some', 'arbitrary', 'values']).should.eventually.be.rejected .should.be.rejectedWith(scrypt.InvalidOptions)
scrypt.hash({ some: 'arbitrary values' }).should.eventually.be.rejected scrypt
scrypt.hash(Symbol('something else')).should.eventually.be.rejected .hash('bad password', { saltLength: 15 /* too small */ })
scrypt.hash(() => "no, you can't do this either").should.eventually.be.rejected .should.be.rejectedWith(scrypt.InvalidOptions)
}) })
it('throws an error when the password is not the right thing', function () {
scrypt.hash(undefined).should.be.rejected
scrypt.hash(null).should.be.rejected
scrypt.hash(123.4).should.be.rejected
scrypt.hash(NaN).should.be.rejected
scrypt.hash(['some', 'arbitrary', 'values']).should.be.rejected
scrypt.hash({ some: 'arbitrary values' }).should.be.rejected
scrypt.hash(Symbol('something else')).should.be.rejected
scrypt.hash(() => "no, you can't do this either").should.be.rejected
})
}) })
describe('hashSync() error cases', function () { describe('hashSync() error cases', function () {
it('throws an error when it receives an invalid cost value', async function () { it('throws an error when it receives an invalid cost value', async function () {
scrypt.hashSync.bind(undefined, 'bad password', { cost: 2.3 }).should.throw(scrypt.InvalidOptions) scrypt.hashSync
scrypt.hashSync.bind(undefined, 'bad password', { cost: 2 /* too small */ }).should.throw(scrypt.InvalidOptions) .bind(undefined, 'bad password', { cost: 2.3 })
}) .should.throw(scrypt.InvalidOptions)
it('throws an error when it receives an invalid keyLength value', async function () { scrypt.hashSync
scrypt.hashSync.bind(undefined, 'bad password', { keyLength: 2.3 }).should.throw(scrypt.InvalidOptions) .bind(undefined, 'bad password', { cost: 2 /* too small */ })
}) .should.throw(scrypt.InvalidOptions)
it('throws an error when it receives an invalid saltLength value', async function () { })
scrypt.hashSync.bind(undefined, 'bad password', { saltLength: 2.3 }).should.throw(scrypt.InvalidOptions) it('throws an error when it receives an invalid keyLength value', async function () {
scrypt.hashSync.bind(undefined, 'bad password', { saltLength: 15 }).should.throw(scrypt.InvalidOptions) scrypt.hashSync
}) .bind(undefined, 'bad password', { keyLength: 2.3 })
it('throws an error when the password is not the right thing', function () { .should.throw(scrypt.InvalidOptions)
scrypt.hashSync.bind(undefined, undefined).should.throw() })
scrypt.hashSync.bind(undefined, null).should.throw() it('throws an error when it receives an invalid saltLength value', async function () {
scrypt.hashSync.bind(undefined, 123.4).should.throw() scrypt.hashSync
scrypt.hashSync.bind(undefined, NaN).should.throw() .bind(undefined, 'bad password', { saltLength: 2.3 })
scrypt.hashSync.bind(undefined, ['some', 'arbitrary', 'values']).should.throw() .should.throw(scrypt.InvalidOptions)
scrypt.hashSync.bind(undefined, { some: 'arbitrary values' }).should.throw() scrypt.hashSync
scrypt.hashSync.bind(undefined, Symbol('something else')).should.throw() .bind(undefined, 'bad password', { saltLength: 15 })
scrypt.hashSync.bind(undefined, () => "no, you can't do this either").should.throw() .should.throw(scrypt.InvalidOptions)
}) })
it('throws an error when the password is not the right thing', function () {
scrypt.hashSync.bind(undefined, undefined).should.throw()
scrypt.hashSync.bind(undefined, null).should.throw()
scrypt.hashSync.bind(undefined, 123.4).should.throw()
scrypt.hashSync.bind(undefined, NaN).should.throw()
scrypt.hashSync
.bind(undefined, ['some', 'arbitrary', 'values'])
.should.throw()
scrypt.hashSync.bind(undefined, { some: 'arbitrary values' }).should.throw()
scrypt.hashSync.bind(undefined, Symbol('something else')).should.throw()
scrypt.hashSync
.bind(undefined, () => "no, you can't do this either")
.should.throw()
})
}) })
describe('verify() error cases', function () { describe('verify() error cases', function () {
it('throws an error when the password is not the right thing', function () { it('throws an error when the password is not the right thing', function () {
const validHash = 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g' const validHash =
scrypt.verify(validHash, undefined).should.eventually.be.rejected 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g'
scrypt.verify(validHash, null).should.eventually.be.rejected scrypt.verify(validHash, undefined).should.be.rejected
scrypt.verify(validHash, 123.4).should.eventually.be.rejected scrypt.verify(validHash, null).should.be.rejected
scrypt.verify(validHash, NaN).should.eventually.be.rejected scrypt.verify(validHash, 123.4).should.be.rejected
scrypt.verify(validHash, ['some', 'arbitrary', 'values']).should.eventually.be.rejected scrypt.verify(validHash, NaN).should.be.rejected
scrypt.verify(validHash, { some: 'arbitrary values' }).should.eventually.be.rejected scrypt.verify(validHash, ['some', 'arbitrary', 'values']).should.be.rejected
scrypt.verify(validHash, Symbol('something else')).should.eventually.be.rejected scrypt.verify(validHash, { some: 'arbitrary values' }).should.be.rejected
scrypt.verify(validHash, () => "no, you can't do this either").should.eventually.be.rejected scrypt.verify(validHash, Symbol('something else')).should.be.rejected
}) scrypt.verify(validHash, () => "no, you can't do this either").should.be
it('throws an error when the hash is not the expected format', async function () { .rejected
const validHash = 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g' scrypt.verify(validHash, Promise.resolve("no, you can't do this either"))
const hashParts = validHash.split('.') .should.be.rejected
async function checkThrows(value, cause) { })
let thrown it('throws an error when the hash is not the expected format', async function () {
try { const validHash =
await scrypt.verify(value, 'bad password') 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g'
thrown = false const hashParts = validHash.split('.')
} catch (e) { async function checkThrows(value, cause) {
thrown = true let thrown
e.should.be.an.instanceof(scrypt.ParseError) try {
expect(e.malformedString).to.eq(value) await scrypt.verify(value, 'bad password')
if (cause) thrown = false
expect(e.cause).to.be.an.instanceof(cause) } catch (e) {
else thrown = true
expect(e.cause).to.be.undefined e.should.be.an.instanceof(scrypt.ParseError)
} expect(e.malformedString).to.eq(value)
thrown.should.be.true if (cause) expect(e.cause).to.be.an.instanceof(cause)
} else expect(e.cause).to.be.undefined
await checkThrows(undefined, TypeError) }
await checkThrows(1234, TypeError) thrown.should.be.true
await checkThrows(null, TypeError) }
await checkThrows([1, 2, 3], TypeError) await checkThrows(undefined, TypeError)
scrypt.verify({ await checkThrows(1234, TypeError)
// This is technically valid, why try to prevent it? 🤷 await checkThrows(null, TypeError)
split: () => hashParts await checkThrows([1, 2, 3], TypeError)
}, 'bad password').should.eventually.be.true scrypt.verify(
await checkThrows({}, TypeError) {
// This is technically valid, why try to prevent it? 🤷
split: () => hashParts,
},
'bad password',
).should.eventually.be.true
await checkThrows({}, TypeError)
await checkThrows(validHash.substring(1)) // No cost await checkThrows(validHash.substring(1)) // No cost
// no salt // no salt
await checkThrows('e..' + hashParts[2]) await checkThrows('e..' + hashParts[2])
// no key // no key
await checkThrows(hashParts[0] + '.' + hashParts[1]) await checkThrows(hashParts[0] + '.' + hashParts[1])
await checkThrows('...') await checkThrows('...')
await checkThrows('') await checkThrows('')
}) })
}) })
describe('verifySync() error cases', function () { describe('verifySync() error cases', function () {
it('throws an error when the password is not the right thing', function () { it('throws an error when the password is not the right thing', function () {
const validHash = 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g' const validHash =
scrypt.verifySync.bind(undefined, validHash, undefined).should.throw() 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g'
scrypt.verifySync.bind(undefined, validHash, null).should.throw() scrypt.verifySync.bind(undefined, validHash, undefined).should.throw()
scrypt.verifySync.bind(undefined, validHash, 123.4).should.throw() scrypt.verifySync.bind(undefined, validHash, null).should.throw()
scrypt.verifySync.bind(undefined, validHash, NaN).should.throw() scrypt.verifySync.bind(undefined, validHash, 123.4).should.throw()
scrypt.verifySync.bind(undefined, validHash, ['some', 'arbitrary', 'values']).should.throw() scrypt.verifySync.bind(undefined, validHash, NaN).should.throw()
scrypt.verifySync.bind(undefined, validHash, { some: 'arbitrary values' }).should.throw() scrypt.verifySync
scrypt.verifySync.bind(undefined, validHash, Symbol('something else')).should.throw() .bind(undefined, validHash, ['some', 'arbitrary', 'values'])
scrypt.verifySync.bind(undefined, validHash, () => "no, you can't do this either").should.throw() .should.throw()
scrypt.verifySync.bind(undefined, validHash, Promise.resolve("no, you can't do this either")).should.throw() scrypt.verifySync
}) .bind(undefined, validHash, { some: 'arbitrary values' })
it('throws an error when the hash is not the expected format', function () { .should.throw()
const validHash = 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g' scrypt.verifySync
const hashParts = validHash.split('.') .bind(undefined, validHash, Symbol('something else'))
function checkThrows(value, cause) { .should.throw()
let thrown scrypt.verifySync
try { .bind(undefined, validHash, () => "no, you can't do this either")
scrypt.verifySync(value, 'bad password') .should.throw()
thrown = false scrypt.verifySync
} catch (e) { .bind(
thrown = true undefined,
e.should.be.an.instanceof(scrypt.ParseError) validHash,
expect(e.malformedString).to.eq(value) Promise.resolve("no, you can't do this either"),
if (cause) )
expect(e.cause).to.be.an.instanceof(cause) .should.throw()
else })
expect(e.cause).to.be.undefined it('throws an error when the hash is not the expected format', function () {
} const validHash =
thrown.should.be.true 'e.WCu8zB9FHLhXaAf5Svn7NE4ySqB5X45X.ZLpxVwLz1816kmshqXHn12X_4_lZD_0Yl-27KSLLzdhXJ0Fr2huSD7BvoMlBMOMUQBKyKXPzhKI01_Ot-C_w8g'
} const hashParts = validHash.split('.')
checkThrows(undefined, TypeError) function checkThrows(value, cause) {
checkThrows(1234, TypeError) let thrown
checkThrows(null, TypeError) try {
checkThrows([1, 2, 3], TypeError) scrypt.verifySync(value, 'bad password')
scrypt.verify({ thrown = false
// This is technically valid, why try to prevent it? 🤷 } catch (e) {
split: () => hashParts thrown = true
}, 'bad password').should.eventually.be.true e.should.be.an.instanceof(scrypt.ParseError)
checkThrows({}, TypeError) expect(e.malformedString).to.eq(value)
if (cause) expect(e.cause).to.be.an.instanceof(cause)
else expect(e.cause).to.be.undefined
}
thrown.should.be.true
}
checkThrows(undefined, TypeError)
checkThrows(1234, TypeError)
checkThrows(null, TypeError)
checkThrows([1, 2, 3], TypeError)
scrypt.verify(
{
// This is technically valid, why try to prevent it? 🤷
split: () => hashParts,
},
'bad password',
).should.eventually.be.true
checkThrows({}, TypeError)
checkThrows(validHash.substring(1)) // No cost checkThrows(validHash.substring(1)) // No cost
// no salt // no salt
checkThrows('e..' + hashParts[2]) checkThrows('e..' + hashParts[2])
// no key // no key
checkThrows(hashParts[0] + '.' + hashParts[1]) checkThrows(hashParts[0] + '.' + hashParts[1])
checkThrows('...') checkThrows('...')
checkThrows('') checkThrows('')
}) })
}) })

View file

@ -444,6 +444,11 @@ picomatch@^2.0.4, picomatch@^2.2.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
prettier@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
randombytes@^2.1.0: randombytes@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"