From eca13126843f7f5a8fa5fb1a76ce08dacf95958a Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Fri, 23 Jul 2021 10:17:57 -0700 Subject: [PATCH 1/6] chore: use pbkdf2 per transform --- index.js | 37 +++++++++++++++++++++++++++++-------- package-lock.json | 40 ++++++++-------------------------------- package.json | 1 + test.js | 25 ++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index 59e4f95..fc76249 100644 --- a/index.js +++ b/index.js @@ -1,31 +1,52 @@ const { secretbox, hash, randomBytes } = require('tweetnacl') const { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } = require('tweetnacl-util') +const { pbkdf2 } = require('pbkdf2') const NO_PASSWORD = 'A password is required for encryption or decryption.' const COULD_NOT_DECRYPT = 'Could not decrypt!' +const SALT_LENGTH = secretbox.nonceLength +const KEY_LENGTH = secretbox.keyLength +const ITERATIONS = 1e3 +const HASH = 'sha512' + module.exports = class Crypt { + static async deriveKey (password, salt) { + if (!salt) { salt = randomBytes(SALT_LENGTH) } + const key = await new Promise((resolve, reject) => { + pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, HASH, (err, key) => { + /* istanbul ignore next */ + if (err) { return reject(err) } else { return resolve(key) } + }) + }) + return { key, salt } + } + constructor (password) { if (!password) { throw new Error(NO_PASSWORD) } - this._key = hash(decodeUTF8(password)).slice(0, secretbox.keyLength) + this._pass = hash(decodeUTF8(password)).slice(0, KEY_LENGTH) } async encrypt (plaintext) { + const { key, salt } = await Crypt.deriveKey(this._pass) const nonce = randomBytes(secretbox.nonceLength) const messageUint8 = decodeUTF8(plaintext) - const box = secretbox(messageUint8, nonce, this._key) - const fullMessage = new Uint8Array(nonce.length + box.length) - fullMessage.set(nonce) - fullMessage.set(box, nonce.length) + const box = secretbox(messageUint8, nonce, key) + const fullMessage = new Uint8Array(salt.length + nonce.length + box.length) + fullMessage.set(salt) + fullMessage.set(nonce, salt.length) + fullMessage.set(box, salt.length + nonce.length) const base64FullMessage = encodeBase64(fullMessage) return base64FullMessage } async decrypt (messageWithNonce) { const messageWithNonceAsUint8Array = decodeBase64(messageWithNonce) - const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength) - const message = messageWithNonceAsUint8Array.slice(secretbox.nonceLength) - const decrypted = secretbox.open(message, nonce, this._key) + const salt = messageWithNonceAsUint8Array.slice(0, SALT_LENGTH) + const nonce = messageWithNonceAsUint8Array.slice(SALT_LENGTH, SALT_LENGTH + secretbox.nonceLength) + const message = messageWithNonceAsUint8Array.slice(SALT_LENGTH + secretbox.nonceLength) + const { key } = await Crypt.deriveKey(this._pass, salt) + const decrypted = secretbox.open(message, nonce, key) if (!decrypted) { throw new Error(COULD_NOT_DECRYPT) } else { diff --git a/package-lock.json b/package-lock.json index 5afc1d5..5f88e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,11 @@ "requires": true, "packages": { "": { + "name": "garbados-crypt", "version": "1.0.2-alpha", "license": "Apache-2.0", "dependencies": { + "pbkdf2": "^3.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" }, @@ -1714,7 +1716,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2107,7 +2108,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -2120,7 +2120,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -4020,7 +4019,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -4034,7 +4032,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4176,8 +4173,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inline-source-map": { "version": "0.6.2", @@ -5020,7 +5016,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -6843,7 +6838,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -7608,7 +7602,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -7626,8 +7619,7 @@ "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "node_modules/safe-regex": { "version": "1.1.0", @@ -7748,7 +7740,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8358,7 +8349,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -9183,8 +9173,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/uuid": { "version": "3.4.0", @@ -11464,7 +11453,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -11816,7 +11804,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -11829,7 +11816,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -13451,7 +13437,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, "requires": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -13462,7 +13447,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13582,8 +13566,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "inline-source-map": { "version": "0.6.2", @@ -14248,7 +14231,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -15752,7 +15734,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -16375,7 +16356,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -16393,8 +16373,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "1.1.0", @@ -16502,7 +16481,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -17048,7 +17026,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -17758,8 +17735,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.4.0", diff --git a/package.json b/package.json index f0d3bdb..cd0e7a3 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "tinyify": "^3.0.0" }, "dependencies": { + "pbkdf2": "^3.1.2", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } diff --git a/test.js b/test.js index 8951417..59b6209 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ /* global describe, it */ const assert = require('assert').strict +const { encodeBase64 } = require('tweetnacl-util') const Crypt = require('.') const PLAINTEXT = 'hello world' @@ -7,6 +8,28 @@ const PASSWORD = 'password' const BENCHMARK = 1e4 // note: 1e4 = 1 and 4 zeroes (10,000) describe('crypt', function () { + it('should derive a key from a password', async function () { + let { key, salt } = await Crypt.deriveKey(PASSWORD) + key = encodeBase64(key) + assert.equal(typeof key, 'string') + let { key: key2 } = await Crypt.deriveKey(PASSWORD, salt) + key2 = encodeBase64(key2) + assert.equal(key, key2) + }) + + it('should require a password', function () { + let ok = false + try { + const crypt = new Crypt() + throw new Error(`crypt created: ${!!crypt}`) + } catch (error) { + if (error.message === 'A password is required for encryption or decryption.') { + ok = true + } + } + assert(ok) + }) + it('should do the crypto dance', async function () { const crypt = new Crypt(PASSWORD) const ciphertext = await crypt.encrypt(PLAINTEXT) @@ -29,7 +52,7 @@ describe('crypt', function () { }) it(`should do the crypto dance ${BENCHMARK} times`, async function () { - this.timeout(BENCHMARK) // assume each op will take no more than 1ms + this.timeout(BENCHMARK * 10) // assume each op will take no more than 10ms const crypt = new Crypt(PASSWORD) for (let i = 0; i < BENCHMARK; i++) { const ciphertext = await crypt.encrypt(PLAINTEXT) From 66f04c7fad77c756e1fc59d4039072038d89f605 Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Fri, 23 Jul 2021 12:59:22 -0700 Subject: [PATCH 2/6] chore: generate key only once --- index.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index fc76249..c6e6a35 100644 --- a/index.js +++ b/index.js @@ -22,31 +22,33 @@ module.exports = class Crypt { return { key, salt } } - constructor (password) { + constructor (password, salt) { if (!password) { throw new Error(NO_PASSWORD) } - this._pass = hash(decodeUTF8(password)).slice(0, KEY_LENGTH) + this._pass = hash(decodeUTF8(password)).slice(KEY_LENGTH) + this._setup = Crypt.deriveKey(this._pass, salt).then(async ({ key, salt: newSalt }) => { + this._key = key + this._salt = salt || newSalt + }) } async encrypt (plaintext) { - const { key, salt } = await Crypt.deriveKey(this._pass) + await this._setup const nonce = randomBytes(secretbox.nonceLength) const messageUint8 = decodeUTF8(plaintext) - const box = secretbox(messageUint8, nonce, key) - const fullMessage = new Uint8Array(salt.length + nonce.length + box.length) - fullMessage.set(salt) - fullMessage.set(nonce, salt.length) - fullMessage.set(box, salt.length + nonce.length) + const box = secretbox(messageUint8, nonce, this._key) + const fullMessage = new Uint8Array(nonce.length + box.length) + fullMessage.set(nonce) + fullMessage.set(box, nonce.length) const base64FullMessage = encodeBase64(fullMessage) return base64FullMessage } async decrypt (messageWithNonce) { + await this._setup const messageWithNonceAsUint8Array = decodeBase64(messageWithNonce) - const salt = messageWithNonceAsUint8Array.slice(0, SALT_LENGTH) - const nonce = messageWithNonceAsUint8Array.slice(SALT_LENGTH, SALT_LENGTH + secretbox.nonceLength) - const message = messageWithNonceAsUint8Array.slice(SALT_LENGTH + secretbox.nonceLength) - const { key } = await Crypt.deriveKey(this._pass, salt) - const decrypted = secretbox.open(message, nonce, key) + const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength) + const message = messageWithNonceAsUint8Array.slice(secretbox.nonceLength) + const decrypted = secretbox.open(message, nonce, this._key) if (!decrypted) { throw new Error(COULD_NOT_DECRYPT) } else { From ea5392159300813d7edb8a360ef871d3c6dad40c Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Thu, 29 Jul 2021 14:26:00 -0700 Subject: [PATCH 3/6] feat: add import, export, and configurable pbkdf2 iterations --- README.md | 18 ++++++++++++- index.js | 65 ++++++++++++++++++++++++++++++++++------------- package-lock.json | 50 +++++++++++++++++++++++++++++++----- package.json | 2 +- test.js | 15 +++++++++++ 5 files changed, 124 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index f6b8370..d1ebec1 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,23 @@ const Crypt = require('garbados-crypt') const crypt = new Crypt(password) ``` -### new Crypt(password) +### new Crypt(password, [salt, [opts]]) - `password`: A string. Make sure it's good! Or not. +- `salt`: A salt, either as a byte array or a string. If omitted, a random salt is generated. *Rather than bother carrying this with you, use `crypt.export()` and `Crypt.import()` to transport your credentials!* +- `opts`: Options! +- `opts.iterations`: The number of iterations to use to hash your password via [pbkdf2](https://en.wikipedia.org/wiki/PBKDF2). Defaults to 10,000. + +### async Crypt.import(exportString) => new Crypt + +Instantiates a new Crypt instance using an encoded string generated by `crypt.export()`. Use this method to import credentials generated on another device! + +- `exportString`: A string generated by `crypt.export()`. + +### async crypt.export() => string + +Exports a string you can use to create a new Crypt instance with `Crypt.import()`. +*Rather than bother carrying around your password and salt, use this to transport credentials across devices!* ### async crypt.encrypt(plaintext) => ciphertext @@ -60,6 +74,8 @@ const crypt = new Crypt(password) If decryption fails, for example because your password is incorrect, an error will be thrown. +### async crypt. + ## Development First, get the source: diff --git a/index.js b/index.js index c6e6a35..45ce2c9 100644 --- a/index.js +++ b/index.js @@ -1,34 +1,65 @@ const { secretbox, hash, randomBytes } = require('tweetnacl') const { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } = require('tweetnacl-util') -const { pbkdf2 } = require('pbkdf2') +const { pbkdf2, createSHA512 } = require('hash-wasm') const NO_PASSWORD = 'A password is required for encryption or decryption.' const COULD_NOT_DECRYPT = 'Could not decrypt!' -const SALT_LENGTH = secretbox.nonceLength -const KEY_LENGTH = secretbox.keyLength -const ITERATIONS = 1e3 -const HASH = 'sha512' +const SALT_LENGTH = 24 +const KEY_LENGTH = 32 +const ITERATIONS = 1e4 module.exports = class Crypt { - static async deriveKey (password, salt) { + static async deriveKey (password, salt, opts = {}) { + const iterations = opts.iterations || ITERATIONS if (!salt) { salt = randomBytes(SALT_LENGTH) } - const key = await new Promise((resolve, reject) => { - pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, HASH, (err, key) => { - /* istanbul ignore next */ - if (err) { return reject(err) } else { return resolve(key) } - }) + const key = await pbkdf2({ + password, + salt, + iterations, + hashLength: KEY_LENGTH, + hashFunction: createSHA512(), + outputType: 'binary' }) return { key, salt } } - constructor (password, salt) { + static async import (password, importString) { + const fullMessage = decodeBase64(importString) + const tempSalt = fullMessage.slice(0, SALT_LENGTH) + const exportBytes = fullMessage.slice(SALT_LENGTH) + const exportEncrypted = encodeUTF8(exportBytes) + const tempCrypt = new Crypt(password, tempSalt) + const exportString = await tempCrypt.decrypt(exportEncrypted) + const [saltString, opts] = JSON.parse(exportString) + const salt = decodeBase64(saltString) + return new Crypt(password, salt, opts) + } + + constructor (password, salt, opts = {}) { if (!password) { throw new Error(NO_PASSWORD) } - this._pass = hash(decodeUTF8(password)).slice(KEY_LENGTH) - this._setup = Crypt.deriveKey(this._pass, salt).then(async ({ key, salt: newSalt }) => { - this._key = key - this._salt = salt || newSalt - }) + this._raw_pass = password + this._pass = hash(decodeUTF8(password)).slice(0, KEY_LENGTH) + this._opts = { iterations: opts.iterations || ITERATIONS } + this._setup = Crypt.deriveKey(this._pass, salt, this._opts) + .then(({ key, salt: newSalt }) => { + this._key = key + this._salt = salt || newSalt + }) + } + + async export () { + await this._setup + const tempCrypt = new Crypt(this._raw_pass) + await tempCrypt._setup + const saltString = encodeBase64(this._salt) + const exportString = JSON.stringify([saltString, this._opts]) + const exportEncrypted = await tempCrypt.encrypt(exportString) + const exportBytes = decodeUTF8(exportEncrypted) + const fullMessage = new Uint8Array(tempCrypt._salt.length + exportBytes.length) + fullMessage.set(tempCrypt._salt) + fullMessage.set(exportBytes, tempCrypt._salt.length) + return encodeBase64(fullMessage) } async encrypt (plaintext) { diff --git a/package-lock.json b/package-lock.json index 5f88e07..164e503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.2-alpha", "license": "Apache-2.0", "dependencies": { - "pbkdf2": "^3.1.2", + "hash-wasm": "^4.9.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" }, @@ -1716,6 +1716,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2108,6 +2109,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -2120,6 +2122,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -4019,6 +4022,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -4032,6 +4036,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4041,6 +4046,11 @@ "node": ">= 6" } }, + "node_modules/hash-wasm": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.9.0.tgz", + "integrity": "sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -4173,7 +4183,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/inline-source-map": { "version": "0.6.2", @@ -5016,6 +5027,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -6838,6 +6850,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -7602,6 +7615,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -7619,7 +7633,8 @@ "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true }, "node_modules/safe-regex": { "version": "1.1.0", @@ -7740,6 +7755,7 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8349,6 +8365,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -9173,7 +9190,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "node_modules/uuid": { "version": "3.4.0", @@ -11453,6 +11471,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -11804,6 +11823,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -11816,6 +11836,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -13437,6 +13458,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, "requires": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -13447,6 +13469,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13455,6 +13478,11 @@ } } }, + "hash-wasm": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.9.0.tgz", + "integrity": "sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==" + }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -13566,7 +13594,8 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "inline-source-map": { "version": "0.6.2", @@ -14231,6 +14260,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -15734,6 +15764,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -16356,6 +16387,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -16373,7 +16405,8 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true }, "safe-regex": { "version": "1.1.0", @@ -16481,6 +16514,7 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -17026,6 +17060,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -17735,7 +17770,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "uuid": { "version": "3.4.0", diff --git a/package.json b/package.json index cd0e7a3..162eb12 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "tinyify": "^3.0.0" }, "dependencies": { - "pbkdf2": "^3.1.2", + "hash-wasm": "^4.9.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } diff --git a/test.js b/test.js index 59b6209..568d661 100644 --- a/test.js +++ b/test.js @@ -60,4 +60,19 @@ describe('crypt', function () { assert.strictEqual(decryptext, PLAINTEXT) } }) + + it('should export to a string', async function () { + const crypt = new Crypt(PASSWORD) + const exportString = await crypt.export() + assert.equal(typeof exportString, 'string') + }) + + it('should import from an export payload', async function () { + const crypt1 = new Crypt(PASSWORD) + const exportString = await crypt1.export() + const crypt2 = await Crypt.import(PASSWORD, exportString) + const encrypted = await crypt1.encrypt(PLAINTEXT) + const decrypted = await crypt2.decrypt(encrypted) + assert.equal(decrypted, PLAINTEXT) + }) }) From fe2a69d016f4ad7a48e98dc2efff05856a891db9 Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Thu, 29 Jul 2021 14:31:59 -0700 Subject: [PATCH 4/6] fix: no need to slice password hash --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 45ce2c9..9bf7337 100644 --- a/index.js +++ b/index.js @@ -39,7 +39,7 @@ module.exports = class Crypt { constructor (password, salt, opts = {}) { if (!password) { throw new Error(NO_PASSWORD) } this._raw_pass = password - this._pass = hash(decodeUTF8(password)).slice(0, KEY_LENGTH) + this._pass = hash(decodeUTF8(password)) this._opts = { iterations: opts.iterations || ITERATIONS } this._setup = Crypt.deriveKey(this._pass, salt, this._opts) .then(({ key, salt: newSalt }) => { From 193727f20f6965fd036e5f3a857eec2fd13451e1 Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Thu, 29 Jul 2021 14:40:05 -0700 Subject: [PATCH 5/6] fix: readme fixes, variable pedantry --- README.md | 3 ++- index.js | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d1ebec1..c6ef2c6 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ const crypt = new Crypt(password) - `opts`: Options! - `opts.iterations`: The number of iterations to use to hash your password via [pbkdf2](https://en.wikipedia.org/wiki/PBKDF2). Defaults to 10,000. -### async Crypt.import(exportString) => new Crypt +### async Crypt.import(password, exportString) => new Crypt Instantiates a new Crypt instance using an encoded string generated by `crypt.export()`. Use this method to import credentials generated on another device! +- `password`: A string, the same string password you used with the Crypt instance that generated the `exportString`! - `exportString`: A string generated by `crypt.export()`. ### async crypt.export() => string diff --git a/index.js b/index.js index 9bf7337..77f6dad 100644 --- a/index.js +++ b/index.js @@ -24,14 +24,15 @@ module.exports = class Crypt { return { key, salt } } - static async import (password, importString) { - const fullMessage = decodeBase64(importString) + static async import (password, exportString) { + // parse exportString: decodeBase64 => + const fullMessage = decodeBase64(exportString) const tempSalt = fullMessage.slice(0, SALT_LENGTH) const exportBytes = fullMessage.slice(SALT_LENGTH) const exportEncrypted = encodeUTF8(exportBytes) const tempCrypt = new Crypt(password, tempSalt) - const exportString = await tempCrypt.decrypt(exportEncrypted) - const [saltString, opts] = JSON.parse(exportString) + const exportJson = await tempCrypt.decrypt(exportEncrypted) + const [saltString, opts] = JSON.parse(exportJson) const salt = decodeBase64(saltString) return new Crypt(password, salt, opts) } @@ -53,8 +54,8 @@ module.exports = class Crypt { const tempCrypt = new Crypt(this._raw_pass) await tempCrypt._setup const saltString = encodeBase64(this._salt) - const exportString = JSON.stringify([saltString, this._opts]) - const exportEncrypted = await tempCrypt.encrypt(exportString) + const exportJson = JSON.stringify([saltString, this._opts]) + const exportEncrypted = await tempCrypt.encrypt(exportJson) const exportBytes = decodeUTF8(exportEncrypted) const fullMessage = new Uint8Array(tempCrypt._salt.length + exportBytes.length) fullMessage.set(tempCrypt._salt) From 482e61213f181d344704124e9cd4bb85fe2e2991 Mon Sep 17 00:00:00 2001 From: Diana Thayer Date: Thu, 29 Jul 2021 14:42:55 -0700 Subject: [PATCH 6/6] chore: 256-bit salts --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 77f6dad..f001bdc 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ const { pbkdf2, createSHA512 } = require('hash-wasm') const NO_PASSWORD = 'A password is required for encryption or decryption.' const COULD_NOT_DECRYPT = 'Could not decrypt!' -const SALT_LENGTH = 24 +const SALT_LENGTH = 32 const KEY_LENGTH = 32 const ITERATIONS = 1e4