diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..714298c --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,29 @@ +# Scrypt.js GitHub Actions Node.js CI workflow + +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + node-version: [ lts/* current ] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install + - run: npm run test-node diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c8f251b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: node_js - -node_js: -- "lts/*" -- "node" - -os: -- linux -- osx -- windows - -after_success: -- nyc npm test && nyc report --reporter=text-lcov | coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ad854..2aef5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ ### Changed +- BREAKING: Convert to ESM +- Run in Node.js & Deno +- Strings accepted for passphrase & key arguments +- Use Web Crypto API in place of node:crypto (except OpenSSL scrypt, timingSafeEqual) - Indicate received type when parameter type checks fail +- Use GitHub Actions CI in place of Travis CI ## [2.0.1] - 2019-05-03 diff --git a/LICENSE b/LICENSE index 709b14b..6f736ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Chris Veness +Copyright (c) 2018-2024 Chris Veness Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ea8622c..04e7ee6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ Scrypt Key Derivation Function ============================== -[![Build Status](https://travis-ci.org/chrisveness/scrypt-kdf.svg?branch=master)](https://travis-ci.org/chrisveness/scrypt-kdf) -[![Coverage Status](https://coveralls.io/repos/github/chrisveness/scrypt-kdf/badge.svg?branch=master)](https://coveralls.io/github/chrisveness/scrypt-kdf?branch=master) +![Node.js CI](https://github.com/chrisveness/scrypt-kdf/actions/workflows/node.js.yml/badge.svg) Scrypt is a *password-based [key derivation function](https://en.wikipedia.org/wiki/Key_derivation_function)*, useful for storing password hashes for verifying interactive logins. @@ -20,36 +19,30 @@ It was originally developed by Colin Percival as part of the [Tarsnap](http://ww Example usage ------------- - + `scrypt-kdf` is available from [npm](https://www.npmjs.com/package/scrypt-kdf): - + $ npm install scrypt-kdf ### hashing - const Scrypt = require('scrypt-kdf'); - + import Scrypt from 'scrypt-kdf'); + const keyBuf = await Scrypt.kdf('my secret pw', { logN: 15 }); const keyStr = keyBuf.toString('base64'); // keyStr is 128-char string which can be stored for subsequent verification ### verifying - const Scrypt = require('scrypt-kdf'); + import Scrypt from 'scrypt-kdf'); const user = await users.findOne({ email: req.body.email }); // for example const keyBuf = Buffer.from(user.password, 'base64'); const ok = await Scrypt.verify(keyBuf, req.body.password); -### ES modules - -If using ES modules (for instance with the [esm](https://www.npmjs.com/package/esm) package), use - - import Scrypt from 'scrypt-kdf'; +### in Deno: -in place of - - const Scrypt = require('scrypt-kdf'); + import Scrypt from 'npm:scrypt-kdf@^3'; API --- @@ -95,20 +88,7 @@ Note that results are dependent on the computer the calculation is run on; calcu OpenSSL implementation ---------------------- -`scrypt-kdf` is a wrapper around the [OpenSSL](https://www.openssl.org/docs/manmaster/man7/scrypt.html) implementation of scrypt made available through the Node.js [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback). - -Scrypt was introduced into Node.js in [v10.5.0](https://nodejs.org/en/blog/release/v10.5.0/), so `scrypt-kdf` requires Node.js v10.5.0 or above; it can also be used with Node.js v8.5.0...v10.4.1 using the [scrypt-async](https://www.npmjs.com/package/scrypt-async) OpenSSL polyfill with the following code fragment: - - const crypto = require('crypto'); - if (!crypto.scrypt) { - const scryptAsync = require('scrypt-async'); - crypto.scrypt = function(password, salt, keylen, options, callback) { - const opt = Object.assign({}, options, { dkLen: keylen }); - scryptAsync(password, salt, opt, (derivedKey) => callback(null, Buffer.from(derivedKey))); - }; - } - -`Scrypt.pickParams()` will not be available with this polyfill. +`scrypt-kdf` is a wrapper around the [OpenSSL](https://www.openssl.org/1.1.1/man7/scrypt.html) implementation of scrypt made available through the Node.js [crypto module](https://nodejs.org/api/crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback). Key format diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..f978541 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,40 @@ +import eslintjs from '@eslint/js'; +import globals from 'globals'; + +export default [ + eslintjs.configs.recommended, + { ignores: [ 'tmp/' ] }, // github.com/eslint/eslint/discussions/18304 + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + Deno: 'readonly', + }, + }, + rules: { + 'array-bracket-spacing': [ 'error', 'always' ], + 'comma-dangle': [ 'error', 'always-multiline' ], + 'comma-spacing': [ 'error' ], + 'curly': [ 'error', 'multi-line' ], + 'indent': [ 'error', 4, { 'SwitchCase': 1 } ], + 'key-spacing': [ 'error', { 'align': 'value' } ], + 'keyword-spacing': [ 'error' ], + 'no-case-declarations': 'warn', + 'no-console': [ 'warn', { 'allow': [ 'error', 'info', 'debug' ] } ], + 'no-irregular-whitespace': 'warn', + 'no-redeclare': 'warn', + 'no-shadow': 'warn', + 'no-unused-vars': 'warn', + 'no-var': 'error', + 'object-curly-spacing': [ 'error', 'always' ], + 'prefer-const': 'error', + 'quotes': [ 'error', 'single', 'avoid-escape' ], + 'require-await': 'error', + 'semi': [ 'error', 'always' ], + 'space-before-blocks': [ 'error', 'always' ], + 'space-in-parens': [ 'error' ], + 'strict': [ 'error', 'global' ], + }, + }, +]; diff --git a/package.json b/package.json index f7cca22..9b66e2b 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,33 @@ { "name": "scrypt-kdf", "description": "Scrypt Key Derivation Function", - "keywords": [ - "crypto", - "scrypt", - "kdf", - "password", - "hash", - "login", - "authenticate", - "verify" - ], + "keywords": [ "crypto", "scrypt", "kdf", "password", "hash", "login", "authenticate", "verify" ], "author": "Chris Veness", "repository": { "type": "git", "url": "https://github.com/chrisveness/scrypt-kdf" }, - "version": "2.0.1", + "version": "3.0.0", "license": "MIT", "main": "scrypt.js", + "type": "module", "types": "scrypt.d.ts", "engines": { - "node": ">=8.5.0" + "node": ">=18.0.0" }, "scripts": { - "test": "mocha --exit test/scrypt-tests.js", - "lint": "eslint scrypt.js test/scrypt-tests.js", - "cover": "c8 -r html npm test" + "test": "npm run test-node && npm run test-deno", + "test-node": "node --test test/tests-node.js", + "test-deno": "deno test -NES test/tests-deno.js", + "lint": "eslint scrypt.js test/tests-*.js", + "cover": "c8 npm test" }, "devDependencies": { - "c8": "^7.0.0", - "chai": "^4.0.0", - "coveralls": "^3.0.0", - "eslint": "^7.0.0", - "mocha": "^7.0.0" - }, - "eslintConfig": { - "env": { - "es2017": true, - "mocha": true, - "node": true - }, - "extends": "eslint:recommended" + "@babel/eslint-parser": "^7", + "@babel/plugin-syntax-import-attributes": "^7", + "@eslint/js": "^9", + "@types/node": "^22", + "c8": "^10", + "globals": "^15" } } diff --git a/scrypt.d.ts b/scrypt.d.ts index 73d490f..407f820 100644 --- a/scrypt.d.ts +++ b/scrypt.d.ts @@ -33,7 +33,7 @@ export type ScryptParams = Required; * @example * const key = await Scrypt.kdf('my secret password', { logN: 15 }); */ -export declare function kdf(passphrase: string|ArrayBufferView, params: Readonly): Promise; +export declare function kdf(passphrase: string|Uint8Array|Buffer, params: Readonly): Promise; /** * Check whether key was generated from passphrase. @@ -45,7 +45,7 @@ export declare function kdf(passphrase: string|ArrayBufferView, params: Readonly * @example * const ok = await Scrypt.verify(key, 'my secret password'); */ -export declare function verify(key: Uint8Array, passphrase: string|ArrayBufferView): Promise; +export declare function verify(key: string|Uint8Array|Buffer, passphrase: string|Uint8Array|Buffer): Promise; /** * View scrypt parameters which were used to derive key. @@ -57,7 +57,7 @@ export declare function verify(key: Uint8Array, passphrase: string|ArrayBufferVi * const key = await Scrypt.kdf('my secret password', { logN: 15 } ); * const params = Scrypt.viewParams(key); // => { logN: 15, r: 8, p: 1 } */ -export declare function viewParams(key: Uint8Array): ScryptParams; +export declare function viewParams(key: string|Uint8Array|Buffer): ScryptParams; /** * Calculate scrypt parameters from maxtime, maxmem, maxmemfrac values. diff --git a/scrypt.js b/scrypt.js index 2528330..c05fc5c 100644 --- a/scrypt.js +++ b/scrypt.js @@ -1,5 +1,5 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -/* Scrypt password-based key derivation function. (c) C.Veness 2018-2019 */ +/* Scrypt password-based key derivation function. © 2018-2024 Chris Veness / Movable Type Ltd */ /* MIT Licence */ /* */ /* The function derives one or more secret keys from a secret string. It is based on memory-hard */ @@ -11,19 +11,17 @@ /* function, returning a derived key with scrypt parameters and salt in Colin Percival's standard */ /* file header format, and a function for verifying that key against the original password. */ /* */ -/* Requires Node.js 10.5.0 or above: see github.com/chrisveness/scrypt-kdf#openssl-implementation */ -/* (unless crypto.scrypt is polyfilled with scrypt-async - in which case v8.5.0+). */ +/* Runs on Node.js v18.0.0+ or Deno v2.0.1+. */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -const crypto = require('crypto'); // nodejs.org/api/crypto.html -const performance = require('perf_hooks').performance; // nodejs.org/api/perf_hooks.html -const os = require('os'); // nodejs.org/api/os.html -const TextEncoder = require('util').TextEncoder; // nodejs.org/api/util.html -const promisify = require('util').promisify; // nodejs.org/api/util.html +import nodeCrypto from 'node:crypto'; // for OpenSSL scrypt, timingSafeEqual +import { Buffer } from 'node:buffer'; // key is returned as Buffer (for better or for worse) +import os from 'node:os'; // for total amoung to system memory +import { TextEncoder } from 'node:util'; // TextEncoder should be a global in Node.js, but it's not +import { promisify } from 'node:util'; -const versionMsg = 'crypto.scrypt not found: Node.js v10.5.0+ required; see github.com/chrisveness/scrypt-kdf#openssl-implementation'; -if (crypto.scrypt == undefined) throw new Error(versionMsg); -const cryptoScrypt = promisify(crypto.scrypt); +const opensslScrypt = promisify(nodeCrypto.scrypt); // OpenSSL scrypt; docs.openssl.org/1.1.1/man7/scrypt/ +const opensslScryptSync = nodeCrypto.scryptSync; class Scrypt { @@ -31,7 +29,7 @@ class Scrypt { /** * Produce derived key using scrypt as a key derivation function. * - * @param {string|TypedArray|Buffer} passphrase - Secret value such as a password from which key is to be derived. + * @param {string|Uint8Array|Buffer} passphrase - Secret value such as a password from which key is to be derived. * @param {Object} params - Scrypt parameters. * @param {number} params.logN - CPU/memory cost parameter. * @param {number=8} params.r - Block size parameter. @@ -42,8 +40,9 @@ class Scrypt { * const key = (await Scrypt.kdf('my secret password', { logN: 15 })).toString('base64'); */ static async kdf(passphrase, params) { - if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`Passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`); - if (typeof params != 'object' || params == null) throw new TypeError(`Params must be an object (received ${typeOf(params)})`); + if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`); + if (params === undefined) throw new TypeError('params must be supplied'); + if (typeof params != 'object' || params == null) throw new TypeError(`params must be an object (received ${typeOf(params)})`); const paramDefaults = { logN: undefined, r: 8, p: 1 }; params = Object.assign({}, paramDefaults, params); @@ -52,27 +51,27 @@ class Scrypt { const logN = Math.round(params.logN); const r = Math.round(params.r); const p = Math.round(params.p); - if (isNaN(logN) || logN != params.logN) throw new RangeError(`Parameter logN must be an integer; received ${params.logN}`); - if (logN < 1 || logN > 30) throw new RangeError(`Parameter logN must be between 1 and 30; received ${params.logN}`); - if (isNaN(r) || r != params.r || r <= 0) throw new RangeError(`Parameter r must be a positive integer; received ${params.r}`); - if (isNaN(p) || p != params.p || p <= 0) throw new RangeError(`Parameter p must be a positive integer; received ${params.p}`); - if (p > (2**30-1)/r) throw new RangeError(`Parameters p*r must be <= 2^30-1`); + if (isNaN(logN) || logN != params.logN) throw new RangeError(`parameter logN must be an integer; received ${params.logN}`); + if (logN < 1 || logN > 30) throw new RangeError(`parameter logN must be between 1 and 30; received ${params.logN}`); + if (isNaN(r) || r != params.r || r <= 0) throw new RangeError(`parameter r must be a positive integer; received ${params.r}`); + if (isNaN(p) || p != params.p || p <= 0) throw new RangeError(`parameter p must be a positive integer; received ${params.p}`); + if (p > (2**30-1)/r) throw new RangeError('parameters p*r must be <= 2^30-1'); // the derived key is 96 bytes: use an ArrayBuffer to view it in different formats - const buffer = new ArrayBuffer(96); + const keyBuff = new ArrayBuffer(96); // a structured view of the derived key const struct = { - scrypt: new Uint8Array(buffer, 0, 6), + scrypt: new Uint8Array(keyBuff, 0, 6), params: { - v: new DataView(buffer, 6, 1), - logN: new DataView(buffer, 7, 1), - r: new DataView(buffer, 8, 4), - p: new DataView(buffer, 12, 4), + v: new DataView(keyBuff, 6, 1), + logN: new DataView(keyBuff, 7, 1), + r: new DataView(keyBuff, 8, 4), + p: new DataView(keyBuff, 12, 4), }, - salt: new Uint8Array(buffer, 16, 32), - checksum: new Uint8Array(buffer, 48, 16), - hmachash: new Uint8Array(buffer, 64, 32), + salt: new Uint8Array(keyBuff, 16, 32), + checksum: new Uint8Array(keyBuff, 48, 16), + hmachash: new Uint8Array(keyBuff, 64, 32), }; // set params @@ -82,11 +81,12 @@ class Scrypt { struct.params.p.setUint32(0, p, false); // big-endian // set salt - struct.salt.set(crypto.randomBytes(32)); + struct.salt.set(crypto.getRandomValues(new Uint8Array(32))); // set checksum of params & salt - const prefix48 = new Uint8Array(buffer, 0, 48); - struct.checksum.set(crypto.createHash('sha256').update(prefix48).digest().slice(0, 16)); + const prefix48 = new Uint8Array(keyBuff, 0, 48); // view onto struct.scrypt, struct.params, struct.salt + const prefix48hash = await crypto.subtle.digest('SHA-256', prefix48); // digest() returns ArrayBuffer... + struct.checksum.set(new Uint8Array(prefix48hash.slice(0, 16))); // note TypedArray.set() requires TypedArray arg, not ArrayBuffer // set HMAC hash from scrypt-derived key try { @@ -97,14 +97,16 @@ class Scrypt { maxmem: 2**31-1, // 2GB is maximum maxmem allowed }; // apply scrypt kdf to salt to derive hmac key - const hmacKey = await cryptoScrypt(passphrase, struct.salt, 64, params); + const hmacKey = await opensslScrypt(passphrase, struct.salt, 64, params); // get hmachash of params, salt, & checksum, using 1st 32 bytes of scrypt hash as key - const prefix64 = new Uint8Array(buffer, 0, 64); - const hmacHash = crypto.createHmac('sha256', hmacKey.slice(32)).update(prefix64).digest(); - struct.hmachash.set(hmacHash); + const prefix64 = new Uint8Array(keyBuff, 0, 64); + const algorithm = { name: 'HMAC', hash: 'SHA-256' }; + const cryptoKey = await crypto.subtle.importKey('raw', hmacKey.slice(32), algorithm, false, [ 'sign' ]); + const hmacHash = await crypto.subtle.sign(algorithm.name, cryptoKey, prefix64); // sign() returns ArrayBuffer... + struct.hmachash.set(new Uint8Array(hmacHash)); // note TypedArray.set() requires TypedArray arg, not ArrayBuffer - return Buffer.from(buffer); // return ArrayBuffer as Buffer/Uint8Array + return Buffer.from(keyBuff); // return ArrayBuffer as Buffer/Uint8Array } catch (e) { throw new Error(e.message); // e.g. memory limit exceeded; localise error to this function } @@ -114,8 +116,8 @@ class Scrypt { /** * Check whether key was generated from passphrase. * - * @param {Buffer|Uint8Array} key - Derived key obtained from Scrypt.kdf(). - * @param {string|TypedArray|Buffer} passphrase - Passphrase originally used to generate key. + * @param {string|Uint8Array|Buffer} key - Derived key obtained from Scrypt.kdf(). + * @param {string|Uint8Array|Buffer} passphrase - Passphrase originally used to generate key. * @returns {Promise} True if key was generated from passphrase. * * @example @@ -123,33 +125,34 @@ class Scrypt { * const ok = await Scrypt.verify(Buffer.from(key, 'base64'), 'my secret password'); */ static async verify(key, passphrase) { - if (!(key instanceof Uint8Array)) throw new TypeError(`Key must be a Buffer (received ${typeOf(key)})`); - if (key.length != 96) throw new RangeError('Invalid key'); - if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`Passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`); + const keyArr = typeof key == 'string' ? new Uint8Array([ ...atob(key) ].map(ch => ch.charCodeAt(0))) : key; + if (!(keyArr instanceof Uint8Array)) throw new TypeError(`key must be a Uint8Array/Buffer (received ${typeOf(keyArr)})`); + if (keyArr.length != 96) throw new RangeError('invalid key'); + if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`); // use the underlying ArrayBuffer to view key in different formats - const buffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength); + const keyBuff = keyArr.buffer.slice(keyArr.byteOffset, keyArr.byteOffset + keyArr.byteLength); // a structured view of the derived key const struct = { - scrypt: new Uint8Array(buffer, 0, 6), + scrypt: new Uint8Array(keyBuff, 0, 6), params: { - v: new DataView(buffer, 6, 1), - logN: new DataView(buffer, 7, 1), - r: new DataView(buffer, 8, 4), - p: new DataView(buffer, 12, 4), + v: new DataView(keyBuff, 6, 1), + logN: new DataView(keyBuff, 7, 1), + r: new DataView(keyBuff, 8, 4), + p: new DataView(keyBuff, 12, 4), }, - salt: new Uint8Array(buffer, 16, 32), - checksum: new Uint8Array(buffer, 48, 16), - hmachash: new Uint8Array(buffer, 64, 32), + salt: new Uint8Array(keyBuff, 16, 32), + checksum: new Uint8Array(keyBuff, 48, 16), + hmachash: new Uint8Array(keyBuff, 64, 32), }; // verify checksum of params & salt - const prefix48 = new Uint8Array(buffer, 0, 48); - const checksum = crypto.createHash('sha256').update(prefix48).digest().slice(0, 16); + const prefix48 = new Uint8Array(keyBuff, 0, 48); // view onto struct.scrypt, struct.params, struct.salt + const checksumRecalcd = await crypto.subtle.digest('SHA-256', prefix48); - if (!crypto.timingSafeEqual(checksum, struct.checksum)) return false; + if (!nodeCrypto.timingSafeEqual(struct.checksum, checksumRecalcd.slice(0, 16))) return false; // rehash scrypt-derived key try { @@ -161,14 +164,16 @@ class Scrypt { }; // apply scrypt kdf to salt to derive hmac key - const hmacKey = await cryptoScrypt(passphrase, struct.salt, 64, params); + const hmacKey = await opensslScrypt(passphrase, struct.salt, 64, params); // get hmachash of params, salt, & checksum, using 1st 32 bytes of scrypt hash as key - const prefix64 = new Uint8Array(buffer, 0, 64); - const hmacHash = crypto.createHmac('sha256', hmacKey.slice(32)).update(prefix64).digest(); + const prefix64 = new Uint8Array(keyBuff, 0, 64); + const algorithm = { name: 'HMAC', hash: 'SHA-256' }; + const cryptoKey = await crypto.subtle.importKey('raw', hmacKey.slice(32), algorithm, false, [ 'sign' ]); + const hmacHash = await crypto.subtle.sign(algorithm.name, cryptoKey, prefix64); // verify hash - return crypto.timingSafeEqual(hmacHash, struct.hmachash); + return nodeCrypto.timingSafeEqual(hmacHash, struct.hmachash); } catch (e) { throw new Error(e.message); // localise error to this function [can't happen?] } @@ -178,7 +183,7 @@ class Scrypt { /** * View scrypt parameters which were used to derive key. * - * @param {Buffer|Uint8Array} key - Derived base64 key obtained from Scrypt.kdf(). + * @param {string|Uint8Array|Buffer} key - Derived base64 key obtained from Scrypt.kdf(). * @returns {Object} Scrypt parameters logN, r, p. * * @example @@ -186,24 +191,25 @@ class Scrypt { * const params = Scrypt.viewParams(key); // => { logN: 15, r: 8, p: 1 } */ static viewParams(key) { - if (!(key instanceof Uint8Array)) throw new TypeError(`Key must be a Buffer (received ${typeOf(key)})`); - if (key.length != 96) throw new RangeError('Invalid key'); + const keyArr = typeof key == 'string' ? new Uint8Array([ ...atob(key) ].map(ch => ch.charCodeAt(0))) : key; + if (!(keyArr instanceof Uint8Array)) throw new TypeError(`key must be a Uint8Array/Buffer (received ${typeOf(keyArr)})`); + if (keyArr.length != 96) throw new RangeError('invalid key'); // use the underlying ArrayBuffer to view key in structured format - const buffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength); + const keyBuff = keyArr.buffer.slice(keyArr.byteOffset, keyArr.byteOffset + keyArr.byteLength); // a structured view of the derived key const struct = { - scrypt: new Uint8Array(buffer, 0, 6), + scrypt: new Uint8Array(keyBuff, 0, 6), params: { - v: new DataView(buffer, 6, 1), - logN: new DataView(buffer, 7, 1), - r: new DataView(buffer, 8, 4), - p: new DataView(buffer, 12, 4), + v: new DataView(keyBuff, 6, 1), + logN: new DataView(keyBuff, 7, 1), + r: new DataView(keyBuff, 8, 4), + p: new DataView(keyBuff, 12, 4), }, - salt: new Uint8Array(buffer, 16, 32), - checksum: new Uint8Array(buffer, 48, 16), - hmachash: new Uint8Array(buffer, 64, 32), + salt: new Uint8Array(keyBuff, 16, 32), + checksum: new Uint8Array(keyBuff, 48, 16), + hmachash: new Uint8Array(keyBuff, 64, 32), }; const params = { @@ -247,7 +253,7 @@ class Scrypt { let i = 0; const start = performance.now(); while (performance.now()-start < 1) { - crypto.scryptSync('', '', 64, { N: 128, r: 1, p: 1 }); + opensslScryptSync('', '', 64, { N: 128, r: 1, p: 1 }); i += 512; // we invoked the salsa20/8 core 512 times } const duration = (performance.now()-start) / 1000; // in seconds @@ -292,4 +298,4 @@ function typeOf(obj) { /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -module.exports = Scrypt; // ≡ export default Scrypt; +export default Scrypt; diff --git a/test/scrypt-tests.js b/test/scrypt-tests.js deleted file mode 100644 index d5d3ee4..0000000 --- a/test/scrypt-tests.js +++ /dev/null @@ -1,209 +0,0 @@ -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -/* Tests for scrypt key derivation function. (c) C.Veness 2018-2019 */ -/* MIT Licence */ -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -const expect = require('chai').expect; // BDD/TDD assertion library - -const Scrypt = require('../scrypt.js'); - -const password = 'my secret password'; -const key0salt = 'c2NyeXB0AAwAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA08wOZXFAec6Si7mP1SzrmK6Pvpx2zfUEXXAuM68S4DAnUER44bh+BxsnxMC75Jgs'; - - -describe('Scrypt tests', function() { - this.slow(20); - - describe('Hash & verify (base64)', function() { - this.slow(200); // kdf() is intentionally slow - - it('with just logN param', async function() { - const key = (await Scrypt.kdf(password, { logN: 12 })).toString('base64'); - expect(Scrypt.viewParams(Buffer.from(key, 'base64'))).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(Buffer.from(key, 'base64'), password)).to.be.true; - }); - - it('with logN, r, p params', async function() { - const key = (await Scrypt.kdf(password, { logN: 12, r: 9, p: 2 })).toString('base64'); - expect(Scrypt.viewParams(Buffer.from(key, 'base64'))).to.deep.equal({ logN: 12, r: 9, p: 2 }); - expect(await Scrypt.verify(Buffer.from(key, 'base64'), password)).to.be.true; - }); - - it('with params as strings', async function() { - const key = (await Scrypt.kdf(password, { logN: '12', r: '8', p: '1' })).toString('base64'); - expect(Scrypt.viewParams(Buffer.from(key, 'base64'))).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(Buffer.from(key, 'base64'), password)).to.be.true; - }); - - it('fails to verify with bad passphrase', async function() { - const key = (await Scrypt.kdf(password, { logN: 12 })).toString('base64'); - expect(await Scrypt.verify(Buffer.from(key, 'base64'), 'wrong password')).to.be.false; - }); - }); - - describe('Verify previous key (base64)', function() { - it('verifies null-salt key', async function() { - expect(await Scrypt.verify(Buffer.from(key0salt, 'base64'), password)).to.be.true; - }); - - it('fails to verify null-salt key with bad passphrase', async function() { - expect(await Scrypt.verify(Buffer.from(key0salt, 'base64'), 'wrong password')).to.be.false; - }); - }); - - describe('Uint8Array/Buffer key', function() { - this.slow(200); // kdf() is intentionally slow - - it('Uint8Array', async function() { - const pwUint8Array = new Uint8Array([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]); - const key = await Scrypt.kdf(pwUint8Array, { logN: 12 }); - expect(Scrypt.viewParams(new Uint8Array(key))).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(new Uint8Array(key), pwUint8Array)).to.be.true; - }); - - it('Buffer', async function() { - const pwBuffer = Buffer.from([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]); - const key = await Scrypt.kdf(pwBuffer, { logN: 12 }); - expect(Scrypt.viewParams(key)).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(key, pwBuffer)).to.be.true; - }); - }); - - describe('TypedArray/Buffer passphrase', function() { - this.slow(200); // kdf() is intentionally slow - - it('Uint8Array', async function() { - const pwTypedArray = new Uint8Array([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]); - const key = await Scrypt.kdf(pwTypedArray, { logN: 12 }); - expect(Scrypt.viewParams(key)).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(key, pwTypedArray)).to.be.true; - }); - - it('Float64Array', async function() { - const pwTypedArray = new Float64Array([ 99.8, 98.7, 97.6, 96.5 ]); - const key = await Scrypt.kdf(pwTypedArray, { logN: 12 }); - expect(Scrypt.viewParams(key)).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(key, pwTypedArray)).to.be.true; - }); - - it('Buffer', async function() { - const pwBuffer = Buffer.from([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]); - const key = await Scrypt.kdf(pwBuffer, { logN: 12 }); - expect(Scrypt.viewParams(key)).to.deep.equal({ logN: 12, r: 8, p: 1 }); - expect(await Scrypt.verify(key, pwBuffer)).to.be.true; - }); - }); - - describe('Pick params', function() { - it('Picks params for 100ms', async function() { - const params = Scrypt.pickParams(0.1, 1024*1024*1024, 0.5); - expect(params).to.have.all.keys('logN', 'r', 'p'); - expect(params.logN).to.be.within(8, 20); - expect(params.r).to.equal(8); - expect(params.p).to.equal(1); - }); - - it('Picks params with default maxmem/maxmemfrac', async function() { - const params = Scrypt.pickParams(0.1); - expect(params).to.have.all.keys('logN', 'r', 'p'); - expect(params.logN).to.be.within(8, 20); - expect(params.r).to.equal(8); - expect(params.p).to.equal(1); - }); - - it('Picks params with 0 maxmem', async function() { - const params = Scrypt.pickParams(0.1, 0); - expect(params.logN).to.be.within(8, 20); - }); - - it('Picks params with 0 maxmemfrac', async function() { - const params = Scrypt.pickParams(0.1, 0, 0); - expect(params.logN).to.be.within(8, 20); - }); - - it('Picks params setting N based on memory limit', async function() { - const params = Scrypt.pickParams(1, 1024, 0.1); - expect(params.logN).to.be.within(8, 20); - expect(params.p).to.be.above(1); - }); - }); - - describe('Error checking', function() { - - describe('kdf errors', function() { - it('throws on numeric passphrase', () => Scrypt.kdf(99) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Passphrase must be a string, TypedArray, or Buffer (received number)'))); - it('throws on no params', () => Scrypt.kdf(password) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Params must be an object (received undefined)'))); - it('throws on bad params', () => Scrypt.kdf(password, null) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Params must be an object (received null)'))); - it('throws on bad params', () => Scrypt.kdf(password, false) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Params must be an object (received boolean)'))); - it('throws on bad params', () => Scrypt.kdf(password, 99) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Params must be an object (received number)'))); - it('throws on bad params', () => Scrypt.kdf(password, 'bad params') - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Params must be an object (received string)'))); - it('throws on bad logN', () => Scrypt.kdf(password, { logN: 'bad' }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter logN must be an integer; received bad'))); - it('throws on zero logN', () => Scrypt.kdf(password, { logN: 0 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter logN must be between 1 and 30; received 0'))); - it('throws on non-integer logN', () => Scrypt.kdf(password, { logN: 12.12 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter logN must be an integer; received 12.12'))); - it('throws on non-integer r', () => Scrypt.kdf(password, { logN: 12, r: 8.8 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter r must be a positive integer; received 8.8'))); - it('throws on non-integer p', () => Scrypt.kdf(password, { logN: 12, p: 1.1 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter p must be a positive integer; received 1.1'))); - it('throws on 0 r', () => Scrypt.kdf(password, { logN: 12, r: 0 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter r must be a positive integer; received 0'))); - it('throws on 0 p', () => Scrypt.kdf(password, { logN: 12, p: 0 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameter p must be a positive integer; received 0'))); - it('throws on out-of-range r', () => Scrypt.kdf(password, { logN: 12, r: 2**30 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameters p*r must be <= 2^30-1'))); - it('throws on out-of-range p', () => Scrypt.kdf(password, { logN: 12, p: 2**30 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Parameters p*r must be <= 2^30-1'))); - it('throws on EVP PBE memory limit exceeded', () => Scrypt.kdf(password, { logN: 12, r: 2**20 }) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.match(/EVP_PBE_scrypt:memory limit exceeded$/))); - }); - - describe('verify errors', function() { - it('throws on bad passphrase type', async () => Scrypt.verify(await Scrypt.kdf(password, { logN: 12 }), null) - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Passphrase must be a string, TypedArray, or Buffer (received null)'))); - it('throws on bad key type', () => Scrypt.verify(null, 'passwd') - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Key must be a Buffer (received null)'))); - it('throws on bad key', () => Scrypt.verify(Buffer.from('key', 'base64'), 'passwd') - .then(() => { throw new Error('Test should fail'); }) - .catch(error => expect(error.message).to.equal('Invalid key'))); - it('fails to verify on checksum failure', async () => { - const key = await Scrypt.kdf(password, { logN: 12 }); - key[7] = 11; // patch logN to new value - expect(Scrypt.viewParams(key)).to.deep.equal({ logN: 11, r: 8, p: 1 }); - expect(await Scrypt.verify(key, password)).to.be.false; - }); - }); - - describe('viewParams errors', function() { // note Scrypt.viewParams is not async - it('throws on null key', () => expect(() => Scrypt.viewParams(null)).to.throw(TypeError, 'Key must be a Buffer')); - it('throws on numeric key', () => expect(() => Scrypt.viewParams(99)).to.throw(TypeError, 'Key must be a Buffer')); - it('throws on invalid key', () => expect(() => Scrypt.viewParams(Buffer.from('bad key', 'base64'))).to.throw(RangeError, 'Invalid key')); - }); - - }); -}); diff --git a/test/tests-deno.js b/test/tests-deno.js new file mode 100644 index 0000000..c64150a --- /dev/null +++ b/test/tests-deno.js @@ -0,0 +1,209 @@ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* Tests for scrypt key derivation function using Deno. */ +/* © 2024 Chris Veness / Movable Type Ltd */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +import { assert, assertEquals, assertThrows, assertRejects } from 'jsr:@std/assert'; + +import { Buffer } from 'node:buffer'; + +import Scrypt from '../scrypt.js'; + +const password = 'my secret password'; +const key0salt = 'c2NyeXB0AAwAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA08wOZXFAec6Si7mP1SzrmK6Pvpx2zfUEXXAuM68S4DAnUER44bh+BxsnxMC75Jgs'; + + +Deno.test('Hash & verify (base64)', async function(t) { + + await t.step('with just logN param, args as Buffer', async function() { + const keyBuff = await Scrypt.kdf(password, { logN: 12 }); + assertEquals(Scrypt.viewParams(keyBuff), { logN: 12, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(keyBuff, password), true); + }); + + await t.step('with logN, r, p params, args as Buffer', async function() { + const keyBuff = await Scrypt.kdf(password, { logN: 12, r: 9, p: 2 }); + assertEquals(Scrypt.viewParams(keyBuff), { logN: 12, r: 9, p: 2 }); + assertEquals(await Scrypt.verify(keyBuff, password), true); + }); + + await t.step('with args as strings', async function() { + const keyStr = (await Scrypt.kdf(password, { logN: '12', r: '8', p: '1' })).toString('base64'); + assertEquals(Scrypt.viewParams(keyStr), { logN: 12, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(keyStr, password), true); + }); + + await t.step('fails to verify with bad passphrase', async function() { + const keyStr = (await Scrypt.kdf(password, { logN: 12 })).toString('base64'); + assertEquals(await Scrypt.verify(keyStr, 'wrong password'), false); + }); +}); + +Deno.test('Verify previous key (base64)', async function(t) { + await t.step('verifies null-salt key', async function() { + assertEquals(await Scrypt.verify(Buffer.from(key0salt, 'base64'), password), true); + }); + + await t.step('fails to verify null-salt key with bad passphrase', async function() { + assertEquals(await Scrypt.verify(Buffer.from(key0salt, 'base64'), 'wrong password'), false); + }); +}); + +Deno.test('Args as String/Uint8Array/Buffer', async function(t) { + + await t.step('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'); + assertEquals(Scrypt.viewParams(keyStr), { logN: 12, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(keyStr, pwStr), true); + }); + + await t.step('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 })); + assertEquals(Scrypt.viewParams(keyArr), { logN: 12, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(keyArr, pwArr), true); + }); + + await t.step('Buffer', async function() { + const pwBuff = Buffer.from([ 99, 98, 97, 96, 95, 94, 94, 92, 91 ]); + const keyBuff = await Scrypt.kdf(pwBuff, { logN: 12 }); + assertEquals(Scrypt.viewParams(keyBuff), { logN: 12, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(keyBuff, pwBuff), true); + }); +}); + +Deno.test('Pick params', async function(t) { + await t.step('Picks params for 100ms', function() { + const params = Scrypt.pickParams(0.1, 1024*1024*1024, 0.5); + assertEquals(Object.keys(params), [ 'logN', 'r', 'p' ]); + assert(params.logN >= 8 && params.logN <= 20); + assertEquals(params.r, 8); + assertEquals(params.p, 1); + }); + + await t.step('Picks params with default maxmem/maxmemfrac', function() { + const params = Scrypt.pickParams(0.1); + assertEquals(Object.keys(params), [ 'logN', 'r', 'p' ]); + assert(params.logN >= 8 && params.logN <= 20); + assertEquals(params.r, 8); + assertEquals(params.p, 1); + }); + + await t.step('Picks params with 0 maxmem', function() { + const params = Scrypt.pickParams(0.1, 0); + assert(params.logN >= 8 && params.logN <= 20); + }); + + await t.step('Picks params with 0 maxmemfrac', function() { + const params = Scrypt.pickParams(0.1, 0, 0); + assert(params.logN >= 8 && params.logN <= 20); + }); + + await t.step('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); + }); +}); + +Deno.test('Kdf errors', async function(t) { + await t.step('rejects on numeric passphrase', function() { + assertRejects(async () => await Scrypt.kdf(99)); + }); + + await t.step('rejects on no params', function() { + assertRejects(async () => await Scrypt.kdf(password)); + }); + + await t.step('rejects on bad params', function() { + assertRejects(async () => await Scrypt.kdf(password, null)); + }); + + await t.step('rejects on bad params', function() { + assertRejects(async () => await Scrypt.kdf(password, false)); + }); + + await t.step('rejects on bad params', function() { + assertRejects(async () => await Scrypt.kdf(password, 99)); + }); + + await t.step('rejects on bad params', function() { + assertRejects(async () => await Scrypt.kdf(password, 'bad params')); + }); + + await t.step('rejects on bad logN', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 'bad' })); + }); + + await t.step('rejects on zero logN', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 0 })); + }); + + await t.step('rejects on non-integer logN', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12.12 })); + }); + + await t.step('rejects on non-integer r', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, r: 8.8 })); + }); + + await t.step('rejects on non-integer p', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, p: 1.1 })); + }); + + await t.step('rejects on 0 r', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, r: 0 })); + }); + + await t.step('rejects on 0 p', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, p: 0 })); + }); + + await t.step('rejects on out-of-range r', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, r: 2**30 })); + }); + + await t.step('rejects on out-of-range p', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, p: 2**30 })); + }); + + await t.step('rejects on EVP PBE memory limit exceeded', function() { + assertRejects(async () => await Scrypt.kdf(password, { logN: 12, r: 2 ** 20 })); + }); +}); + +Deno.test('Verify errors', async function(t) { + await t.step('rejects on bad passphrase type', function() { + assertRejects(async () => await Scrypt.verify(await Scrypt.kdf(password, { logN: 12 }), null)); + }); + + await t.step('rejects on bad key type', function() { + assertRejects(async () => await Scrypt.verify(null, 'passwd')); + }); + + await t.step('rejects on bad key', function() { + assertRejects(async () => await Scrypt.verify(Buffer.from('key', 'base64'), 'passwd')); + }); + + await t.step('fails to verify on checksum failure', async function() { + const key = await Scrypt.kdf(password, { logN: 12 }); + key[7] = 11; // patch logN to new value + assertEquals(Scrypt.viewParams(key), { logN: 11, r: 8, p: 1 }); + assertEquals(await Scrypt.verify(key, password), false); + }); +}); + +Deno.test('ViewParams errors', async function(t) { // note Scrypt.viewParams is not async + await t.step('throws on null key', function() { + assertThrows(() => Scrypt.viewParams(null)); + }); + + await t.step('throws on numeric key', function() { + assertThrows(() => Scrypt.viewParams(99)); + }); + + await t.step('throws on invalid key', function() { + assertThrows(() => Scrypt.viewParams(Buffer.from('bad key', 'base64'))); + }); +}); diff --git a/test/tests-node.js b/test/tests-node.js new file mode 100644 index 0000000..f2f3592 --- /dev/null +++ b/test/tests-node.js @@ -0,0 +1,210 @@ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* 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')); + }); +});