Бенчмаркинг WebCrypto намного медленнее, чем сторонние библиотеки?

Я оцениваю, как производительность WebCrypto сравнивается со сторонними криптографическими библиотеками SJCL и Forge. Я ожидаю, что WebCrypto будет гораздо быстрее, поскольку это собственная реализация браузера. Это также было ранее и показал такое.

Я реализовал следующие тесты, используя Benchmark.js для проверки получения ключа (PBKDF2-SHA256), шифрования (AES-CBC ) и расшифровать (AES-CBC). Эти тесты показывают, что веб-шифрование значительно медленнее, чем SJCL и Forge для шифрования/дешифрования.

Эталонный код

Смотрите скрипку здесь: 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
    });
});

Результаты тестов (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

Результаты тестов (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

Что здесь происходит? Почему WebCrypto намного медленнее для функций шифрования/дешифрования? Я неправильно использую Benchmark.js или что-то в этом роде?


person kspearrin    schedule 13.03.2017    source источник
comment
1. PBKDF2 не должен быть быстрым, разумно безопасное значение составляет 100 мс. 2. если какое-то сообщение представляет собой зашифрованные данные, вы в основном измеряете время установки AES.   -  person zaph    schedule 13.03.2017
comment
@zaph Я понимаю, что PBKDF2 не должен быть быстрым, однако при сравнении его с другими реализациями PBKDF2 вы можете надеяться на лучшую производительность. Это позволит мне ввести больше итераций без ущерба для времени ожидания клиента. Тесты показывают, что PBKDF2 в любом случае намного быстрее с веб-криптографией. Вопрос в основном ориентирован на тесты шифрования/дешифрования.   -  person kspearrin    schedule 13.03.2017
comment
@zaph алгоритм PBKDF2 не должен быть быстрым, но реализация PBKDF2 должна быть настолько быстрой (эффективной), насколько это возможно. Злоумышленник, стремящийся взломать пароли, будет использовать высокооптимизированную реализацию в своей атаке. Если вы используете медленную реализацию, вы либо упрощаете их работу, либо усложняете себе жизнь.   -  person Chris_F    schedule 10.03.2021


Ответы (1)


У меня есть подозрение, что при такой короткой длине сообщения вы в основном измеряете накладные расходы на вызовы. С его асинхронным интерфейсом, основанным на обещаниях, WebCrypto, вероятно, немного проигрывает.

Я изменил ваш тест шифрования, чтобы использовать открытый текст размером 1,5 КБ, и результаты выглядят совсем по-другому:

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

С открытым текстом размером 96 КБ разница еще больше:

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
person Ilmari Karonen    schedule 13.03.2017
comment
Это интересно, так как моя реализация будет использоваться для большого количества небольших битов данных, как показывает тест. Видите ли вы какие-либо способы уменьшить количество подслушанных вызовов для такого варианта использования? - person kspearrin; 13.03.2017
comment
WebCrypto с использованием 96 КиБ составляет около 16 МБ/с. Нативная скорость на iPhone 6S составляет около 350 МБ/с, так что с производительностью по-прежнему сложно работать. - person zaph; 14.03.2017
comment
@kspearrin Есть способы быстрее обрабатывать небольшие шифрования, но они зависят от вашей модели угроз. Например, все они будут или могут быть зашифрованы одним и тем же ключом? - person zaph; 14.03.2017
comment
@zaph Примеры тестов используют одни и те же производные CryptoKey (webcryptoKey) для всех веб-криптотестов. - person kspearrin; 14.03.2017
comment
@kspearrin: кроме микрооптимизаций, таких как повторное использование массива ivBytes, первого параметра для .encrypt() и контекста функции обратного вызова .then() вместо их перераспределения каждый раз, на самом деле нет. Может показаться, что обещания ES6 просто неизбежно медленные. Тем не менее, вы уверены, что даже 0,1 мс на шифрование действительно является серьезной проблемой производительности в вашем случае использования? - person Ilmari Karonen; 14.03.2017