scrypt-kdf
Version:
Scrypt Key Derivation Function
211 lines (166 loc) • 9.61 kB
JavaScript
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Tests for scrypt key derivation function using Node.js. */
/* © 2018-2024 Chris Veness / Movable Type Ltd */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { Buffer } from 'node:buffer';
import Scrypt from '../scrypt.js';
const password = 'my secret password';
const key0salt = 'c2NyeXB0AAwAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA08wOZXFAec6Si7mP1SzrmK6Pvpx2zfUEXXAuM68S4DAnUER44bh+BxsnxMC75Jgs';
describe('Hash & verify (base64)', function() {
test('with just logN param, args as Buffer', async function() {
const keyBuff = await Scrypt.kdf(password, { logN: 12 });
assert.deepEqual(Scrypt.viewParams(keyBuff), { logN: 12, r: 8, p: 1 });
assert.equal(await Scrypt.verify(keyBuff, password), true);
});
test('with logN, r, p params, args as Buffer', async function() {
const keyBuff = await Scrypt.kdf(password, { logN: 12, r: 9, p: 2 });
assert.deepEqual(Scrypt.viewParams(keyBuff), { logN: 12, r: 9, p: 2 });
assert.equal(await Scrypt.verify(keyBuff, password), true);
});
test('with args as strings', async function() {
const keyStr = (await Scrypt.kdf(password, { logN: '12', r: '8', p: '1' })).toString('base64');
assert.deepEqual(Scrypt.viewParams(keyStr), { logN: 12, r: 8, p: 1 });
assert.equal(await Scrypt.verify(keyStr, password), true);
});
test('fails to verify with bad passphrase', async function() {
const keyStr = (await Scrypt.kdf(password, { logN: 12 })).toString('base64');
assert.equal(await Scrypt.verify(keyStr, 'wrong password'), false);
});
});
describe('Verify previous key (base64)', function() {
test('verifies null-salt key', async function() {
assert.equal(await Scrypt.verify(Buffer.from(key0salt, 'base64'), password), true);
});
test('fails to verify null-salt key with bad passphrase', async function() {
assert.equal(await Scrypt.verify(Buffer.from(key0salt, 'base64'), 'wrong password'), false);
});
});
describe('Args as String/Uint8Array/Buffer', function() {
test('String', async function() {
const pwStr = String.fromCharCode(...[ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]);
const keyStr = (await Scrypt.kdf(pwStr, { logN: 12 })).toString('base64');
assert.deepEqual(Scrypt.viewParams(keyStr), { logN: 12, r: 8, p: 1 });
assert.equal(await Scrypt.verify(keyStr, pwStr), true);
});
test('Uint8Array', async function() {
const pwArr = new Uint8Array([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]);
const keyArr = new Uint8Array(await Scrypt.kdf(pwArr, { logN: 12 }));
assert.deepEqual(Scrypt.viewParams(keyArr), { logN: 12, r: 8, p: 1 });
assert.equal(await Scrypt.verify(keyArr, pwArr), true);
});
test('Buffer', async function() {
const pwBuff = Buffer.from([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]);
const keyBuff = await Scrypt.kdf(pwBuff, { logN: 12 });
assert.deepEqual(Scrypt.viewParams(keyBuff), { logN: 12, r: 8, p: 1 });
assert.equal(await Scrypt.verify(keyBuff, pwBuff), true);
});
});
describe('Pick params', function() {
test('Picks params for 100ms', function() {
const params = Scrypt.pickParams(0.1, 1024*1024*1024, 0.5);
assert.deepEqual(Object.keys(params), [ 'logN', 'r', 'p' ]);
assert(params.logN >= 8 && params.logN <= 20);
assert.equal(params.r, 8);
assert.equal(params.p, 1);
});
test('Picks params with default maxmem/maxmemfrac', function() {
const params = Scrypt.pickParams(0.1);
assert.deepEqual(Object.keys(params), [ 'logN', 'r', 'p' ]);
assert(params.logN >= 8 && params.logN <= 20);
assert.equal(params.r, 8);
assert.equal(params.p, 1);
});
test('Picks params with 0 maxmem', function() {
const params = Scrypt.pickParams(0.1, 0);
assert(params.logN >= 8 && params.logN <= 20);
});
test('Picks params with 0 maxmemfrac', function() {
const params = Scrypt.pickParams(0.1, 0, 0);
assert(params.logN >= 8 && params.logN <= 20);
});
test('Picks params setting N based on memory limit', function() {
const params = Scrypt.pickParams(1, 1024, 0.1);
assert(params.logN >= 8 && params.logN <= 20);
assert(params.p > 1);
});
});
describe('Kdf errors', function() {
test('rejects on numeric passphrase', function() {
assert.rejects(async () => await Scrypt.kdf(99), new TypeError('passphrase must be a string, TypedArray, or Buffer (received number)'));
});
test('rejects on no params', function() {
assert.rejects(async () => await Scrypt.kdf(password), new TypeError('params must be supplied'));
});
test('rejects on bad params', function() {
assert.rejects(async () => await Scrypt.kdf(password, null), new TypeError('params must be an object (received null)'));
});
test('rejects on bad params', function() {
assert.rejects(async () => await Scrypt.kdf(password, false), new TypeError('params must be an object (received boolean)'));
});
test('rejects on bad params', function() {
assert.rejects(async () => await Scrypt.kdf(password, 99), new TypeError('params must be an object (received number)'));
});
test('rejects on bad params', function() {
assert.rejects(async () => await Scrypt.kdf(password, 'bad params'), new TypeError('params must be an object (received string)'));
});
test('rejects on bad logN', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 'bad' }), new RangeError('parameter logN must be an integer; received bad'));
});
test('rejects on zero logN', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 0 }), new RangeError('parameter logN must be between 1 and 30; received 0'));
});
test('rejects on non-integer logN', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12.12 }), new RangeError('parameter logN must be an integer; received 12.12'));
});
test('rejects on non-integer r', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, r: 8.8 }), new RangeError('parameter r must be a positive integer; received 8.8'));
});
test('rejects on non-integer p', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, p: 1.1 }), new RangeError('parameter p must be a positive integer; received 1.1'));
});
test('rejects on 0 r', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, r: 0 }), new RangeError('parameter r must be a positive integer; received 0'));
});
test('rejects on 0 p', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, p: 0 }), new RangeError('parameter p must be a positive integer; received 0'));
});
test('rejects on out-of-range r', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, r: 2**30 }), new RangeError('parameters p*r must be <= 2^30-1'));
});
test('rejects on out-of-range p', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, p: 2**30 }), new RangeError('parameters p*r must be <= 2^30-1'));
});
test('rejects on EVP PBE memory limit exceeded', function() {
assert.rejects(async () => await Scrypt.kdf(password, { logN: 12, r: 2 ** 20 }), new Error('Invalid scrypt params: error:030000AC:digital envelope routines::memory limit exceeded'));
});
});
describe('Verify errors', function() {
test('rejects on bad passphrase type', function() {
assert.rejects(async () => await Scrypt.verify(await Scrypt.kdf(password, { logN: 12 }), null), new TypeError('passphrase must be a string, TypedArray, or Buffer (received null)'));
});
test('rejects on bad key type', function() {
assert.rejects(async () => await Scrypt.verify(null, 'passwd'), new TypeError('key must be a Uint8Array/Buffer (received null)'));
});
test('rejects on bad key', function() {
assert.rejects(async () => await Scrypt.verify(Buffer.from('key', 'base64'), 'passwd'), new RangeError('invalid key'));
});
test('fails to verify on checksum failure', async function() {
const key = await Scrypt.kdf(password, { logN: 12 });
key[7] = 11; // patch logN to new value
assert.deepEqual(Scrypt.viewParams(key), { logN: 11, r: 8, p: 1 });
assert.equal(await Scrypt.verify(key, password), false);
});
});
describe('ViewParams errors', function() { // note Scrypt.viewParams is not async
test('throws on null key', function() {
assert.throws(() => Scrypt.viewParams(null), new TypeError('key must be a Uint8Array/Buffer (received null)'));
});
test('throws on numeric key', function() {
assert.throws(() => Scrypt.viewParams(99), new TypeError('key must be a Uint8Array/Buffer (received number)'));
});
test('throws on invalid key', function() {
assert.throws(() => Scrypt.viewParams(Buffer.from('bad key', 'base64')), new RangeError('invalid key'));
});
});