From 39505c81b13fa7c807ed97c2776674dedda6b2db Mon Sep 17 00:00:00 2001 From: lilia Date: Sun, 26 Oct 2014 20:02:11 -0700 Subject: [PATCH] Finish up webcrypto integration, Fixes #72 We now correctly and opportunistically use the webcrypto API if available, polyfilling if it's not detected. This change also includes a layer of abstraction over the webcrypto interface so we no longer have to deal with key-imports or algorithm names all over the place. Since we no longer support AES-CTR, code outside this file can simply call `textsecure.subtle.(key, data [, iv])`. --- js/crypto.js | 14 ++--- js/test.js | 24 +++----- js/webcrypto.js | 156 +++++++++++++++++++++++++++--------------------- 3 files changed, 104 insertions(+), 90 deletions(-) diff --git a/js/crypto.js b/js/crypto.js index 420c7aad..478bae78 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -60,7 +60,7 @@ window.textsecure.crypto = function() { } function HmacSHA256(key, input) { - return window.textsecure.subtle.sign({name: "HMAC", hash: "SHA-256"}, key, input); + return window.textsecure.subtle.sign(key, input); } testing_only.privToPub = function(privKey, isIdentity) { @@ -690,7 +690,7 @@ window.textsecure.crypto = function() { macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1); return verifyMAC(macInput.buffer, keys[1], mac).then(function() { - return window.textsecure.subtle.decrypt({name: "AES-CBC", iv: keys[2].slice(0, 16)}, keys[0], toArrayBuffer(message.ciphertext)) + return window.textsecure.subtle.decrypt(keys[0], toArrayBuffer(message.ciphertext), keys[2].slice(0, 16)) .then(function(paddedPlaintext) { paddedPlaintext = new Uint8Array(paddedPlaintext); @@ -743,7 +743,7 @@ window.textsecure.crypto = function() { var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { - return window.textsecure.subtle.decrypt({name: "AES-CBC", iv: iv}, aes_key, ciphertext); + return window.textsecure.subtle.decrypt(aes_key, ciphertext, iv); }); }; @@ -757,7 +757,7 @@ window.textsecure.crypto = function() { var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { - return window.textsecure.subtle.decrypt({name: "AES-CBC", iv: iv}, aes_key, ciphertext); + return window.textsecure.subtle.decrypt(aes_key, ciphertext, iv); }); }; @@ -765,7 +765,7 @@ window.textsecure.crypto = function() { var aes_key = keys.slice(0, 32); var mac_key = keys.slice(32, 64); - return window.textsecure.subtle.encrypt({name: "AES-CBC", iv: iv}, aes_key, plaintext).then(function(ciphertext) { + return window.textsecure.subtle.encrypt(aes_key, plaintext, iv).then(function(ciphertext) { var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(ciphertext), 16); @@ -819,7 +819,7 @@ window.textsecure.crypto = function() { msg.counter = chain.chainKey.counter; msg.previousCounter = session.currentRatchet.previousCounter; - return window.textsecure.subtle.encrypt({name: "AES-CBC", iv: keys[2].slice(0, 16)}, keys[0], paddedPlaintext.buffer).then(function(ciphertext) { + return window.textsecure.subtle.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) { msg.ciphertext = ciphertext; var encodedMsg = toArrayBuffer(msg.encode()); @@ -958,7 +958,7 @@ window.textsecure.crypto = function() { var ciphertext = message.slice(16 + 1, message.length - 32); return verifyMAC(ivAndCiphertext, ecRes[1], mac).then(function() { - window.textsecure.subtle.decrypt({name: "AES-CBC", iv: iv}, ecRes[0], ciphertext).then(function(plaintext) { + window.textsecure.subtle.decrypt(ecRes[0], ciphertext, iv).then(function(plaintext) { var identityKeyMsg = textsecure.protobuf.IdentityKey.decode(plaintext); privToPub(toArrayBuffer(identityKeyMsg.identityKey)).then(function(identityKeyPair) { diff --git a/js/test.js b/js/test.js index d8d60ea0..dea558dc 100644 --- a/js/test.js +++ b/js/test.js @@ -35,11 +35,9 @@ describe("Cryptographic primitives", function() { var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f'); var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'); var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644'); - window.textsecure.subtle.importKey('raw', key, {name: 'AES-CBC'}, true, ['encrypt']).then(function(key) { - return window.textsecure.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, plaintext).then(function(result) { - assert.strictEqual(getString(result), getString(ciphertext)); - }).then(done).catch(done); - }).catch(done); + window.textsecure.subtle.encrypt(key, plaintext, iv).then(function(result) { + assert.strictEqual(getString(result), getString(ciphertext)); + }).then(done).catch(done); }); }); @@ -49,11 +47,9 @@ describe("Cryptographic primitives", function() { var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f'); var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'); var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644'); - window.textsecure.subtle.importKey('raw', key, {name: 'AES-CBC'}, true, ['decrypt']).then(function(key) { - return window.textsecure.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, ciphertext).then(function(result) { - assert.strictEqual(getString(result), getString(plaintext)); - }).then(done).catch(done); - }).catch(done); + window.textsecure.subtle.decrypt(key, ciphertext, iv).then(function(result) { + assert.strictEqual(getString(result), getString(plaintext)); + }).then(done).catch(done); }); }); @@ -62,11 +58,9 @@ describe("Cryptographic primitives", function() { var key = hexToArrayBuffer('6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245'); var input = hexToArrayBuffer('752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b'); var mac = getString(hexToArrayBuffer('05d1243e6465ed9620c9aec1c351a186')); - window.textsecure.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, true, ['sign']).then(function(key) { - return window.textsecure.subtle.sign({name: 'HMAC', hash: 'SHA-256'}, key, input).then(function(result) { - assert.strictEqual(getString(result).substring(0, mac.length), mac); - }).then(done).catch(done); - }).catch(done); + window.textsecure.subtle.sign(key, input).then(function(result) { + assert.strictEqual(getString(result).substring(0, mac.length), mac); + }).then(done).catch(done); }); }); diff --git a/js/webcrypto.js b/js/webcrypto.js index 8dbdd7f4..e19061cf 100644 --- a/js/webcrypto.js +++ b/js/webcrypto.js @@ -13,78 +13,98 @@ * along with this program. If not, see . */ -// All inputs/outputs are arraybuffers! -window.textsecure.subtle = (function() { - var StaticArrayBufferProto = new ArrayBuffer().__proto__; - function assertIsArrayBuffer(thing) { - if (thing !== Object(thing) || thing.__proto__ != StaticArrayBufferProto) - throw new Error("Needed a ArrayBuffer"); - } +'use strict'; +;(function() { + // Test for webcrypto support, polyfill if needed. + if (window.crypto.subtle === undefined || window.crypto.subtle === null) { + window.crypto.subtle = (function() { + var StaticArrayBufferProto = new ArrayBuffer().__proto__; + function assertIsArrayBuffer(thing) { + if (thing !== Object(thing) || thing.__proto__ != StaticArrayBufferProto) + throw new Error("Needed a ArrayBuffer"); + } - // private implementation functions - function HmacSHA256(key, input) { - assertIsArrayBuffer(key); - assertIsArrayBuffer(input); - return CryptoJS.HmacSHA256( - CryptoJS.lib.WordArray.create(input), - CryptoJS.enc.Latin1.parse(getString(key)) - ).toString(CryptoJS.enc.Latin1); - }; + // Synchronous implementation functions for polyfilling webcrypto + // All inputs/outputs are arraybuffers! + function HmacSHA256(key, input) { + assertIsArrayBuffer(key); + assertIsArrayBuffer(input); + return CryptoJS.HmacSHA256( + CryptoJS.lib.WordArray.create(input), + CryptoJS.enc.Latin1.parse(getString(key)) + ).toString(CryptoJS.enc.Latin1); + }; - function encryptAESCBC(plaintext, key, iv) { - assertIsArrayBuffer(plaintext); - assertIsArrayBuffer(key); - assertIsArrayBuffer(iv); - return CryptoJS.AES.encrypt(CryptoJS.enc.Latin1.parse(getString(plaintext)), - CryptoJS.enc.Latin1.parse(getString(key)), - {iv: CryptoJS.enc.Latin1.parse(getString(iv))}) - .ciphertext.toString(CryptoJS.enc.Latin1); - }; + function encryptAESCBC(plaintext, key, iv) { + assertIsArrayBuffer(plaintext); + assertIsArrayBuffer(key); + assertIsArrayBuffer(iv); + return CryptoJS.AES.encrypt( + CryptoJS.enc.Latin1.parse(getString(plaintext)), + CryptoJS.enc.Latin1.parse(getString(key)), + { iv: CryptoJS.enc.Latin1.parse(getString(iv)) } + ).ciphertext.toString(CryptoJS.enc.Latin1); + }; - function decryptAESCBC(ciphertext, key, iv) { - assertIsArrayBuffer(ciphertext); - assertIsArrayBuffer(key); - assertIsArrayBuffer(iv); - return CryptoJS.AES.decrypt(btoa(getString(ciphertext)), - CryptoJS.enc.Latin1.parse(getString(key)), - {iv: CryptoJS.enc.Latin1.parse(getString(iv))}) - .toString(CryptoJS.enc.Latin1); - }; + function decryptAESCBC(ciphertext, key, iv) { + assertIsArrayBuffer(ciphertext); + assertIsArrayBuffer(key); + assertIsArrayBuffer(iv); + return CryptoJS.AES.decrypt( + btoa(getString(ciphertext)), + CryptoJS.enc.Latin1.parse(getString(key)), + { iv: CryptoJS.enc.Latin1.parse(getString(iv)) } + ).toString(CryptoJS.enc.Latin1); + }; - // utility function for connecting front and back ends via promises - // Takes an implementation function and 0 or more arguments - function promise(implementation) { - var args = Array.prototype.slice.call(arguments); - args.shift(); - return Promise.resolve(toArrayBuffer(implementation.apply(this, args))); - } + // utility function for connecting front and back ends via promises + // Takes an implementation function and 0 or more arguments + function promise(implementation) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + return new Promise(function(resolve) { + resolve(toArrayBuffer(implementation.apply(this, args))); + }); + }; - // public interface functions - function encrypt(algorithm, key, data) { - if (algorithm.name === "AES-CTR") - return promise(encryptAESCTR, data, key, algorithm.counter); - if (algorithm.name === "AES-CBC") - return promise(encryptAESCBC, data, key, algorithm.iv.buffer || algorithm.iv); + return { + encrypt: function(algorithm, key, data) { + if (algorithm.name === "AES-CBC") + return promise(encryptAESCBC, data, key, algorithm.iv.buffer || algorithm.iv); + }, + + decrypt: function(algorithm, key, data) { + if (algorithm.name === "AES-CBC") + return promise(decryptAESCBC, data, key, algorithm.iv.buffer || algorithm.iv); + }, + + sign: function(algorithm, key, data) { + if (algorithm.name === "HMAC" && algorithm.hash === "SHA-256") + return promise(HmacSHA256, key, data); + }, + + importKey: function(format, key, algorithm, extractable, usages) { + return new Promise(function(resolve,reject){ resolve(key); }); + } + }; + })(); + } // if !window.crypto.subtle + + window.textsecure.subtle = { + encrypt: function(key, data, iv) { + return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { + return window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); + }); + }, + decrypt: function(key, data, iv) { + return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) { + return window.crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); + }); + }, + sign: function(key, data) { + return window.crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { + return window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data); + }); + }, }; - function decrypt(algorithm, key, data) { - if (algorithm.name === "AES-CTR") - return promise(decryptAESCTR, data, key, algorithm.counter); - if (algorithm.name === "AES-CBC") - return promise(decryptAESCBC, data, key, algorithm.iv.buffer || algorithm.iv); - }; - function sign(algorithm, key, data) { - if (algorithm.name === "HMAC" && algorithm.hash === "SHA-256") - return promise(HmacSHA256, key, data); - }; - - function importKey(format, key, algorithm, extractable, usages) { - return new Promise(function(resolve,reject){ resolve(key); }); - }; - - return { - encrypt : encrypt, - decrypt : decrypt, - sign : sign, - importKey : importKey, - } })();