Prettier+some nitpicks
This commit is contained in:
parent
8c18fc4f92
commit
1432f43035
|
|
@ -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
279
index.js
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue