const crypto = require('crypto');
const zlib = require('zlib');
const { promisify } = require('util');
+const { base64ToBase64URL, base64URLToBase64 } = require('@squeep/base64url');
const common = require('./common');
const allVersions = require('./version-parameters');
const { performance } = require('perf_hooks');
/**
* @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 tag = cipher.getAuthTag();
const merged = Buffer.concat([versionBuffer, flagsBuffer, iv, salt, tag, encrypted, final]).toString('base64');
- const result = common.base64ToBase64URL(merged);
+ const result = base64ToBase64URL(merged);
timingsMs.end = timingsMs.postCrypt = performance.now();
this.logger.debug(_scope, 'statistics', { version, flags: this._prettyFlags(flags), serialized: contents.length, compressed: payload.length, encoded: result.length, ...MysteryBox._timingsLog(timingsMs) });
throw new RangeError('nothing to unpack');
}
- const raw = Buffer.from(common.base64URLToBase64(box), 'base64');
+ const raw = Buffer.from(base64URLToBase64(box), 'base64');
let offset = 0;
const version = raw.slice(offset, 1).readUInt8(0);
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();