use native base64url encoding, remove external dependency
[squeep-mystery-box] / lib / mystery-box.js
index cb85ebe698453a70f48f6af68bdad8d7cdeb9b08..55967ee2269f00bc97057f7139269ba1491157bb 100644 (file)
@@ -3,7 +3,6 @@
 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');
@@ -74,12 +73,16 @@ class MysteryBox {
     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;
@@ -110,6 +113,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
@@ -178,15 +213,14 @@ class MysteryBox {
 
     // 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) });
@@ -215,10 +249,10 @@ class MysteryBox {
       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');
     }
@@ -231,23 +265,23 @@ 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();
 
@@ -255,7 +289,7 @@ class MysteryBox {
     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);