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.
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
});
});
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
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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With