
This ended up turning into a rewrite/refactor of the background page. For best results, view this diff with `-w` to ignore whitespace. In order to support retrying message decryption, possibly at a much later time than the message is received, we now implement the following: Each message is saved before it is decrypted. This generates a unique message_id which is later used to update the database entry with the message contents, or with any errors generated during processing. When an IncomingIdentityKeyError occurs, we catch it and save it on the model, then update the front end as usual. When the user clicks to accept the new key, the error is replayed, which causes the message to be decrypted and then passed to the background page for normal processing.
298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
/* vim: ts=4:sw=4:expandtab
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
window.textsecure = window.textsecure || {};
|
|
|
|
/*********************************
|
|
*** Type conversion utilities ***
|
|
*********************************/
|
|
// Strings/arrays
|
|
//TODO: Throw all this shit in favor of consistent types
|
|
//TODO: Namespace
|
|
var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
|
|
var StaticArrayBufferProto = new ArrayBuffer().__proto__;
|
|
var StaticUint8ArrayProto = new Uint8Array().__proto__;
|
|
function getString(thing) {
|
|
if (thing === Object(thing)) {
|
|
if (thing.__proto__ == StaticUint8ArrayProto)
|
|
return String.fromCharCode.apply(null, thing);
|
|
if (thing.__proto__ == StaticArrayBufferProto)
|
|
return getString(new Uint8Array(thing));
|
|
if (thing.__proto__ == StaticByteBufferProto)
|
|
return thing.toString("binary");
|
|
}
|
|
return thing;
|
|
}
|
|
|
|
function getStringable(thing) {
|
|
return (typeof thing == "string" || typeof thing == "number" || typeof thing == "boolean" ||
|
|
(thing === Object(thing) &&
|
|
(thing.__proto__ == StaticArrayBufferProto ||
|
|
thing.__proto__ == StaticUint8ArrayProto ||
|
|
thing.__proto__ == StaticByteBufferProto)));
|
|
}
|
|
|
|
function isEqual(a, b, mayBeShort) {
|
|
// TODO: Special-case arraybuffers, etc
|
|
if (a === undefined || b === undefined)
|
|
return false;
|
|
a = getString(a);
|
|
b = getString(b);
|
|
var maxLength = mayBeShort ? Math.min(a.length, b.length) : Math.max(a.length, b.length);
|
|
if (maxLength < 5)
|
|
throw new Error("a/b compare too short");
|
|
return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length));
|
|
}
|
|
|
|
function toArrayBuffer(thing) {
|
|
//TODO: Optimize this for specific cases
|
|
if (thing === undefined)
|
|
return undefined;
|
|
if (thing === Object(thing) && thing.__proto__ == StaticArrayBufferProto)
|
|
return thing;
|
|
|
|
if (thing instanceof Array) {
|
|
// Assuming Uint16Array from curve25519
|
|
var res = new ArrayBuffer(thing.length * 2);
|
|
var uint = new Uint16Array(res);
|
|
for (var i = 0; i < thing.length; i++)
|
|
uint[i] = thing[i];
|
|
return res;
|
|
}
|
|
|
|
if (!getStringable(thing))
|
|
throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer");
|
|
var str = getString(thing);
|
|
var res = new ArrayBuffer(str.length);
|
|
var uint = new Uint8Array(res);
|
|
for (var i = 0; i < str.length; i++)
|
|
uint[i] = str.charCodeAt(i);
|
|
return res;
|
|
}
|
|
|
|
// Number formatting utils
|
|
window.textsecure.utils = function() {
|
|
var self = {};
|
|
self.unencodeNumber = function(number) {
|
|
return number.split(".");
|
|
};
|
|
|
|
/**************************
|
|
*** JSON'ing Utilities ***
|
|
**************************/
|
|
function ensureStringed(thing) {
|
|
if (getStringable(thing))
|
|
return getString(thing);
|
|
else if (thing instanceof Array) {
|
|
var res = [];
|
|
for (var i = 0; i < thing.length; i++)
|
|
res[i] = ensureStringed(thing[i]);
|
|
return res;
|
|
} else if (thing === Object(thing)) {
|
|
var res = {};
|
|
for (var key in thing)
|
|
res[key] = ensureStringed(thing[key]);
|
|
return res;
|
|
}
|
|
throw new Error("unsure of how to jsonify object of type " + typeof thing);
|
|
|
|
}
|
|
|
|
self.jsonThing = function(thing) {
|
|
return JSON.stringify(ensureStringed(thing));
|
|
}
|
|
|
|
return self;
|
|
}();
|
|
|
|
window.textsecure.throwHumanError = function(error, type, humanError) {
|
|
var e = new Error(error);
|
|
if (type !== undefined)
|
|
e.name = type;
|
|
e.humanError = humanError;
|
|
throw e;
|
|
}
|
|
|
|
var handleAttachment = function(attachment) {
|
|
function getAttachment() {
|
|
return textsecure.api.getAttachment(attachment.id.toString());
|
|
}
|
|
|
|
function decryptAttachment(encrypted) {
|
|
return textsecure.protocol.decryptAttachment(
|
|
encrypted,
|
|
attachment.key.toArrayBuffer()
|
|
);
|
|
}
|
|
|
|
function updateAttachment(data) {
|
|
attachment.data = data;
|
|
}
|
|
|
|
return getAttachment().
|
|
then(decryptAttachment).
|
|
then(updateAttachment);
|
|
};
|
|
|
|
textsecure.processDecrypted = function(decrypted) {
|
|
|
|
// Now that its decrypted, validate the message and clean it up for consumer processing
|
|
// Note that messages may (generally) only perform one action and we ignore remaining fields
|
|
// after the first action.
|
|
|
|
if (decrypted.flags == null)
|
|
decrypted.flags = 0;
|
|
|
|
if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
|
|
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
|
|
return;
|
|
if (decrypted.flags != 0) {
|
|
throw new Error("Unknown flags in message");
|
|
}
|
|
|
|
var promises = [];
|
|
|
|
if (decrypted.group !== null) {
|
|
decrypted.group.id = getString(decrypted.group.id);
|
|
var existingGroup = textsecure.storage.groups.getNumbers(decrypted.group.id);
|
|
if (existingGroup === undefined) {
|
|
if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
|
|
throw new Error("Got message for unknown group");
|
|
}
|
|
textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
|
|
} else {
|
|
var fromIndex = existingGroup.indexOf(proto.source);
|
|
|
|
if (fromIndex < 0) {
|
|
//TODO: This could be indication of a race...
|
|
throw new Error("Sender was not a member of the group they were sending from");
|
|
}
|
|
|
|
switch(decrypted.group.type) {
|
|
case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE:
|
|
if (decrypted.group.avatar !== null)
|
|
promises.push(handleAttachment(decrypted.group.avatar));
|
|
|
|
if (existingGroup.filter(function(number) { decrypted.group.members.indexOf(number) < 0 }).length != 0) {
|
|
throw new Error("Attempted to remove numbers from group with an UPDATE");
|
|
}
|
|
decrypted.group.added = decrypted.group.members.filter(function(number) { return existingGroup.indexOf(number) < 0; });
|
|
|
|
var newGroup = textsecure.storage.groups.addNumbers(decrypted.group.id, decrypted.group.added);
|
|
if (newGroup.length != decrypted.group.members.length ||
|
|
newGroup.filter(function(number) { return decrypted.group.members.indexOf(number) < 0; }).length != 0) {
|
|
throw new Error("Error calculating group member difference");
|
|
}
|
|
|
|
//TODO: Also follow this path if avatar + name haven't changed (ie we should start storing those)
|
|
if (decrypted.group.avatar === null && decrypted.group.added.length == 0 && decrypted.group.name === null) {
|
|
return;
|
|
}
|
|
|
|
//TODO: Strictly verify all numbers (ie dont let verifyNumber do any user-magic tweaking)
|
|
|
|
decrypted.body = null;
|
|
decrypted.attachments = [];
|
|
|
|
break;
|
|
case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT:
|
|
textsecure.storage.groups.removeNumber(decrypted.group.id, proto.source);
|
|
|
|
decrypted.body = null;
|
|
decrypted.attachments = [];
|
|
case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER:
|
|
decrypted.group.name = null;
|
|
decrypted.group.members = [];
|
|
decrypted.group.avatar = null;
|
|
|
|
break;
|
|
default:
|
|
throw new Error("Unknown group message type");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var i in decrypted.attachments) {
|
|
promises.push(handleAttachment(decrypted.attachments[i]));
|
|
}
|
|
return Promise.all(promises).then(function() {
|
|
return decrypted;
|
|
});
|
|
}
|
|
|
|
window.textsecure.registerSingleDevice = function(number, verificationCode, stepDone) {
|
|
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
|
|
textsecure.storage.putEncrypted('signaling_key', signalingKey);
|
|
|
|
var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
|
|
password = password.substring(0, password.length - 2);
|
|
textsecure.storage.putEncrypted("password", password);
|
|
|
|
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
|
|
registrationId = registrationId & 0x3fff;
|
|
textsecure.storage.putUnencrypted("registrationId", registrationId);
|
|
|
|
return textsecure.api.confirmCode(number, verificationCode, password, signalingKey, registrationId, true).then(function() {
|
|
var numberId = number + ".1";
|
|
textsecure.storage.putUnencrypted("number_id", numberId);
|
|
textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegionCodeForNumber(number));
|
|
stepDone(1);
|
|
|
|
return textsecure.protocol.generateKeys().then(function(keys) {
|
|
stepDone(2);
|
|
return textsecure.api.registerKeys(keys).then(function() {
|
|
stepDone(3);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
window.textsecure.registerSecondDevice = function(encodedDeviceInit, cryptoInfo, stepDone) {
|
|
var deviceInit = textsecure.protobuf.DeviceInit.decode(encodedDeviceInit, 'binary');
|
|
return cryptoInfo.decryptAndHandleDeviceInit(deviceInit).then(function(identityKey) {
|
|
if (identityKey.server != textsecure.api.relay)
|
|
throw new Error("Unknown relay used by master");
|
|
var number = identityKey.phoneNumber;
|
|
|
|
stepDone(1);
|
|
|
|
var signalingKey = textsecure.crypto.getRandomBytes(32 + 20);
|
|
textsecure.storage.putEncrypted('signaling_key', signalingKey);
|
|
|
|
var password = btoa(getString(textsecure.crypto.getRandomBytes(16)));
|
|
password = password.substring(0, password.length - 2);
|
|
textsecure.storage.putEncrypted("password", password);
|
|
|
|
var registrationId = new Uint16Array(textsecure.crypto.getRandomBytes(2))[0];
|
|
registrationId = registrationId & 0x3fff;
|
|
textsecure.storage.putUnencrypted("registrationId", registrationId);
|
|
|
|
return textsecure.api.confirmCode(number, identityKey.provisioningCode, password, signalingKey, registrationId, false).then(function(result) {
|
|
var numberId = number + "." + result;
|
|
textsecure.storage.putUnencrypted("number_id", numberId);
|
|
textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegion(number));
|
|
stepDone(2);
|
|
|
|
return textsecure.protocol.generateKeys().then(function(keys) {
|
|
stepDone(3);
|
|
return textsecure.api.registerKeys(keys).then(function() {
|
|
stepDone(4);
|
|
//TODO: Send DeviceControl.NEW_DEVICE_REGISTERED to all other devices
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|