Updates, NaCL

This commit is contained in:
Matt Corallo 2014-01-22 06:23:41 +00:00
parent eec4c66ef6
commit 8db3885659
14 changed files with 1128 additions and 174 deletions

View file

@ -1,14 +1,16 @@
if (!localStorage.getItem('first_install_ran')) {
localStorage.setItem('first_install_ran', 1);
chrome.tabs.create({url: "options.html"});
} else {
if (isRegistrationDone()) {
subscribeToPush(function(message) {
console.log("Got message from " + message.source + ": \"" + getString(message.message));
var newUnreadCount = storage.getUnencrypted("unreadCount") + 1;
storage.putUnencrypted("unreadCount", newUnreadCount);
chrome.browserAction.setBadgeText({text: newUnreadCount + ""});
storeMessage(message);
});
registerOnLoadFunction(function() {
if (!localStorage.getItem('first_install_ran')) {
localStorage.setItem('first_install_ran', 1);
chrome.tabs.create({url: "options.html"});
} else {
if (isRegistrationDone()) {
subscribeToPush(function(message) {
console.log("Got message from " + message.source + ": \"" + getString(message.message));
var newUnreadCount = storage.getUnencrypted("unreadCount") + 1;
storage.putUnencrypted("unreadCount", newUnreadCount);
chrome.browserAction.setBadgeText({text: newUnreadCount + ""});
storeMessage(message);
});
}
}
}
});

View file

@ -19,7 +19,9 @@ function b64ToUint6 (nChr) {
function base64DecToArr (sBase64, nBlocksSize) {
var
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);
nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2;
var aBBytes = new ArrayBuffer(nOutLen);
var taBytes = new Uint8Array(aBBytes);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
@ -31,7 +33,7 @@ function base64DecToArr (sBase64, nBlocksSize) {
nUint24 = 0;
}
}
return taBytes;
return aBBytes;
}
/*********************************
@ -39,20 +41,20 @@ function base64DecToArr (sBase64, nBlocksSize) {
*********************************/
// Strings/arrays
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
var StaticUint8ArrayProto = new Uint8Array().__proto__;
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
function getString(thing) {
if (thing.__proto__ == StaticUint8ArrayProto)
if (thing.__proto__ == StaticArrayBufferProto)
return String.fromCharCode.apply(null, thing);
if (thing != undefined && thing.__proto__ == StaticByteBufferProto)
return thing.toString("utf8");
return thing;
}
function getUint8Array(string) {
function getArrayBuffer(string) {
return base64DecToArr(btoa(string));
}
function base64ToUint8Array(string) {
function base64ToArrayBuffer(string) {
return base64DecToArr(string);
}
@ -238,15 +240,49 @@ function getDeviceObjectListFromNumber(number) {
return deviceObjectList;
}
/**********************
*** NaCL Interface ***
**********************/
var onLoadCallbacks = [];
var naclLoaded = 0;
function registerOnLoadFunction(func) {
if (naclLoaded)
func();
onLoadCallbacks[onLoadCallbacks.length] = func;
}
var naclMessageNextId = 0;
var naclMessageIdCallbackMap = {};
function moduleDidLoad() {
common.hideModule();
naclLoaded = 1;
for (var i = 0; i < onLoadCallbacks.length; i++)
onLoadCallbacks[i]();
onLoadCallbacks = [];
}
function handleMessage(message) {
console.log("Got message");
console.log(message);
naclMessageIdCallbackMap[message.data.call_id](message.data);
}
function postNaclMessage(message, callback) {
naclMessageIdCallbackMap[naclMessageNextId] = callback;
message.call_id = naclMessageNextId++;
common.naclModule.postMessage(message);
}
/*******************************************
*** Utilities to manage keys/randomness ***
*******************************************/
function getRandomBytes(size) {
//TODO: Better random (https://www.grc.com/r&d/js.htm?)
try {
var array = new Uint8Array(size);
var buffer = new ArrayBuffer(size);
var array = new Uint8Array(buffer);
window.crypto.getRandomValues(array);
return array;
return buffer;
} catch (err) {
//TODO: ummm...wat?
throw err;
@ -254,23 +290,26 @@ function getRandomBytes(size) {
}
(function(crypto, $, undefined) {
var createNewKeyPair = function() {
var createNewKeyPair = function(callback) {
//TODO
var privKey = getRandomBytes(32);
privKey[0] &= 248;
privKey[31] &= 127;
privKey[31] |= 64;
var pubKey = "BRTJzsHPUWRRBxyo5MoaBRidMk2fwDlfqvU91b6pzbED";
var privKey = "";
return { pubKey: pubKey, privKey: privKey };
postNaclMessage({command: "bytesToPriv", priv: privKey}, function(message) {
postNaclMessage({command: "privToPub", priv: message.res}, function(message) {
callback({ pubKey: message.res, privKey: privKey });
});
});
}
var crypto_storage = {};
crypto_storage.getNewPubKeySTORINGPrivKey = function(keyName) {
var keyPair = createNewKeyPair();
storage.putEncrypted("25519Key" + keyName, keyPair);
return keyPair.pubKey;
crypto_storage.getNewPubKeySTORINGPrivKey = function(keyName, callback) {
createNewKeyPair(function(keyPair) {
storage.putEncrypted("25519Key" + keyName, keyPair);
callback(keyPair.pubKey);
});
}
crypto_storage.getStoredPubKey = function(keyName) {
@ -379,7 +418,7 @@ function getRandomBytes(size) {
chain.chainKey.counter = counter;
}
var maybeStepRatchet = function(session, remoteKey, previousCounter) {
var maybeStepRatchet = function(session, remoteKey, previousCounter, callback) {
if (sesion[remoteKey] !== undefined) //TODO: null???
return;
@ -397,12 +436,16 @@ function getRandomBytes(size) {
var masterKey = HKDF(ECDHE(remoteKey, ratchet.ephemeralKeyPair.privKey), ratchet.rootKey, "WhisperRatchet");
session[remoteKey] = { messageKeys: {}, chainKey: { counter: 0, key: masterKey.substring(32, 64) } };
ratchet.ephemeralKeyPair = createNewKeyPair();
masterKey = HKDF(ECDHE(remoteKey, ratchet.ephemeralKeyPair.privKey), masterKey.substring(0, 32), "WhisperRatchet");
ratchet.rootKey = masterKey.substring(0, 32);
session[nextRatchet.ephemeralKeyPair.pubKey] = { messageKeys: {}, chainKey: { counter: 0, key: masterKey.substring(32, 64) } };
createNewKeyPair(function(keyPair) {
ratchet.ephemeralKeyPair = keyPair;
ratchet.lastRemoteEphemeralKey = remoteKey;
masterKey = HKDF(ECDHE(remoteKey, ratchet.ephemeralKeyPair.privKey), masterKey.substring(0, 32), "WhisperRatchet");
ratchet.rootKey = masterKey.substring(0, 32);
session[nextRatchet.ephemeralKeyPair.pubKey] = { messageKeys: {}, chainKey: { counter: 0, key: masterKey.substring(32, 64) } };
ratchet.lastRemoteEphemeralKey = remoteKey;
callback();
});
}
var doDecryptWhisperMessage = function(ciphertext, mac, messageKey, counter) {
@ -414,7 +457,7 @@ function getRandomBytes(size) {
}
// returns decrypted protobuf
var decryptWhisperMessage = function(encodedNumber, messageBytes) {
var decryptWhisperMessage = function(encodedNumber, messageBytes, callback) {
var session = crypto_storage.getSession(encodedNumber);
if (session === undefined)
throw "No session currently open with " + encodedNumber;
@ -427,18 +470,19 @@ function getRandomBytes(size) {
var message = decodeWhisperMessageProtobuf(messageProto);
maybeStepRatchet(session, getString(message.ephemeralKey), message.previousCounter);
var chain = session[getString(message.ephemeralKey)];
maybeStepRatchet(session, getString(message.ephemeralKey), message.previousCounter, function() {
var chain = session[getString(message.ephemeralKey)];
fillMessageKeys(chain, message.counter);
fillMessageKeys(chain, message.counter);
var plaintext = doDecryptWhisperMessage(message.ciphertext, mac, chain.messageKeys[message.counter], message.counter);
delete chain.messageKeys[message.counter];
var plaintext = doDecryptWhisperMessage(message.ciphertext, mac, chain.messageKeys[message.counter], message.counter);
delete chain.messageKeys[message.counter];
removeOldChains(session);
removeOldChains(session);
crypto_storage.saveSession(encodedNumber, session);
return decodePushMessageContentProtobuf(atob(plaintext));
crypto_storage.saveSession(encodedNumber, session);
callback(decodePushMessageContentProtobuf(atob(plaintext)));
});
}
/*************************
@ -477,18 +521,18 @@ function getRandomBytes(size) {
return atob(plaintext.toString(CryptoJS.enc.Base64));
}
crypto.handleIncomingPushMessageProto = function(proto) {
crypto.handleIncomingPushMessageProto = function(proto, callback) {
switch(proto.type) {
case 0: //TYPE_MESSAGE_PLAINTEXT
proto.message = decodePushMessageContentProtobuf(getString(proto.message));
callback(decodePushMessageContentProtobuf(getString(proto.message)));
break;
case 1: //TYPE_MESSAGE_CIPHERTEXT
proto.message = decryptWhisperMessage(proto.source, getString(proto.message));
decryptWhisperMessage(proto.source, getString(proto.message), function(result) { callback(result); });
break;
case 3: //TYPE_MESSAGE_PREKEY_BUNDLE
var preKeyProto = decodePreKeyWhisperMessageProtobuf(getString(proto.message));
initSessionFromPreKeyWhisperMessage(proto.source, preKeyProto);
proto.message = decryptWhisperMessage(proto.source, getString(preKeyProto.message));
decryptWhisperMessage(proto.source, getString(preKeyProto.message), function(result) { callback(result); });
break;
}
}
@ -499,26 +543,42 @@ function getRandomBytes(size) {
}
var GENERATE_KEYS_KEYS_GENERATED = 100;
crypto.generateKeys = function() {
crypto.generateKeys = function(callback) {
var identityKey = crypto_storage.getStoredPubKey("identityKey");
var identityKeyCalculated = function(pubKey) {
identityKey = pubKey;
var firstKeyId = storage.getEncrypted("maxPreKeyId", -1) + 1;
storage.putEncrypted("maxPreKeyId", firstKeyId + GENERATE_KEYS_KEYS_GENERATED);
if (firstKeyId > 16777000)
throw "You crazy motherfucker";
var keys = {};
keys.keys = [];
var keysLeft = GENERATE_KEYS_KEYS_GENERATED;
for (var i = firstKeyId; i < firstKeyId + GENERATE_KEYS_KEYS_GENERATED; i++) {
crypto_storage.getNewPubKeySTORINGPrivKey("preKey" + i, function(pubKey) {
keys.keys[i] = {keyId: i, publicKey: pubKey, identityKey: identityKey};
keysLeft--;
if (keysLeft == 0) {
// 0xFFFFFF == 16777215
keys.lastResortKey = {keyId: 16777215, publicKey: crypto_storage.getStoredPubKey("preKey16777215"), identityKey: identityKey};//TODO: Rotate lastResortKey
if (keys.lastResortKey.publicKey === undefined) {
crypto_storage.getNewPubKeySTORINGPrivKey("preKey16777215", function(pubKey) {
keys.lastResortKey.publicKey = pubKey;
callback(keys);
});
} else
callback(keys);
}
});
}
}
if (identityKey === undefined)
identityKey = crypto_storage.getNewPubKeySTORINGPrivKey("identityKey"); //TODO: should probably just throw?
var firstKeyId = storage.getEncrypted("maxPreKeyId", -1) + 1;
storage.putEncrypted("maxPreKeyId", firstKeyId + GENERATE_KEYS_KEYS_GENERATED);
if (firstKeyId > 16777000)
throw "You crazy motherfucker";
var keys = {};
keys.keys = [];
for (var i = firstKeyId; i < firstKeyId + GENERATE_KEYS_KEYS_GENERATED; i++)
keys.keys[i] = {keyId: i, publicKey: crypto_storage.getNewPubKeySTORINGPrivKey("preKey" + i), identityKey: identityKey};
// 0xFFFFFF == 16777215
keys.lastResortKey = {keyId: 16777215, publicKey: crypto_storage.getStoredPubKey("preKey16777215"), identityKey: identityKey};//TODO: Rotate lastResortKey
if (keys.lastResortKey.publicKey === undefined)
keys.lastResortKey.publicKey = crypto_storage.getNewPubKeySTORINGPrivKey("preKey16777215");
return keys;
crypto_storage.getNewPubKeySTORINGPrivKey("identityKey", function(pubKey) { identityKeyCalculated(pubKey); });
else
identityKeyCalculated(pubKey);
}
}( window.crypto = window.crypto || {}, jQuery ));
@ -621,9 +681,9 @@ function subscribeToPush(message_callback) {
}
try {
crypto.handleIncomingPushMessageProto(proto); // Decrypts/decodes/fills in fields/etc
message_callback(proto);
crypto.handleIncomingPushMessageProto(proto, function(decrypted) {
message_callback(decrypted);
}); // Decrypts/decodes/fills in fields/etc
} catch (e) {
//TODO: Tell the user decryption failed
}
@ -739,8 +799,8 @@ function sendMessageToNumbers(numbers, message, success_callback, error_callback
}, error_callback);
}
function requestIdentityPrivKeyFromMasterDevice(number, identityKey) {
sendMessageToDevices([getDeviceObject(getNumberFromString(number)) + ".1"],
{message: "Identity Key request"}, function() {}, function() {});//TODO
}

View file

@ -74,17 +74,18 @@ $('#init-go').click(function() {
var register_keys_func = function() {
$('#verify2done').html('done');
var keys = crypto.generateKeys();
$('#verify3done').html('done');
doAjax({call: 'keys', httpType: 'PUT', do_auth: true, jsonData: keys,
success_callback: function(response) {
$('#complete-number').html(number);
$('#verify').hide();
$('#setup-complete').show();
registrationDone();
}, error_callback: function(code) {
alert(code); //TODO
}
crypto.generateKeys(function(keys) {
$('#verify3done').html('done');
doAjax({call: 'keys', httpType: 'PUT', do_auth: true, jsonData: keys,
success_callback: function(response) {
$('#complete-number').html(number);
$('#verify').hide();
$('#setup-complete').show();
registrationDone();
}, error_callback: function(code) {
alert(code); //TODO
}
});
});
}
@ -120,9 +121,11 @@ $('#init-go').click(function() {
}
});
if (!isRegistrationDone()) {
$('#init-setup').show();
} else {
$('#complete-number').html(storage.getUnencrypted("number_id").split(".")[0]);
$('#setup-complete').show();
}
registerOnLoadFunction(function() {
if (!isRegistrationDone()) {
$('#init-setup').show();
} else {
$('#complete-number').html(storage.getUnencrypted("number_id").split(".")[0]);
$('#setup-complete').show();
}
});

View file

@ -7,55 +7,57 @@ $('#send_link').onclick = function() {
$('#send').show();
}
if (storage.getUnencrypted("number_id") === undefined) {
chrome.tabs.create({url: "options.html"});
} else {
function fillMessages() {
var MAX_MESSAGES_PER_CONVERSATION = 4;
var MAX_CONVERSATIONS = 5;
registerOnLoadFunction(function() {
if (storage.getUnencrypted("number_id") === undefined) {
chrome.tabs.create({url: "options.html"});
} else {
function fillMessages() {
var MAX_MESSAGES_PER_CONVERSATION = 4;
var MAX_CONVERSATIONS = 5;
var conversations = [];
var conversations = [];
var messageMap = getMessageMap();
for (conversation in messageMap) {
var messages = messageMap[conversation];
messages.sort(function(a, b) { return b.timestamp - a.timestamp; });
conversations[conversations.length] = messages;
}
conversations.sort(function(a, b) { return b[0].timestamp - a[0].timestamp });
var ul = $('#messages');
ul.html('');
for (var i = 0; i < MAX_CONVERSATIONS && i < conversations.length; i++) {
var conversation = conversations[i];
ul.append('<li>');
for (var j = 0; j < MAX_MESSAGES_PER_CONVERSATION && j < conversation.length; j++) {
var message = conversation[j];
ul.append("From: " + message.sender + ", at: " + timestampToHumanReadable(message.timestamp) + "<br>");
ul.append("Message: " + message.message + "<br><br>");
var messageMap = getMessageMap();
for (conversation in messageMap) {
var messages = messageMap[conversation];
messages.sort(function(a, b) { return b.timestamp - a.timestamp; });
conversations[conversations.length] = messages;
}
ul.append("<input type='text' id=text" + i + " /><button id=button" + i + ">Send</button><br>");
$('#button' + i).click(function() {
var sendDestinations = [conversation[0].sender];
for (var j = 0; j < conversation[0].destinations.length; j++)
sendDestinations[sendDestinations.length] = conversation[0].destinations[j];
sendMessageToNumbers(sendDestinations, { message: $('#text' + i).val() }, function(result) {
console.log("Sent message: " + JSON.stringify(result));
}, function(error_msg) {
alert(error_msg); //TODO
});
});
ul.append('</li>');
}
}
$(window).bind('storage', function(e) {
console.log("Got localStorage update for key " + e.key);
if (event.key == "emessageMap")//TODO: Fix when we get actual encryption
fillMessages();
});
fillMessages();
storage.putUnencrypted("unreadCount", 0);
chrome.browserAction.setBadgeText({text: ""});
}
conversations.sort(function(a, b) { return b[0].timestamp - a[0].timestamp });
var ul = $('#messages');
ul.html('');
for (var i = 0; i < MAX_CONVERSATIONS && i < conversations.length; i++) {
var conversation = conversations[i];
ul.append('<li>');
for (var j = 0; j < MAX_MESSAGES_PER_CONVERSATION && j < conversation.length; j++) {
var message = conversation[j];
ul.append("From: " + message.sender + ", at: " + timestampToHumanReadable(message.timestamp) + "<br>");
ul.append("Message: " + message.message + "<br><br>");
}
ul.append("<input type='text' id=text" + i + " /><button id=button" + i + ">Send</button><br>");
$('#button' + i).click(function() {
var sendDestinations = [conversation[0].sender];
for (var j = 0; j < conversation[0].destinations.length; j++)
sendDestinations[sendDestinations.length] = conversation[0].destinations[j];
sendMessageToNumbers(sendDestinations, { message: $('#text' + i).val() }, function(result) {
console.log("Sent message: " + JSON.stringify(result));
}, function(error_msg) {
alert(error_msg); //TODO
});
});
ul.append('</li>');
}
}
$(window).bind('storage', function(e) {
console.log("Got localStorage update for key " + e.key);
if (event.key == "emessageMap")//TODO: Fix when we get actual encryption
fillMessages();
});
fillMessages();
storage.putUnencrypted("unreadCount", 0);
chrome.browserAction.setBadgeText({text: ""});
}
});

View file

@ -1,37 +1,62 @@
// Setup dumb test wrapper
var testsdiv = $('#tests');
var testsOutstanding = [];
function TEST(func, name) {
var funcName = name === undefined ? func + "" : name;
try {
if (func())
var testIndex = testsOutstanding.length;
function callback(result) {
if (result)
testsdiv.append('<p style="color: green;">' + funcName + ' passed</p>');
else
testsdiv.append('<p style="color: red;">' + funcName + ' returned false</p>');
delete testsOutstanding[testIndex];
}
try {
testsOutstanding[testIndex] = funcName;
func(callback);
} catch (e) {
testsdiv.append('<p style="color: red;">' + funcName + ' threw ' + e + '</p>');
}
}
// Random tests to check my JS knowledge
TEST(function() { return !objectContainsKeys({}); });
TEST(function() { return objectContainsKeys({ a: undefined }); });
TEST(function() { return objectContainsKeys({ a: null }); });
registerOnLoadFunction(function() {
localStorage.clear();
// Basic sanity-checks on the crypto library
TEST(function() {
var PushMessageProto = dcodeIO.ProtoBuf.loadProtoFile("IncomingPushMessageSignal.proto").build("textsecure.PushMessageContent");
var IncomingMessageProto = dcodeIO.ProtoBuf.loadProtoFile("IncomingPushMessageSignal.proto").build("textsecure.IncomingPushMessageSignal");
// Random tests to check my JS knowledge
TEST(function(callback) { callback(!objectContainsKeys({})); });
TEST(function(callback) { callback(objectContainsKeys({ a: undefined })); });
TEST(function(callback) { callback(objectContainsKeys({ a: null })); });
var text_message = new PushMessageProto();
text_message.body = "Hi Mom";
var server_message = {type: 0, // unencrypted
source: "+19999999999", timestamp: 42, message: text_message.encode() };
// Basic sanity-checks on the crypto library
TEST(function(callback) {
var PushMessageProto = dcodeIO.ProtoBuf.loadProtoFile("protos/IncomingPushMessageSignal.proto").build("textsecure.PushMessageContent");
var IncomingMessageProto = dcodeIO.ProtoBuf.loadProtoFile("protos/IncomingPushMessageSignal.proto").build("textsecure.IncomingPushMessageSignal");
crypto.handleIncomingPushMessageProto(server_message);
return server_message.message.body == text_message.body &&
server_message.message.attachments.length == text_message.attachments.length &&
text_message.attachments.length == 0;
}, 'Unencrypted PushMessageProto "decrypt"');
var text_message = new PushMessageProto();
text_message.body = "Hi Mom";
var server_message = {type: 0, // unencrypted
source: "+19999999999", timestamp: 42, message: text_message.encode() };
// TODO: Run through the test vectors for the axolotl ratchet
crypto.handleIncomingPushMessageProto(server_message, function(message) {
callback(message.body == text_message.body &&
message.attachments.length == text_message.attachments.length &&
text_message.attachments.length == 0);
});
}, 'Unencrypted PushMessageProto "decrypt"');
TEST(function(callback) {
crypto.generateKeys(function() {
callback(true);
});
}, "Test simple create key");
// TODO: Run through the test vectors for the axolotl ratchet
window.setTimeout(function() {
for (var i = 0; i < testsOutstanding.length; i++)
if (testsOutstanding[i] !== undefined)
testsdiv.append('<p style="color: red;">' + testsOutstanding[i] + ' timed out</p>');
localStorage.clear();
}, 1000);
});