Cable-Desktop/libtextsecure/sendmessage.js
lilia 67f25214d3 Refactor outgoing identity key conflict handling
saveKeysToDeviceObject is the detector of outgoing identity key errors.
Catch these key errors closer to the source by pulling the
getKeysForNumber into the context of sendMessageToDevices, which lets
it access registerError and the message protobuf.

Previously identity key errors would be uncaught if all existing
sessions with a recipient were closed/deleted, since we would
preemptively fetch the new identity key. The old error handling only
kicked in after a 409/410 response from the server when posting a
message encrypted for a stale session.

// FREEBIE
2015-07-21 16:00:09 -07:00

512 lines
23 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/>.
*/
// sendMessage(numbers = [], message = PushMessageContentProto, callback(success/failure map))
window.textsecure.messaging = function() {
'use strict';
var self = {};
// success_callback(server success/failure map), error_callback(error_msg)
// message == PushMessageContentProto (NOT STRING)
function sendMessageToDevices(timestamp, number, deviceObjectList, message, success_callback, error_callback) {
var jsonData = [];
var relay = undefined;
var promises = [];
var addEncryptionFor = function(i) {
if (deviceObjectList[i].relay !== undefined) {
if (relay === undefined)
relay = deviceObjectList[i].relay;
else if (relay != deviceObjectList[i].relay)
return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
} else {
if (relay === undefined)
relay = "";
else if (relay != "")
return new Promise(function() { throw new Error("Mismatched relays for number " + number); });
}
return textsecure.protocol_wrapper.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) {
return textsecure.protocol_wrapper.getRegistrationId(deviceObjectList[i].encodedNumber).then(function(registrationId) {
return textsecure.storage.devices.removeTempKeysFromDevice(deviceObjectList[i].encodedNumber).then(function() {
jsonData[i] = {
type: encryptedMsg.type,
destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1],
destinationRegistrationId: registrationId,
content: encryptedMsg.body,
timestamp: timestamp
};
if (deviceObjectList[i].relay !== undefined)
jsonData[i].relay = deviceObjectList[i].relay;
});
});
});
}
for (var i = 0; i < deviceObjectList.length; i++)
promises[i] = addEncryptionFor(i);
return Promise.all(promises).then(function() {
var legacy = (message instanceof textsecure.protobuf.DataMessage);
return TextSecureServer.sendMessages(number, jsonData, legacy);
});
}
var makeAttachmentPointer;
var refreshGroup = function(number, groupId, devicesForNumber) {
groupId = getString(groupId);
var doUpdate = false;
return Promise.all(devicesForNumber.map(function(device) {
return textsecure.protocol_wrapper.getRegistrationId(device.encodedNumber).then(function(registrationId) {
return textsecure.storage.groups.needUpdateByDeviceRegistrationId(
groupId, number, device.encodedNumber, registrationId
).then(function(needUpdate) {
if (needUpdate) doUpdate = true;
});
});
})).then(function() {
if (!doUpdate) return;
return textsecure.storage.groups.getGroup(groupId).then(function(group) {
var numberIndex = group.numbers.indexOf(number);
if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race
return Promise.reject("Tried to refresh group to non-member");
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
if (group.avatar !== undefined) {
return makeAttachmentPointer(group.avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
});
} else {
return sendMessageToDevices(Date.now(), number, devicesForNumber, proto);
}
});
});
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
});
};
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.SEND_MESSAGE);
var sendMessageProto = function(timestamp, numbers, message, callback) {
var numbersCompleted = 0;
var errors = [];
var successfulNumbers = [];
var numberCompleted = function() {
numbersCompleted++;
if (numbersCompleted >= numbers.length)
callback({success: successfulNumbers, failure: errors});
};
var registerError = function(number, message, error) {
if (error) {
if (error.humanError)
message = error.humanError;
} else
error = new Error(message);
errors[errors.length] = { number: number, reason: message, error: error };
numberCompleted();
};
var reloadDevicesAndSend = function(number, recurse) {
return function() {
return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) {
if (devicesForNumber.length == 0)
return registerError(number, "Got empty device list when loading device keys", null);
doSendMessage(number, devicesForNumber, recurse);
});
}
};
function getKeysForNumber(number, updateDevices) {
var handleResult = function(response) {
return Promise.all(response.devices.map(function(device) {
if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1)
return textsecure.storage.devices.saveKeysToDeviceObject({
encodedNumber: number + "." + device.deviceId,
identityKey: response.identityKey,
preKey: device.preKey.publicKey,
preKeyId: device.preKey.keyId,
signedKey: device.signedPreKey.publicKey,
signedKeyId: device.signedPreKey.keyId,
signedKeySignature: device.signedPreKey.signature,
registrationId: device.registrationId
}).catch(function(error) {
if (error.message === "Identity key changed") {
error = new textsecure.OutgoingIdentityKeyError(number, message.toArrayBuffer(), timestamp, error.identityKey);
registerError(number, "Identity key changed", error);
}
throw error;
});
}));
};
if (updateDevices === undefined) {
return TextSecureServer.getKeysForNumber(number).then(handleResult);
} else {
var promises = [];
for (var i in updateDevices)
promises[promises.length] = TextSecureServer.getKeysForNumber(number, updateDevices[i]).then(handleResult);
return Promise.all(promises);
}
}
var doSendMessage = function(number, devicesForNumber, recurse) {
var groupUpdate = Promise.resolve(true);
if (message.group && message.group.id && message.group.type != textsecure.protobuf.GroupContext.Type.QUIT)
groupUpdate = refreshGroup(number, message.group.id, devicesForNumber);
return groupUpdate.then(function() {
return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) {
successfulNumbers[successfulNumbers.length] = number;
numberCompleted();
});
}).catch(function(error) {
if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) {
if (!recurse)
return registerError(number, "Hit retry limit attempting to reload device list", error);
var p;
if (error.code == 409) {
p = textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices);
} else {
p = Promise.all(error.response.staleDevices.map(function(deviceId) {
return textsecure.protocol_wrapper.closeOpenSessionForDevice(number + '.' + deviceId);
}));
}
p.then(function() {
var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices);
getKeysForNumber(number, resetDevices)
.then(reloadDevicesAndSend(number, false))
.catch(function(error) {
registerError(number, "Failed to reload device keys", error);
});
});
} else {
registerError(number, "Failed to create or send message", error);
}
});
};
var getDevicesAndSendToNumber = function(number) {
textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) {
return Promise.all(devicesForNumber.map(function(device) {
return textsecure.protocol_wrapper.hasOpenSession(device.encodedNumber).then(function(result) {
if (!result)
return getKeysForNumber(number, [parseInt(textsecure.utils.unencodeNumber(device.encodedNumber)[1])]);
});
})).then(function() {
return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devicesForNumber) {
if (devicesForNumber.length == 0) {
getKeysForNumber(number)
.then(reloadDevicesAndSend(number, true))
.catch(function(error) {
registerError(number, "Failed to retreive new device keys for number " + number, error);
});
} else
doSendMessage(number, devicesForNumber, true);
});
});
});
};
for (var i in numbers)
getDevicesAndSendToNumber(numbers[i]);
}
makeAttachmentPointer = function(attachment) {
var proto = new textsecure.protobuf.AttachmentPointer();
proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16);
return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) {
return TextSecureServer.putAttachment(encryptedBin).then(function(id) {
proto.id = id;
proto.contentType = attachment.contentType;
return proto;
});
});
}
var sendIndividualProto = function(number, proto, timestamp) {
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
});
}
var sendSyncMessage = function(message, timestamp, destination) {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp;
sentMessage.message = message;
if (destination) {
sentMessage.destination = destination;
}
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.sent = sentMessage;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
}
self.sendRequestGroupSyncMessage = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.request = request;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
};
self.sendRequestContactSyncMessage = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var request = new textsecure.protobuf.SyncMessage.Request();
request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.request = request;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
};
var sendGroupProto = function(numbers, proto, timestamp) {
timestamp = timestamp || Date.now();
var me = textsecure.storage.user.getNumber();
numbers = numbers.filter(function(number) { return number != me; });
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, numbers, proto, function(res) {
if (res.failure.length > 0)
reject(res.failure);
else
resolve();
});
}).then(function() {
return sendSyncMessage(proto, timestamp);
});
}
self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
var promises = [];
for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray;
return sendIndividualProto(number, proto, timestamp).then(function() {
return sendSyncMessage(proto, timestamp, number);
});
});
}
self.closeSession = function(number) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = "TERMINATE";
proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
return sendIndividualProto(number, proto, Date.now()).then(function(res) {
return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devices) {
return Promise.all(devices.map(function(device) {
return textsecure.protocol_wrapper.closeOpenSessionForDevice(device.encodedNumber);
})).then(function() {
return res;
});
});
});
}
self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
var promises = [];
for (var i in attachments)
promises.push(makeAttachmentPointer(attachments[i]));
return Promise.all(promises).then(function(attachmentsArray) {
proto.attachments = attachmentsArray;
return sendGroupProto(numbers, proto, timestamp);
});
});
}
self.createGroup = function(numbers, name, avatar) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
return textsecure.storage.groups.createNewGroup(numbers).then(function(group) {
proto.group.id = toArrayBuffer(group.id);
var numbers = group.numbers;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = numbers;
proto.group.name = name;
if (avatar !== undefined) {
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
});
} else {
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
}
});
}
self.updateGroup = function(groupId, name, avatar, numbers) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) {
if (numbers === undefined) {
return Promise.reject(new Error("Unknown Group"));
}
proto.group.members = numbers;
if (avatar !== undefined && avatar !== null) {
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
});
} else {
return sendGroupProto(numbers, proto).then(function() {
return proto.group.id;
});
}
});
}
self.addNumberToGroup = function(groupId, number) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return sendGroupProto(numbers, proto);
});
}
self.setGroupName = function(groupId, name) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return sendGroupProto(numbers, proto);
});
}
self.setGroupAvatar = function(groupId, avatar) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
proto.group.members = numbers;
return makeAttachmentPointer(avatar).then(function(attachment) {
proto.group.avatar = attachment;
return sendGroupProto(numbers, proto);
});
});
}
self.leaveGroup = function(groupId) {
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
return Promise.reject(new Error("Unknown Group"));
return textsecure.storage.groups.deleteGroup(groupId).then(function() {
return sendGroupProto(numbers, proto);
});
});
}
return self;
}();