Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Benchmarking WebCrypto is much slower than third-party libraries?

I am evaluating how WebCrypto performance compares to third-party crypto libraries SJCL and Forge. I would expect WebCrypto to be much faster since it is a native browser implementation. This has also been benchmarked before and has shown such.

I have implemented the following tests using Benchmark.js to test key derivation (PBKDF2-SHA256), encrypt (AES-CBC), and decrypt (AES-CBC). These tests show web crypto to be significantly slower than both SJCL and Forge for encrypt/decrypt.

Benchmark Code

See fiddle here: https://jsfiddle.net/kspearrin/1Lzvpzkz/

var iterations = 5000;
var keySize = 256;

sjcl.beware['CBC mode is dangerous because it doesn\'t protect message integrity.']();

// =========================================================
// Precomputed enc values for decrypt benchmarks
// =========================================================

var encIv = 'FX7Y3pYmcLIQt6WrKc62jA==';
var encCt = 'EDlxtzpEOfGIAIa8PkCQmA==';

// =========================================================
// Precomputed keys for benchmarks
// =========================================================

function sjclMakeKey() {
  return sjcl.misc.pbkdf2('mypassword', 'a salt', iterations, keySize, null);
}

var sjclKey = sjclMakeKey();

function forgeMakeKey() {
  return forge.pbkdf2('mypassword', 'a salt', iterations, keySize / 8, 'sha256');
}

var forgeKey = forgeMakeKey();

var webcryptoKey = null;
window.crypto.subtle.importKey(
  'raw', fromUtf8('mypassword'), {
    name: 'PBKDF2'
  },
  false, ['deriveKey', 'deriveBits']
).then(function(importedKey) {
  window.crypto.subtle.deriveKey({
      'name': 'PBKDF2',
      salt: fromUtf8('a salt'),
      iterations: iterations,
      hash: {
        name: 'SHA-256'
      }
    },
    importedKey, {
      name: 'AES-CBC',
      length: keySize
    },
    true, ['encrypt', 'decrypt']
  ).then(function(derivedKey) {
    webcryptoKey = derivedKey;
  });
});

// =========================================================
// IV helpers for encrypt benchmarks so all are using same PRNG methods
// =========================================================

function getRandomSjclBytes() {
  var bytes = new Uint32Array(4);
  return window.crypto.getRandomValues(bytes);
}

function getRandomForgeBytes() {
  var bytes = new Uint8Array(16);
  window.crypto.getRandomValues(bytes);
  return String.fromCharCode.apply(null, bytes);
}

// =========================================================
// Serialization helpers for web crypto
// =========================================================

function fromUtf8(str) {
  var strUtf8 = unescape(encodeURIComponent(str));
  var ab = new Uint8Array(strUtf8.length);
  for (var i = 0; i < strUtf8.length; i++) {
    ab[i] = strUtf8.charCodeAt(i);
  }
  return ab;
}

function toUtf8(buf, inputType) {
  inputType = inputType || 'ab';

  var bytes = new Uint8Array(buf);
  var encodedString = String.fromCharCode.apply(null, bytes),
    decodedString = decodeURIComponent(escape(encodedString));
  return decodedString;
}

function fromB64(str) {
  var binary_string = window.atob(str);
  var len = binary_string.length;
  var bytes = new Uint8Array(len);
  for (var i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i);
  }
  return bytes.buffer;
}

function toB64(buf) {
  var binary = '';
  var bytes = new Uint8Array(buf);
  var len = bytes.byteLength;
  for (var i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return window.btoa(binary);
}

// =========================================================
// The benchmarks
// =========================================================

$("#makekey").click(function() {
  console.log('Starting test: Make Key (PBKDF2)');

  var suite = new Benchmark.Suite;

  suite
    .add('SJCL', function() {
      sjclMakeKey();
    })
    .add('Forge', function() {
      forgeMakeKey();
    })
    .add('WebCrypto', {
      defer: true,
      fn(deferred) {
        window.crypto.subtle.importKey(
          'raw', fromUtf8('mypassword'), {
            name: 'PBKDF2'
          },
          false, ['deriveKey', 'deriveBits']
        ).then(function(importedKey) {
          window.crypto.subtle.deriveKey({
              'name': 'PBKDF2',
              salt: fromUtf8('a salt'),
              iterations: iterations,
              hash: {
                name: 'SHA-256'
              }
            },
            importedKey, {
              name: 'AES-CBC',
              length: keySize
            },
            true, ['encrypt', 'decrypt']
          ).then(function(derivedKey) {
            window.crypto.subtle.exportKey('raw', derivedKey)
              .then(function(exportedKey) {
                deferred.resolve();
              });
          });
        });
      }
    })
    .on('cycle', function(event) {
      console.log(String(event.target));
    })
    .on('complete', function() {
      console.log('Fastest is ' + this.filter('fastest').map('name'));
    })
    .run({
      'async': true
    });
});

// =========================================================
// =========================================================

$("#encrypt").click(function() {
  console.log('Starting test: Encrypt');

  var suite = new Benchmark.Suite;

  suite
    .add('SJCL', function() {
      var response = {};
      var params = {
        mode: 'cbc',
        iv: getRandomSjclBytes()
      };
      var ctJson = sjcl.encrypt(sjclKey, 'some message', params, response);

      var result = {
        ct: ctJson.match(/"ct":"([^"]*)"/)[1],
        iv: sjcl.codec.base64.fromBits(response.iv)
      };
    })
    .add('Forge', function() {
      var buffer = forge.util.createBuffer('some message', 'utf8');
      var cipher = forge.cipher.createCipher('AES-CBC', forgeKey);
      var ivBytes = getRandomForgeBytes();
      cipher.start({
        iv: ivBytes
      });
      cipher.update(buffer);
      cipher.finish();
      var encryptedBytes = cipher.output.getBytes();

      var result = {
        iv: forge.util.encode64(ivBytes),
        ct: forge.util.encode64(encryptedBytes)
      };
    })
    .add('WebCrypto', {
      defer: true,
      fn(deferred) {
        var ivBytes = window.crypto.getRandomValues(new Uint8Array(16));
        window.crypto.subtle.encrypt({
          name: 'AES-CBC',
          iv: ivBytes
        }, webcryptoKey, fromUtf8('some message')).then(function(encrypted) {
          var ivResult = toB64(ivBytes);
          var ctResult = toB64(encrypted);
          deferred.resolve();
        });
      }
    })
    .on('cycle', function(event) {
      console.log(String(event.target));
    })
    .on('complete', function() {
      console.log('Fastest is ' + this.filter('fastest').map('name'));
    })
    .run({
      'async': true
    });
});

// =========================================================
// =========================================================

$("#decrypt").click(function() {
  console.log('Starting test: Decrypt');

  var suite = new Benchmark.Suite;

  suite
    .add('SJCL', function() {
      var ivBits = sjcl.codec.base64.toBits(encIv);
      var ctBits = sjcl.codec.base64.toBits(encCt);
      var aes = new sjcl.cipher.aes(sjclKey);

      var messageBits = sjcl.mode.cbc.decrypt(aes, ctBits, ivBits, null);
      var result = sjcl.codec.utf8String.fromBits(messageBits);
    })
    .add('Forge', function() {
      var decIvBytes = forge.util.decode64(encIv);
      var ctBytes = forge.util.decode64(encCt);
      var ctBuffer = forge.util.createBuffer(ctBytes);

      var decipher = forge.cipher.createDecipher('AES-CBC', forgeKey);
      decipher.start({
        iv: decIvBytes
      });
      decipher.update(ctBuffer);
      decipher.finish();

      var result = decipher.output.toString('utf8');
    })
    .add('WebCrypto', {
      defer: true,
      fn(deferred) {
        var ivBytes = fromB64(encIv);
        var ctBytes = fromB64(encCt);

        window.crypto.subtle.decrypt({
          name: 'AES-CBC',
          iv: ivBytes
        }, webcryptoKey, ctBytes).then(function(decrypted) {
          var result = toUtf8(decrypted);
          deferred.resolve();
        });
      }
    })
    .on('cycle', function(event) {
      console.log(String(event.target));
    })
    .on('complete', function() {
      console.log('Fastest is ' + this.filter('fastest').map('name'));
    })
    .run({
      'async': true
    });
});

Benchmark Results (Chrome)

Starting test: Make Key (PBKDF2)
SJCL x 26.31 ops/sec ±1.11% (37 runs sampled)
Forge x 13.55 ops/sec ±1.46% (26 runs sampled)
WebCrypto x 172 ops/sec ±2.71% (58 runs sampled)
Fastest is WebCrypto

Starting test: Encrypt
SJCL x 42,618 ops/sec ±1.43% (60 runs sampled)
Forge x 76,653 ops/sec ±1.76% (60 runs sampled)
WebCrypto x 18,011 ops/sec ±5.16% (47 runs sampled)
Fastest is Forge

Starting test: Decrypt
SJCL x 79,352 ops/sec ±2.51% (50 runs sampled)
Forge x 154,463 ops/sec ±2.12% (61 runs sampled)
WebCrypto x 22,368 ops/sec ±4.08% (53 runs sampled)
Fastest is Forge

Benchmark Results (Firefox)

Starting test: Make Key (PBKDF2)
SJCL x 20.21 ops/sec ±1.18% (34 runs sampled)
Forge x 11.63 ops/sec ±6.35% (30 runs sampled)
WebCrypto x 101 ops/sec ±9.68% (46 runs sampled)
Fastest is WebCrypto

Starting test: Encrypt
SJCL x 32,135 ops/sec ±4.37% (51 runs sampled)
Forge x 99,216 ops/sec ±7.50% (47 runs sampled)
WebCrypto x 11,458 ops/sec ±2.79% (52 runs sampled)
Fastest is Forge

Starting test: Decrypt
SJCL x 87,290 ops/sec ±4.35% (45 runs sampled)
Forge x 114,086 ops/sec ±6.76% (46 runs sampled)
WebCrypto x 10,170 ops/sec ±3.69% (42 runs sampled)
Fastest is Forge

What is going on here? Why is WebCrypto so much slower for encrypt/decrypt functions? Am I using Benchmark.js incorrectly or something?

like image 878
kspearrin Avatar asked Mar 13 '17 20:03

kspearrin


1 Answers

I have a hunch that, with such a short message length, you're mostly measuring invocation overhead. With its asynchronous promise-based interface, WebCrypto probably loses out a bit there.

I modified your encryption benchmark to use a 1.5 kib plaintext, and the results look very different:

Starting test: Encrypt
SJCL x 3,632 ops/sec ±2.20% (61 runs sampled)
Forge x 2,968 ops/sec ±3.02% (60 runs sampled)
WebCrypto x 5,522 ops/sec ±6.94% (42 runs sampled)
Fastest is WebCrypto

With a 96 kib plaintext, the difference is even greater:

Starting test: Encrypt
SJCL x 56.77 ops/sec ±5.43% (49 runs sampled)
Forge x 48.17 ops/sec ±1.12% (41 runs sampled)
WebCrypto x 162 ops/sec ±4.53% (45 runs sampled)
Fastest is WebCrypto
like image 101
Ilmari Karonen Avatar answered Sep 29 '22 09:09

Ilmari Karonen