X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fmystery-box.js;h=05fe28f72c872ac80b0604af4d15bbe8f52e069f;hb=9932160785c38ea561d361ffacf1dd515f529a6b;hp=6198cc816484b406804cf4a4cdd3c84685bce60e;hpb=383899395bf599359d60337edb651af0592f82cb;p=squeep-mystery-box diff --git a/lib/mystery-box.js b/lib/mystery-box.js index 6198cc8..05fe28f 100644 --- a/lib/mystery-box.js +++ b/lib/mystery-box.js @@ -3,6 +3,7 @@ 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'); @@ -62,23 +63,27 @@ 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 // Filter any unavailable algorithms const availableCiphers = crypto.getCiphers(); + const availableHashes = crypto.getHashes(); + // Add legacy scrypt to available hashes for filtering key derivations + availableHashes.push('scrypt'); this.versionParameters = Object.entries(allVersions).reduce((acc, [v, p]) => { - if (availableCiphers.includes(p.algorithm)) { + const validCipher = availableCiphers.includes(p.algorithm); + const validKeyDeriver = availableHashes.includes(p.keyDeriver); + if (validCipher && validKeyDeriver) { acc[v] = p; // eslint-disable-line security/detect-object-injection } return acc; @@ -109,6 +114,38 @@ class MysteryBox { } + /** + * Generate key data. + */ + static async _keyFromSecret(deriver, secret, salt, keyBytes) { + switch (deriver) { + case 'shake256': { + const hash = crypto.createHash('shake256', { outputLength: keyBytes }); + hash.update(salt); + hash.update(secret); + return hash.digest(); + } + + case 'blake2b512': { + const hash = crypto.createHash('blake2b512'); + hash.update(salt); + hash.update(secret); + const digest = hash.digest(); + // should assert that keyBytes <= 64 + // but not concerned about that for now + // until we have a new algorithm with bigger key size + return digest.subarray(0, keyBytes); + } + + case 'scrypt': + return scryptAsync(secret, salt, keyBytes); + + default: + throw new RangeError('unsupported key deriver'); + } + } + + /** * Put contents into a mysterious box. * @param {Object|Buffer} contents @@ -175,7 +212,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 MysteryBox._keyFromSecret(v.keyDeriver, secret, salt, v.keyBytes); const cipher = crypto.createCipheriv(v.algorithm, key, iv, v.algOptions); cipher.setAAD(aadBuffer); const encrypted = cipher.update(payload); @@ -183,7 +222,7 @@ class MysteryBox { 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) }); @@ -212,10 +251,10 @@ class MysteryBox { 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 version = raw.subarray(offset, 1).readUInt8(0); if (!(version in this.versionParameters)) { throw new RangeError('unsupported version'); } @@ -228,31 +267,47 @@ class MysteryBox { throw new RangeError('not enough to unpack'); } - const flags = raw.slice(offset, offset + v.flagsBytes).readUInt8(0); + const flags = raw.subarray(offset, offset + v.flagsBytes).readUInt8(0); offset += v.flagsBytes; const { compression, payloadIsBuffer } = MysteryBox._decodeFlags(flags); - const iv = raw.slice(offset, offset + v.ivBytes); + const iv = raw.subarray(offset, offset + v.ivBytes); offset += v.ivBytes; - const salt = raw.slice(offset, offset + v.saltBytes); + const salt = raw.subarray(offset, offset + v.saltBytes); offset += v.saltBytes; - const aad = raw.slice(0, offset); // Everything up to here + const aad = raw.subarray(0, offset); // Everything up to here - const tag = raw.slice(offset, offset + v.tagBytes); + const tag = raw.subarray(offset, offset + v.tagBytes); offset += v.tagBytes; - const encrypted = raw.slice(offset); + const encrypted = raw.subarray(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 MysteryBox._keyFromSecret(v.keyDeriver, 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();