}
+/**
+ * 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
/**
* @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
// 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);
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();
});
}); // 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
}
});
+ 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);
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 = {
});
it('handles incomplete', async function () {
+ this.slow(500);
const encryptedResult = await mb.pack({ foo: 'bar' });
try {
await mb.unpack(encryptedResult.slice(0, 6));