last(?) round of crypto.js AB-type-conversion changes, new tests
This commit is contained in:
parent
f46d8eaaad
commit
a7de5e2159
2 changed files with 158 additions and 94 deletions
145
js/crypto.js
145
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) {
|
||||
if (version === undefined)
|
||||
version = 1;
|
||||
|
||||
return HmacSHA256(key, toArrayBuffer(String.fromCharCode(version) + getString(data))).then(function(calculated_mac) {
|
||||
var macString = getString(mac);
|
||||
|
||||
if (calculated_mac.substring(0, macString.length) != macString)
|
||||
throw new Error("Bad MAC");
|
||||
});
|
||||
}
|
||||
|
||||
var calculateMACWithVersionByte = function(data, key, version) {
|
||||
if (version === undefined)
|
||||
version = 1;
|
||||
|
||||
return HmacSHA256(key, toArrayBuffer(String.fromCharCode(version) + getString(data)));
|
||||
var prependedData = new Uint8Array(data.byteLength + 1);
|
||||
prependedData[0] = version;
|
||||
prependedData.set(new Uint8Array(data), 1);
|
||||
|
||||
return HmacSHA256(key, prependedData.buffer);
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
107
js/test.js
107
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('<p style="color: red;">' + funcName + ' called back multiple times</p>');
|
||||
else if (result === true)
|
||||
else if (error !== undefined) {
|
||||
console.log(error.stack);
|
||||
testsdiv.append('<p style="color: red;">' + funcName + ' threw ' + error + '</p>');
|
||||
} else if (result === true)
|
||||
testsdiv.append('<p style="color: green;">' + funcName + ' passed</p>');
|
||||
else
|
||||
testsdiv.append('<p style="color: red;">' + funcName + ' returned ' + result + '</p>');
|
||||
|
@ -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('<p style="color: red;">' + funcName + ' threw ' + e + '</p>');
|
||||
});
|
||||
func().then(resolve).catch(function(e) { resolve(null, e); });
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
testsdiv.append('<p style="color: red;">' + funcName + ' threw ' + e + '</p>');
|
||||
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');
|
||||
|
|
Loading…
Add table
Reference in a new issue