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');
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;
}
+ /**
+ * 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
// Always encrypt with first secret
const secret = this.secrets[0];
- const key = await scryptAsync(secret, salt, v.keyBytes);
+ 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);
const final = cipher.final();
const tag = cipher.getAuthTag();
- const merged = Buffer.concat([versionBuffer, flagsBuffer, iv, salt, tag, encrypted, final]).toString('base64');
- const result = base64ToBase64URL(merged);
+ const result = Buffer.concat([versionBuffer, flagsBuffer, iv, salt, tag, encrypted, final]).toString('base64url');
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(base64URLToBase64(box), 'base64');
+ const raw = Buffer.from(box, 'base64url');
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');
}
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();
let err;
let success = false;
for await (const secret of this.secrets) {
- const key = await scryptAsync(secret, salt, v.keyBytes);
+ 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);