From 5c90dcd8088365a17f699dd683958536bc33c08c Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Sat, 30 Jul 2022 14:12:04 -0700 Subject: [PATCH] support providing multiple secrets, always encrypt with first, attempt decryption with all, to allow secret rolling --- lib/common.js | 16 +++++++++++++ lib/mystery-box.js | 36 +++++++++++++++++++++-------- test/lib/common.js | 17 ++++++++++++++ test/lib/mystery-box.js | 50 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 9 deletions(-) diff --git a/lib/common.js b/lib/common.js index 4c7e639..1d4223b 100644 --- a/lib/common.js +++ b/lib/common.js @@ -19,7 +19,23 @@ const fileScope = (filename) => { } +/** + * Return an array containing x if x is something and not an array + * @param {*} x + */ +const ensureArray = (x) => { + if (x === undefined) { + return []; + } + if (!Array.isArray(x)) { + return Array(x); + } + return x; +}; + + module.exports = { + ensureArray, fileScope, randomBytesAsync, }; \ No newline at end of file diff --git a/lib/mystery-box.js b/lib/mystery-box.js index 2343c5e..cb85ebe 100644 --- a/lib/mystery-box.js +++ b/lib/mystery-box.js @@ -63,15 +63,15 @@ class MysteryBox { /** * @param {Console} logger * @param {Object} options - * @param {String} options.encryptionSecret + * @param {String|String[]} options.encryptionSecret - if an array, will always encrypt with first secret, will attempt to decrypt with all; useful for rolling secrets * @param {Number=} options.defaultFlags */ constructor(logger, options = {}) { this.logger = logger; this.options = options; - this.secret = options.encryptionSecret; - if (!this.secret) { + this.secrets = common.ensureArray(options.encryptionSecret); + if (!this.secrets.length) { throw new Error('missing encryption secret'); } // TODO: support secret rolling @@ -176,7 +176,9 @@ class MysteryBox { // Authenticate all this data const aadBuffer = Buffer.concat([versionBuffer, flagsBuffer, iv, salt]); - const key = await scryptAsync(this.secret, salt, v.keyBytes); + // Always encrypt with first secret + const secret = this.secrets[0]; + const key = await scryptAsync(secret, salt, v.keyBytes); const cipher = crypto.createCipheriv(v.algorithm, key, iv, v.algOptions); cipher.setAAD(aadBuffer); const encrypted = cipher.update(payload); @@ -248,12 +250,28 @@ class MysteryBox { const encrypted = raw.slice(offset); timingsMs.preCrypt = performance.now(); - const key = await scryptAsync(this.secret, salt, v.keyBytes); - const decipher = crypto.createDecipheriv(v.algorithm, key, iv, v.algOptions); - decipher.setAAD(aad); - decipher.setAuthTag(tag); - const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + let decrypted; + let err; + let success = false; + for await (const secret of this.secrets) { + const key = await scryptAsync(secret, salt, v.keyBytes); + const decipher = crypto.createDecipheriv(v.algorithm, key, iv, v.algOptions); + decipher.setAAD(aad); + decipher.setAuthTag(tag); + + try { + decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + success = true; + break; + } catch (e) { + err = e; + continue; + } + } + if (!success) { + throw err; + } let payload; timingsMs.preCompress = timingsMs.postCrypt = performance.now(); diff --git a/test/lib/common.js b/test/lib/common.js index 344e162..ad93ea1 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -19,4 +19,21 @@ describe('Common', function () { }); }); // fileScope + describe('ensureArray', function () { + it('returns empty array for no data', function () { + const result = common.ensureArray(); + assert.deepStrictEqual(result, []); + }); + it('returns same array passed in', function () { + const expected = [1, 2, 3, 'foo']; + const result = common.ensureArray(expected); + assert.deepStrictEqual(result, expected); + }); + it('returns array containing non-array data', function () { + const data = 'bar'; + const result = common.ensureArray(data); + assert.deepStrictEqual(result, [data]); + }); + }); // ensureArray + }); // Common diff --git a/test/lib/mystery-box.js b/test/lib/mystery-box.js index a6e69b9..802d8b3 100644 --- a/test/lib/mystery-box.js +++ b/test/lib/mystery-box.js @@ -30,6 +30,23 @@ describe('MysteryBox', function () { } }); + it('accepts multiple secrets', async function () { + this.slow(500); + options = { + encryptionSecret: ['first poor secret', 'second poor secret'], + }; + mb = new MysteryBox(stubLogger, options); + object = { + foo: 'bar', + baz: 'quux', + flarp: 13, + }; + const encryptedResult = await mb.pack(object); + const decryptedResult = await mb.unpack(encryptedResult); + assert.deepStrictEqual(decryptedResult, object); + + }); + it('covers options', function () { try { mb = new MysteryBox(stubLogger); @@ -106,6 +123,38 @@ describe('MysteryBox', function () { assert.deepStrictEqual(decryptedResult, object); }); + it('decrypts secondary (older) secret', async function () { + this.slow(500); + const oldmb = new MysteryBox(stubLogger, { encryptionSecret: 'old secret' }); + const newmb = new MysteryBox(stubLogger, { encryptionSecret: ['new secret', 'old secret'] }); + object = { + foo: 'bar', + baz: 'quux', + flarp: 13, + }; + const oldEncrypted = await oldmb.pack(object); + const newDecrypted = await newmb.unpack(oldEncrypted); + assert.deepStrictEqual(newDecrypted, object); + }); + + it('fails to decrypt invalid secret', async function () { + this.slow(500); + const oldmb = new MysteryBox(stubLogger, { encryptionSecret: 'very old secret' }); + const newmb = new MysteryBox(stubLogger, { encryptionSecret: ['new secret', 'old secret'] }); + object = { + foo: 'bar', + baz: 'quux', + flarp: 13, + }; + const oldEncrypted = await oldmb.pack(object); + try { + await newmb.unpack(oldEncrypted); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof Error); + } + }); + it('encrypts and decrypts all available versions +brotli', async function () { Object.keys(mb.versionParameters).map((v) => Number(v)).forEach(async (version) => { object = { @@ -172,6 +221,7 @@ describe('MysteryBox', function () { }); it('handles incomplete', async function () { + this.slow(500); const encryptedResult = await mb.pack({ foo: 'bar' }); try { await mb.unpack(encryptedResult.slice(0, 6)); -- 2.43.2