diff --git a/js/crypto.js b/js/crypto.js index cf85578b8..8fce9d1df 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -28,8 +28,7 @@ window.crypto = (function() { var origPub = new Uint8Array(pubKey); var pub = new ArrayBuffer(33); var pubWithPrefix = new Uint8Array(pub); - for (var i = 0; i < 32; i++) - pubWithPrefix[i+1] = origPub[i]; + pubWithPrefix.set(origPub, 1); pubWithPrefix[0] = 5; return pub; } @@ -113,32 +112,19 @@ window.crypto = (function() { //TODO: Think about replacing CryptoJS stuff with optional NaCL-based implementations // Probably means all of the low-level crypto stuff here needs pulled out into its own file crypto_tests.ECDHE = function(pubKey, privKey) { - if (privKey !== undefined) { - privKey = toArrayBuffer(privKey); - if (privKey.byteLength != 32) - throw new Error("Invalid private key"); - } else + if (privKey === undefined || privKey.byteLength != 32) throw new Error("Invalid private key"); - if (pubKey !== undefined) { - pubKey = toArrayBuffer(pubKey); - var pubView = new Uint8Array(pubKey); - if (pubKey.byteLength == 33 && pubView[0] == 5) { - pubKey = new ArrayBuffer(32); - var pubCopy = new Uint8Array(pubKey); - for (var i = 0; i < 32; i++) - pubCopy[i] = pubView[i+1]; - } else if (pubKey.byteLength != 32) - throw new Error("Invalid public key"); - } + if (pubKey === undefined || pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) + throw new Error("Invalid public key"); return new Promise(function(resolve) { if (USE_NACL) { - postNaclMessage({command: "ECDHE", priv: privKey, pub: pubKey}).then(function(message) { + postNaclMessage({command: "ECDHE", priv: privKey, pub: pubKey.slice(1)}).then(function(message) { resolve(message.res); }); } else { - resolve(toArrayBuffer(curve25519(new Uint16Array(privKey), new Uint16Array(pubKey)))); + resolve(toArrayBuffer(curve25519(new Uint16Array(privKey), new Uint16Array(pubKey.slice(1))))); } }); } @@ -146,11 +132,16 @@ window.crypto = (function() { crypto_tests.HKDF = function(input, salt, info) { // Specific implementation of RFC 5869 that only returns exactly 64 bytes - return HmacSHA256(salt, toArrayBuffer(input)).then(function(PRK) { - var infoString = getString(info); + return HmacSHA256(salt, input).then(function(PRK) { + var infoBuffer = new ArrayBuffer(info.byteLength + 1 + 32); + var infoArray = new Uint8Array(infoBuffer); + infoArray.set(new Uint8Array(info), 32); + infoArray[infoArray.length - 1] = 0; // TextSecure implements a slightly tweaked version of RFC 5869: the 0 and 1 should be 1 and 2 here - return HmacSHA256(PRK, toArrayBuffer(infoString + String.fromCharCode(0))).then(function(T1) { - return HmacSHA256(PRK, toArrayBuffer(getString(T1) + infoString + String.fromCharCode(1))).then(function(T2) { + return HmacSHA256(PRK, infoBuffer.slice(32)).then(function(T1) { + infoArray.set(new Uint8Array(T1)); + infoArray[infoArray.length - 1] = 1; + return HmacSHA256(PRK, infoBuffer).then(function(T2) { return [ T1, T2 ]; }); }); @@ -159,38 +150,34 @@ window.crypto = (function() { var HKDF = function(input, salt, info) { // HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes - if (salt == '') { + if (salt == '') salt = new ArrayBuffer(32); - var uintKey = new Uint8Array(salt); - for (var i = 0; i < 32; i++) - uintKey[i] = 0; - } - - salt = toArrayBuffer(salt); - if (salt.byteLength != 32) throw new Error("Got salt of incorrect length"); + info = toArrayBuffer(info); // TODO: maybe convert calls? + return crypto_tests.HKDF(input, salt, info); } - var verifyMACWithVersionByte = function(data, key, mac, version) { + var calculateMACWithVersionByte = function(data, key, version) { if (version === undefined) version = 1; - return HmacSHA256(key, toArrayBuffer(String.fromCharCode(version) + getString(data))).then(function(calculated_mac) { - var macString = getString(mac); + var prependedData = new Uint8Array(data.byteLength + 1); + prependedData[0] = version; + prependedData.set(new Uint8Array(data), 1); - if (calculated_mac.substring(0, macString.length) != macString) - throw new Error("Bad MAC"); - }); + return HmacSHA256(key, prependedData.buffer); } - var calculateMACWithVersionByte = function(data, key, version) { - if (version === undefined) - version = 1; + var verifyMACWithVersionByte = function(data, key, mac, version) { + return calculateMACWithVersionByte(data, key, version).then(function(calculated_mac) { + var macString = getString(mac);//TODO: Move away from strings for comparison? - return HmacSHA256(key, toArrayBuffer(String.fromCharCode(version) + getString(data))); + if (getString(calculated_mac).substring(0, macString.length) != macString) + throw new Error("Bad MAC"); + }); } /****************************** @@ -199,8 +186,8 @@ window.crypto = (function() { var calculateRatchet = function(session, remoteKey, sending) { var ratchet = session.currentRatchet; - return ECDHE(remoteKey, ratchet.ephemeralKeyPair.privKey).then(function(sharedSecret) { - return HKDF(sharedSecret, ratchet.rootKey, "WhisperRatchet").then(function(masterKey) { + return ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) { + return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) { if (sending) session[getString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } }; else @@ -213,15 +200,13 @@ window.crypto = (function() { var initSession = function(isInitiator, ourEphemeralKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey) { var ourIdentityPrivKey = crypto_storage.getIdentityPrivKey(); - var sharedSecret; - return ECDHE(theirEphemeralPubKey, ourIdentityPrivKey).then(function(ecRes) { - sharedSecret = getString(ecRes); - + var sharedSecret = new Uint8Array(32 * 3); + return ECDHE(theirEphemeralPubKey, ourIdentityPrivKey).then(function(ecRes1) { function finishInit() { return ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey).then(function(ecRes) { - sharedSecret += getString(ecRes); + sharedSecret.set(new Uint8Array(ecRes), 32 * 2); - return HKDF(toArrayBuffer(sharedSecret), '', "WhisperText").then(function(masterKey) { + return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) { var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirEphemeralPubKey }, oldRatchetList: [] }; @@ -244,12 +229,14 @@ window.crypto = (function() { } if (isInitiator) - return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes) { - sharedSecret = sharedSecret + getString(ecRes); + return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) { + sharedSecret.set(new Uint8Array(ecRes1)); + sharedSecret.set(new Uint8Array(ecRes2), 32); }).then(finishInit); else - return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes) { - sharedSecret = getString(ecRes) + sharedSecret; + return ECDHE(theirIdentityPubKey, ourEphemeralKey.privKey).then(function(ecRes2) { + sharedSecret.set(new Uint8Array(ecRes1), 32); + sharedSecret.set(new Uint8Array(ecRes2)) }).then(finishInit); }); } @@ -264,7 +251,7 @@ window.crypto = (function() { else throw new Error("Missing preKey for PreKeyWhisperMessage"); } else - return initSession(false, preKeyPair, encodedNumber, message.identityKey, message.baseKey); + return initSession(false, preKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey)); } var fillMessageKeys = function(chain, counter) { @@ -273,8 +260,11 @@ window.crypto = (function() { if (chain.chainKey.counter < counter) { var key = toArrayBuffer(chain.chainKey.key); - return HmacSHA256(key, toArrayBuffer(String.fromCharCode(1))).then(function(mac) { - return HmacSHA256(key, toArrayBuffer(String.fromCharCode(2))).then(function(key) { + var byteArray = new Uint8Array(1); + byteArray[0] = 1; + return HmacSHA256(key, byteArray.buffer).then(function(mac) { + byteArray[0] = 2; + return HmacSHA256(key, byteArray.buffer).then(function(key) { chain.messageKeys[chain.chainKey.counter + 1] = mac; chain.chainKey.key = key chain.chainKey.counter += 1; @@ -338,23 +328,24 @@ window.crypto = (function() { var message = decodeWhisperMessageProtobuf(messageProto); - return maybeStepRatchet(session, message.ephemeralKey, message.previousCounter).then(function() { + return maybeStepRatchet(session, toArrayBuffer(message.ephemeralKey), message.previousCounter).then(function() { var chain = session[getString(message.ephemeralKey)]; return fillMessageKeys(chain, message.counter).then(function() { - return HKDF(chain.messageKeys[message.counter], '', "WhisperMessageKeys").then(function(keys) { + return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) { delete chain.messageKeys[message.counter]; - verifyMACWithVersionByte(messageProto, keys[1], mac, (2 << 4) | 2); - var counter = intToArrayBuffer(message.counter); - return window.crypto.subtle.decrypt({name: "AES-CTR", counter: counter}, keys[0], toArrayBuffer(message.ciphertext)) - .then(function(plaintext) { + return verifyMACWithVersionByte(toArrayBuffer(messageProto), keys[1], mac, (2 << 4) | 2).then(function() { + var counter = intToArrayBuffer(message.counter); + return window.crypto.subtle.decrypt({name: "AES-CTR", counter: counter}, keys[0], toArrayBuffer(message.ciphertext)) + .then(function(plaintext) { - //TODO: removeOldChains(session); - delete session['pendingPreKey']; + //TODO: removeOldChains(session); + delete session['pendingPreKey']; - crypto_storage.saveSession(encodedNumber, session); - return decodePushMessageContentProtobuf(getString(plaintext)); + crypto_storage.saveSession(encodedNumber, session); + return decodePushMessageContentProtobuf(getString(plaintext)); + }); }); }); }); @@ -416,7 +407,7 @@ window.crypto = (function() { var chain = session[getString(msg.ephemeralKey)]; return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() { - return HKDF(chain.messageKeys[chain.chainKey.counter], '', "WhisperMessageKeys").then(function(keys) { + return HKDF(toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) { delete chain.messageKeys[chain.chainKey.counter]; msg.counter = chain.chainKey.counter; msg.previousCounter = session.currentRatchet.previousCounter; @@ -424,10 +415,13 @@ window.crypto = (function() { var counter = intToArrayBuffer(chain.chainKey.counter); return window.crypto.subtle.encrypt({name: "AES-CTR", counter: counter}, keys[0], plaintext).then(function(ciphertext) { msg.ciphertext = ciphertext; - var encodedMsg = getString(msg.encode()); + var encodedMsg = toArrayBuffer(msg.encode()); return calculateMACWithVersionByte(encodedMsg, keys[1], (2 << 4) | 2).then(function(mac) { - var result = String.fromCharCode((2 << 4) | 2) + encodedMsg + getString(mac).substring(0, 8); + var result = new Uint8Array(encodedMsg.byteLength + 9); + result[0] = (2 << 4) | 2; + result.set(new Uint8Array(encodedMsg), 1); + result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); crypto_storage.saveSession(deviceObject.encodedNumber, session); return result; }); @@ -449,7 +443,7 @@ window.crypto = (function() { session = crypto_storage.getSession(deviceObject.encodedNumber); session.pendingPreKey = baseKey.pubKey; return doEncryptPushMessageContent().then(function(message) { - preKeyMsg.message = toArrayBuffer(message); + preKeyMsg.message = message; var result = String.fromCharCode((2 << 4) | 2) + getString(preKeyMsg.encode()); return {type: 3, body: result}; }); @@ -459,7 +453,7 @@ window.crypto = (function() { return doEncryptPushMessageContent().then(function(message) { if (session.pendingPreKey !== undefined) { preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey); - preKeyMsg.message = toArrayBuffer(message); + preKeyMsg.message = message; var result = String.fromCharCode((2 << 4) | 2) + getString(preKeyMsg.encode()); return {type: 3, body: result}; } else @@ -508,4 +502,3 @@ window.crypto = (function() { return identityKeyCalculated(identityKey); } })(); - diff --git a/js/test.js b/js/test.js index 90d21c8f2..1491b5cc0 100644 --- a/js/test.js +++ b/js/test.js @@ -44,10 +44,13 @@ function TEST(func, name, exclusive) { maxTestId = maxTestId + 1; - function resolve(result) { + function resolve(result, error) { if (testsOutstanding[testIndex] == undefined) testsdiv.append('
' + funcName + ' called back multiple times
'); - else if (result === true) + else if (error !== undefined) { + console.log(error.stack); + testsdiv.append('' + funcName + ' threw ' + error + '
'); + } else if (result === true) testsdiv.append('' + funcName + ' passed
'); else testsdiv.append('' + funcName + ' returned ' + result + '
'); @@ -71,13 +74,9 @@ function TEST(func, name, exclusive) { try { testsOutstanding[testIndex] = funcName; - func().then(resolve).catch(function(e) { - console.log(e.stack); - testsdiv.append('' + funcName + ' threw ' + e + '
'); - }); + func().then(resolve).catch(function(e) { resolve(null, e); }); } catch (e) { - console.log(e.stack); - testsdiv.append('' + funcName + ' threw ' + e + '
'); + resolve(null, e); } } @@ -426,18 +425,90 @@ registerOnLoadFunction(function() { }, "Standard Axolotl Test Vectors as Bob", true); TEST(function() { - var v0 = axolotlTwoPartyTestVectorsBob[0][1]; - var v1 = axolotlTwoPartyTestVectorsBob[1][1]; + // Copy axolotlTwoPartyTestVectorsBob into v + var orig = axolotlTwoPartyTestVectorsBob; + var v = []; + for (var i = 0; i < axolotlTwoPartyTestVectorsBob.length; i++) { + v[i] = []; + v[i][0] = orig[i][0]; + v[i][1] = orig[i][1]; + } - axolotlTwoPartyTestVectorsBob[0][1] = v1; - axolotlTwoPartyTestVectorsBob[0][1].ourPreKey = v0.ourPreKey; - axolotlTwoPartyTestVectorsBob[0][1].preKeyId = v0.preKeyId; - axolotlTwoPartyTestVectorsBob[0][1].ourIdentityKey = v0.ourIdentityKey; - axolotlTwoPartyTestVectorsBob[0][1].newEphemeralKey = v0.newEphemeralKey; + // Swap first and second received prekey messages + v[0][1] = { message: orig[1][1].message, type: orig[1][1].type, expectedSmsText: orig[1][1].expectedSmsText }; + v[0][1].ourPreKey = orig[0][1].ourPreKey; + v[0][1].preKeyId = orig[0][1].preKeyId; + v[0][1].ourIdentityKey = orig[0][1].ourIdentityKey; + v[0][1].newEphemeralKey = orig[0][1].newEphemeralKey; - axolotlTwoPartyTestVectorsBob[1][1] = { message: v0.message, type: v0.type, expectedSmsText: v0.expectedSmsText }; - return axolotlTestVectors(axolotlTwoPartyTestVectorsBob, { encodedNumber: "ALICE" }); - }, "Shuffled Axolotl Test Vectors as Bob", true); + v[1][1] = { message: orig[0][1].message, type: orig[0][1].type, expectedSmsText: orig[0][1].expectedSmsText }; + return axolotlTestVectors(v, { encodedNumber: "ALICE" }); + }, "Shuffled Axolotl Test Vectors as Bob I", true); + + TEST(function() { + // Copy axolotlTwoPartyTestVectorsBob into v + var orig = axolotlTwoPartyTestVectorsBob; + var v = []; + for (var i = 0; i < axolotlTwoPartyTestVectorsBob.length; i++) { + v[i] = []; + v[i][0] = orig[i][0]; + v[i][1] = orig[i][1]; + } + + // Swap second received prekey msg with the first send + v[1] = orig[2]; + v[2] = orig[1]; + + return axolotlTestVectors(v, { encodedNumber: "ALICE" }); + }, "Shuffled Axolotl Test Vectors as Bob II", true); + + TEST(function() { + // Copy axolotlTwoPartyTestVectorsBob into v + var orig = axolotlTwoPartyTestVectorsBob; + var v = []; + for (var i = 0; i < axolotlTwoPartyTestVectorsBob.length; i++) { + v[i] = []; + v[i][0] = orig[i][0]; + v[i][1] = orig[i][1]; + } + + // Move second received prekey msg to the end (incl after the first received message in the second chain) + v[4] = orig[1]; + v[1] = orig[2]; + v[2] = orig[3]; + v[3] = orig[4]; + + return axolotlTestVectors(v, { encodedNumber: "ALICE" }); + }, "Shuffled Axolotl Test Vectors as Bob III", true); + + TEST(function() { + // Copy axolotlTwoPartyTestVectorsBob into v + var orig = axolotlTwoPartyTestVectorsBob; + var v = []; + for (var i = 0; i < axolotlTwoPartyTestVectorsBob.length; i++) { + v[i] = []; + v[i][0] = orig[i][0]; + v[i][1] = orig[i][1]; + } + + // Move first received prekey msg to the end (incl after the first received message in the second chain) + // ... by first swapping first and second received prekey msg + v[0][1] = { message: orig[1][1].message, type: orig[1][1].type, expectedSmsText: orig[1][1].expectedSmsText }; + v[0][1].ourPreKey = orig[0][1].ourPreKey; + v[0][1].preKeyId = orig[0][1].preKeyId; + v[0][1].ourIdentityKey = orig[0][1].ourIdentityKey; + v[0][1].newEphemeralKey = orig[0][1].newEphemeralKey; + + v[1][1] = { message: orig[0][1].message, type: orig[0][1].type, expectedSmsText: orig[0][1].expectedSmsText }; + + // ... then moving the (now-second) message to the end + v[4] = v[1]; + v[1] = orig[2]; + v[2] = orig[3]; + v[3] = orig[4]; + + return axolotlTestVectors(v, { encodedNumber: "ALICE" }); + }, "Shuffled Axolotl Test Vectors as Bob IV", true); TEST(function() { var key = hexToArrayBuffer('6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245');