Implement sync protocol changes

Update protobuf definitions and refactor message receive and decrypt
codepath to support new protocol, including various flavors of sync
messages (sent messages, contacts, and groups).

Also cleans up background.js and lets libtextsecure internalize
textsecure.processDecrypted and ensure that it is called before handing
DataMessages off to the application.

The Envelope structure now has a generic content field and a
legacyMessage field for backwards compatibility. We'll send outgoing
messages as legacy messages, and sync messages as "content" while
continuing to support both legacy and non-legacy messages on the receive
side until old clients have a chance to transition.
This commit is contained in:
lilia 2015-06-01 14:08:21 -07:00
parent 757bcd4e50
commit a833d62a71
13 changed files with 756 additions and 379 deletions

View file

@ -59,6 +59,7 @@ module.exports = function(grunt) {
'libtextsecure/account_manager.js',
'libtextsecure/message_receiver.js',
'libtextsecure/sendmessage.js',
'libtextsecure/contacts_parser.js',
],
dest: 'js/libtextsecure.js',
},

View file

@ -45,28 +45,65 @@
// initialize the socket and start listening for messages
messageReceiver = new textsecure.MessageReceiver(window);
window.addEventListener('signal', function(ev) {
var proto = ev.proto;
if (proto.type === textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT) {
onDeliveryReceipt(proto);
} else {
onMessageReceived(proto);
}
});
window.addEventListener('contact', onContactReceived);
window.addEventListener('receipt', onDeliveryReceipt);
window.addEventListener('message', onMessageReceived);
window.addEventListener('group', onGroupReceived);
window.addEventListener('sent', onSentMessage);
window.addEventListener('error', onError);
messageReceiver.connect();
}
function onMessageReceived(pushMessage) {
function onContactReceived(contactInfo) {
new Whisper.Conversation({
name: contactInfo.name,
id: contactInfo.number,
avatar: contactInfo.avatar,
type: 'private',
active_at: null
}).save();
}
function onGroupReceived(group) {
new Whisper.Conversation({
members: group.members,
name: group.name,
id: group.id,
avatar: group.avatar,
type: 'group',
active_at: null
}).save();
}
function onMessageReceived(ev) {
var data = ev.data;
var message = initIncomingMessage(data.source, data.timestamp);
message.handlePushMessageContent(data.message);
}
function onSentMessage(ev) {
var now = new Date().getTime();
var timestamp = pushMessage.timestamp.toNumber();
var data = ev.data;
var message = new Whisper.Message({
source : pushMessage.source,
sourceDevice : pushMessage.sourceDevice,
relay : pushMessage.relay,
source : textsecure.storage.user.getNumber(),
sent_at : data.timestamp,
received_at : now,
conversationId : data.destination,
type : 'outgoing'
});
message.handlePushMessageContent(data.message);
}
function initIncomingMessage(source, timestamp) {
var now = new Date().getTime();
var message = new Whisper.Message({
source : source,
sent_at : timestamp,
received_at : now,
conversationId : pushMessage.source,
conversationId : source,
type : 'incoming'
});
@ -74,36 +111,38 @@
storage.put("unreadCount", newUnreadCount);
extension.navigator.setBadgeText(newUnreadCount);
message.save().then(function() {
return new Promise(function(resolve) {
resolve(textsecure.protocol_wrapper.handleIncomingPushMessageProto(pushMessage).then(
function(pushMessageContent) {
message.handlePushMessageContent(pushMessageContent);
}
));
}).catch(function(e) {
if (e.name === 'IncomingIdentityKeyError') {
message.save({ errors : [e] }).then(function() {
extension.trigger('updateInbox');
notifyConversation(message);
});
} else if (e.message === 'Bad MAC') {
message.save({ errors : [ _.pick(e, ['name', 'message'])]}).then(function() {
extension.trigger('updateInbox');
notifyConversation(message);
});
} else {
console.log(e);
throw e;
}
return message;
}
function onError(ev) {
var e = ev.error;
if (!ev.proto) {
console.log(e);
throw e;
}
var envelope = ev.proto;
var message = initIncomingMessage(envelope.source, envelope.timestamp.toNumber());
if (e.name === 'IncomingIdentityKeyError') {
message.save({ errors : [e] }).then(function() {
extension.trigger('updateInbox');
notifyConversation(message);
});
});
} else if (e.message !== 'Bad MAC') {
message.save({ errors : [ _.pick(e, ['name', 'message'])]}).then(function() {
extension.trigger('updateInbox');
notifyConversation(message);
});
} else {
console.log(e);
throw e;
}
}
// lazy hack
window.receipts = new Backbone.Collection();
function onDeliveryReceipt(pushMessage) {
function onDeliveryReceipt(ev) {
var pushMessage = ev.proto;
var timestamp = pushMessage.timestamp.toNumber();
var messages = new Whisper.MessageCollection();
var groups = new Whisper.ConversationCollection();

View file

@ -37784,17 +37784,6 @@ axolotlInternal.RecipientRecord = function() {
textsecure.storage.axolotl = new AxolotlStore();
var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl);
var decodeMessageContents = function(res) {
var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]);
if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION &&
finalMessage.sync !== null)
res[1]();
return finalMessage;
};
var handlePreKeyWhisperMessage = function(from, message) {
try {
return axolotlInstance.handlePreKeyWhisperMessage(from, message);
@ -37810,22 +37799,18 @@ axolotlInternal.RecipientRecord = function() {
window.textsecure = window.textsecure || {};
window.textsecure.protocol_wrapper = {
handleIncomingPushMessageProto: function(proto) {
switch(proto.type) {
case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT:
return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != ((3 << 4) | 3))
decrypt: function(source, sourceDevice, type, blob) {
if (sourceDevice === null) { sourceDevice = 0; }
var fromAddress = [source, sourceDevice].join('.');
switch(type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
return axolotlInstance.decryptWhisperMessage(fromAddress, getString(blob));
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
if (blob.readUint8() != ((3 << 4) | 3))
throw new Error("Bad version byte");
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return handlePreKeyWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT:
return Promise.resolve(null);
return handlePreKeyWhisperMessage(fromAddress, getString(blob));
default:
return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); });
return new Promise.reject(new Error("Unknown message type"));
}
},
closeOpenSessionForDevice: function(encodedNumber) {
@ -37852,8 +37837,18 @@ axolotlInternal.RecipientRecord = function() {
};
var tryMessageAgain = function(from, encodedMessage) {
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
}
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(function(res) {
var finalMessage = textsecure.protobuf.DataMessage.decode(res[0]);
if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION &&
finalMessage.sync !== null)
res[1]();
return processDecrypted(finalMessage);
});
};
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION);
})();
@ -38710,8 +38705,7 @@ window.textsecure.utils = function() {
return self;
}();
var handleAttachment = function(attachment) {
function handleAttachment(attachment) {
function getAttachment() {
return TextSecureServer.getAttachment(attachment.id.toString());
}
@ -38728,11 +38722,11 @@ var handleAttachment = function(attachment) {
}
return getAttachment().
then(decryptAttachment).
then(updateAttachment);
};
then(decryptAttachment).
then(updateAttachment);
}
textsecure.processDecrypted = function(decrypted, source) {
function processDecrypted(decrypted, source) {
// 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
@ -38741,21 +38735,11 @@ textsecure.processDecrypted = function(decrypted, source) {
if (decrypted.flags == null)
decrypted.flags = 0;
if (decrypted.sync !== null && textsecure.storage.user.getNumber() != source) {
// Ignore erroneous or malicious sync context from different number
decrypted.sync = null;
}
if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION) {
if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION) {
decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
if (decrypted.sync !== null) {
// We didn't actually close the session - see axolotl_wrapper
// so just throw an error since this message makes no sense
throw new Error("Got a sync END_SESSION message");
}
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
@ -38768,7 +38752,7 @@ textsecure.processDecrypted = function(decrypted, source) {
decrypted.group.id = getString(decrypted.group.id);
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
if (decrypted.group.avatar !== null) {
@ -38784,7 +38768,7 @@ textsecure.processDecrypted = function(decrypted, source) {
}
switch(decrypted.group.type) {
case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE:
case textsecure.protobuf.GroupContext.Type.UPDATE:
if (decrypted.group.avatar !== null)
promises.push(handleAttachment(decrypted.group.avatar));
@ -38811,11 +38795,11 @@ textsecure.processDecrypted = function(decrypted, source) {
});
break;
case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT:
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER:
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
@ -38834,7 +38818,7 @@ textsecure.processDecrypted = function(decrypted, source) {
return Promise.all(promises).then(function() {
return decrypted;
});
};
}
/* vim: ts=4:sw=4:expandtab
*
@ -39487,28 +39471,36 @@ function generateKeys(count, progressCallback) {
connect: function() {
// initialize the socket and start listening for messages
this.socket = TextSecureServer.getMessageWebsocket();
var eventTarget = this.target;
new WebSocketResource(this.socket, function(request) {
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {
var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(plaintext);
// After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
request.respond(200, 'OK');
new WebSocketResource(this.socket, this.handleRequest.bind(this));
},
handleRequest: function(request) {
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
request.respond(200, 'OK');
var ev = new Event('signal');
ev.proto = proto;
eventTarget.dispatchEvent(ev);
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else {
throw new Error('Received message with no content and no legacyMessage');
}
}).catch(function(e) {
console.log("Error handling incoming message:", e);
extension.trigger('error', e);
request.respond(500, 'Bad encrypted websocket message');
});
});
}.bind(this)).catch(function(e) {
request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error');
ev.error = e;
this.target.dispatchEvent(ev);
}.bind(this));
},
getStatus: function() {
if (this.socket) {
@ -39516,11 +39508,120 @@ function generateKeys(count, progressCallback) {
} else {
return -1;
}
},
onDeliveryReceipt: function (envelope) {
var ev = new Event('receipt');
ev.proto = envelope;
this.target.dispatchEvent(ev);
},
decrypt: function(envelope, ciphertext) {
return textsecure.protocol_wrapper.decrypt(
envelope.source,
envelope.sourceDevice,
envelope.type,
ciphertext
).catch(function(error) {
var ev = new Event('error');
ev.error = error;
ev.proto = envelope;
this.target.dispatchEvent(ev);
}.bind(this));
},
handleSentMessage: function(destination, timestamp, message) {
var source = textsecure.storage.user.getNumber();
return processDecrypted(message, source).then(function(message) {
var ev = new Event('sent');
ev.data = {
destination : destination,
timestamp : timestamp.toNumber(),
message : message
};
this.target.dispatchEvent(ev);
}.bind(this));
},
handleDataMessage: function(envelope, message, close_session) {
if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
close_session();
}
return processDecrypted(message, envelope.source).then(function(message) {
var ev = new Event('message');
ev.data = {
source : envelope.source,
timestamp : envelope.timestamp.toNumber(),
message : message
};
this.target.dispatchEvent(ev);
}.bind(this));
},
handleLegacyMessage: function (envelope) {
return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var message = textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message, close_session);
}.bind(this));
},
handleContentMessage: function (envelope) {
return this.decrypt(envelope, envelope.content).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var content = textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage, close_session);
} else {
throw new Error('Got Content message with no dataMessage and no syncMessage');
}
}.bind(this));
},
handleSyncMessage: function(envelope, syncMessage) {
if (envelope.source !== textsecure.storage.user.getNumber()) {
throw new Error('Received sync message from another number');
}
if (envelope.sourceDevice == textsecure.storage.user.getDeviceId()) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.sent) {
var sentMessage = syncMessage.sent;
return this.handleSentMessage(
sentMessage.destination,
sentMessage.timestamp,
sentMessage.message
);
} else if (syncMessage.contacts) {
this.handleContacts(syncMessage.contacts);
} else if (syncMessage.group) {
this.handleGroup(syncMessage.group);
} else {
throw new Error('Got SyncMessage with no sent, contacts, or group');
}
},
handleContacts: function(contacts) {
var eventTarget = this.target;
var attachmentPointer = contacts.blob;
return handleAttachment(attachmentPointer).then(function() {
var contactBuffer = new ContactBuffer(attachmentPointer.data);
var contactInfo = contactBuffer.readContact();
while (contactInfo !== undefined) {
var ev = new Event('contact');
ev.contactInfo = contactInfo;
eventTarget.dispatchEvent(ev);
contactInfo = contactBuffer.readContact();
}
});
},
handleGroup: function(envelope) {
var ev = new Event('group');
ev.group = envelope.group;
this.target.dispatchEvent(ev);
}
};
textsecure.MessageReceiver = MessageReceiver;
}());
/* vim: ts=4:sw=4:expandtab
@ -39638,11 +39739,11 @@ window.textsecure.messaging = function() {
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.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
@ -39659,7 +39760,7 @@ window.textsecure.messaging = function() {
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage);
var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
@ -39705,7 +39806,7 @@ window.textsecure.messaging = function() {
doSendMessage = function(number, devicesForNumber, recurse) {
var groupUpdate = Promise.resolve(true);
if (message.group && message.group.id && message.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT)
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) {
@ -39771,7 +39872,7 @@ window.textsecure.messaging = function() {
}
makeAttachmentPointer = function(attachment) {
var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer();
var proto = new textsecure.protobuf.AttachmentPointer();
proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16);
@ -39799,13 +39900,18 @@ window.textsecure.messaging = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var sync_message = textsecure.protobuf.PushMessageContent.decode(message.encode());
sync_message.sync = new textsecure.protobuf.PushMessageContent.SyncMessageContext();
var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp;
sentMessage.message = message;
if (destination) {
sync_message.sync.destination = destination;
sentMessage.destination = destination;
}
sync_message.sync.timestamp = timestamp;
return sendIndividualProto(myNumber, sync_message, Date.now());
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.sent = sentMessage;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
}
@ -39827,7 +39933,7 @@ window.textsecure.messaging = function() {
}
self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
var promises = [];
@ -39842,9 +39948,9 @@ window.textsecure.messaging = function() {
}
self.closeSession = function(number) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = "TERMINATE";
proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
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) {
@ -39857,11 +39963,11 @@ window.textsecure.messaging = function() {
}
self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER;
proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
@ -39878,14 +39984,14 @@ window.textsecure.messaging = function() {
}
self.createGroup = function(numbers, name, avatar) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
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.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = numbers;
proto.group.name = name;
@ -39905,11 +40011,11 @@ window.textsecure.messaging = function() {
}
self.updateGroup = function(groupId, name, avatar, numbers) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) {
@ -39934,10 +40040,10 @@ window.textsecure.messaging = function() {
}
self.addNumberToGroup = function(groupId, number) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) {
if (numbers === undefined)
@ -39949,10 +40055,10 @@ window.textsecure.messaging = function() {
}
self.setGroupName = function(groupId, name) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
@ -39965,10 +40071,10 @@ window.textsecure.messaging = function() {
}
self.setGroupAvatar = function(groupId, avatar) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
@ -39983,10 +40089,10 @@ window.textsecure.messaging = function() {
}
self.leaveGroup = function(groupId) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT;
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
@ -39999,4 +40105,30 @@ window.textsecure.messaging = function() {
return self;
}();
/*
* vim: ts=4:sw=4:expandtab
*/
function ContactBuffer(arrayBuffer) {
this.buffer = new dCodeIO.ByteBuffer(arrayBuffer);
}
ContactBuffer.prototype = {
constructor: ContactBuffer,
readContact: function() {
try {
var len = this.buffer.readVarint32();
this.buffer.skip(len);
var contactInfo = textsecure.protobuf.ContactDetails.decode(
this.buffer.slice(this.buffer.offset, len)
);
var attachmentLen = contactInfo.avatar.length;
contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, attachmentLen).toArrayBuffer(true /* copy? */);
this.buffer.skip(attachmentLen);
return contactInfo;
} catch(e) {
console.log(e);
}
}
};
})();

View file

@ -136,7 +136,7 @@
type : 'outgoing',
sent_at : now,
received_at : now,
flags : textsecure.protobuf.PushMessageContent.Flags.END_SESSION
flags : textsecure.protobuf.DataMessage.Flags.END_SESSION
}).save();
}

View file

@ -34,7 +34,7 @@
}
},
isEndSession: function() {
var flag = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
var flag = textsecure.protobuf.DataMessage.Flags.END_SESSION;
return !!(this.get('flags') & flag);
},
isGroupUpdate: function() {
@ -128,113 +128,103 @@
// identity key change.
var message = this;
var source = message.get('source');
var type = source === textsecure.storage.user.getNumber() ? 'outgoing' : 'incoming';
var timestamp = message.get('sent_at');
return textsecure.processDecrypted(pushMessageContent, source).then(function(pushMessageContent) {
var conversationId = source;
if (pushMessageContent.sync) {
conversationId = pushMessageContent.sync.destination;
}
var conversationId = message.get('conversationId');
if (pushMessageContent.group) {
conversationId = pushMessageContent.group.id;
}
var conversation = new Whisper.Conversation({id: conversationId});
conversation.fetch().always(function() {
var now = new Date().getTime();
var attributes = { type: 'private' };
if (pushMessageContent.group) {
conversationId = pushMessageContent.group.id;
}
var conversation = new Whisper.Conversation({id: conversationId});
conversation.fetch().always(function() {
var now = new Date().getTime();
var attributes = { type: 'private' };
if (pushMessageContent.group) {
var group_update = {};
var group_update = {};
attributes = {
type: 'group',
groupId: pushMessageContent.group.id,
};
if (pushMessageContent.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) {
attributes = {
type: 'group',
groupId: pushMessageContent.group.id,
type : 'group',
groupId : pushMessageContent.group.id,
name : pushMessageContent.group.name,
avatar : pushMessageContent.group.avatar,
members : pushMessageContent.group.members,
};
if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
attributes = {
type : 'group',
groupId : pushMessageContent.group.id,
name : pushMessageContent.group.name,
avatar : pushMessageContent.group.avatar,
members : pushMessageContent.group.members,
};
group_update = conversation.changedAttributes(_.pick(pushMessageContent.group, 'name', 'avatar'));
var difference = _.difference(pushMessageContent.group.members, conversation.get('members'));
if (difference.length > 0) {
group_update.joined = difference;
}
}
else if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT) {
group_update = { left: source };
attributes.members = _.without(conversation.get('members'), source);
}
if (_.keys(group_update).length > 0) {
message.set({group_update: group_update});
group_update = conversation.changedAttributes(_.pick(pushMessageContent.group, 'name', 'avatar'));
var difference = _.difference(pushMessageContent.group.members, conversation.get('members'));
if (difference.length > 0) {
group_update.joined = difference;
}
}
var type = 'incoming';
if (pushMessageContent.sync) {
type = 'outgoing';
timestamp = pushMessageContent.sync.timestamp.toNumber();
else if (pushMessageContent.group.type === textsecure.protobuf.GroupContext.Type.QUIT) {
group_update = { left: source };
attributes.members = _.without(conversation.get('members'), source);
}
// lazy hack - check for receipts that arrived early.
if (pushMessageContent.sync.destination) {
var receipt = window.receipts.findWhere({
timestamp: timestamp,
source: pushMessageContent.sync.destination
});
if (receipt) {
window.receipts.remove(receipt);
if (_.keys(group_update).length > 0) {
message.set({group_update: group_update});
}
}
if (type === 'outgoing') {
// lazy hack - check for receipts that arrived early.
if (pushMessageContent.group && pushMessageContent.group.id) { // group sync
var members = conversation.get('members') || [];
var receipts = window.receipts.where({ timestamp: timestamp });
for (var i in receipts) {
if (members.indexOf(receipts[i].get('source')) > -1) {
window.receipts.remove(receipts[i]);
message.set({
delivered: (message.get('delivered') || 0) + 1
});
}
} else if (pushMessageContent.group.id) { // group sync
var members = conversation.get('members') || [];
var receipts = window.receipts.where({ timestamp: timestamp });
for (var i in receipts) {
if (members.indexOf(receipts[i].get('source')) > -1) {
window.receipts.remove(receipts[i]);
message.set({
delivered: (message.get('delivered') || 0) + 1
});
}
}
} else {
throw new Error('Received sync message with no destination and no group id');
}
} else {
var receipt = window.receipts.findWhere({
timestamp: timestamp,
source: conversationId
});
if (receipt) {
window.receipts.remove(receipt);
message.set({
delivered: (message.get('delivered') || 0) + 1
});
}
}
attributes.active_at = now;
if (type === 'incoming') {
attributes.unreadCount = conversation.get('unreadCount') + 1;
}
conversation.set(attributes);
}
attributes.active_at = now;
if (type === 'incoming') {
attributes.unreadCount = conversation.get('unreadCount') + 1;
}
conversation.set(attributes);
message.set({
body : pushMessageContent.body,
conversationId : conversation.id,
attachments : pushMessageContent.attachments,
decrypted_at : now,
type : type,
sent_at : timestamp,
flags : pushMessageContent.flags,
errors : []
message.set({
body : pushMessageContent.body,
conversationId : conversation.id,
attachments : pushMessageContent.attachments,
decrypted_at : now,
type : type,
sent_at : timestamp,
flags : pushMessageContent.flags,
errors : []
});
if (message.get('sent_at') > conversation.get('timestamp')) {
conversation.set({
timestamp: message.get('sent_at'),
lastMessage: message.get('body')
});
}
if (message.get('sent_at') > conversation.get('timestamp')) {
conversation.set({
timestamp: message.get('sent_at'),
lastMessage: message.get('body')
});
}
conversation.save().then(function() {
message.save().then(function() {
extension.trigger('updateInbox'); // inbox fetch
if (message.isIncoming()) {
notifyConversation(message);
} else {
updateConversation(conversation.id);
}
});
conversation.save().then(function() {
message.save().then(function() {
extension.trigger('updateInbox'); // inbox fetch
if (message.isIncoming()) {
notifyConversation(message);
} else {
updateConversation(conversation.id);
}
});
});
});

View file

@ -5,17 +5,6 @@
textsecure.storage.axolotl = new AxolotlStore();
var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl);
var decodeMessageContents = function(res) {
var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]);
if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION &&
finalMessage.sync !== null)
res[1]();
return finalMessage;
};
var handlePreKeyWhisperMessage = function(from, message) {
try {
return axolotlInstance.handlePreKeyWhisperMessage(from, message);
@ -31,22 +20,18 @@
window.textsecure = window.textsecure || {};
window.textsecure.protocol_wrapper = {
handleIncomingPushMessageProto: function(proto) {
switch(proto.type) {
case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT:
return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message));
case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT:
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE:
if (proto.message.readUint8() != ((3 << 4) | 3))
decrypt: function(source, sourceDevice, type, blob) {
if (sourceDevice === null) { sourceDevice = 0; }
var fromAddress = [source, sourceDevice].join('.');
switch(type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
return axolotlInstance.decryptWhisperMessage(fromAddress, getString(blob));
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
if (blob.readUint8() != ((3 << 4) | 3))
throw new Error("Bad version byte");
var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice);
return handlePreKeyWhisperMessage(from, getString(proto.message)).then(decodeMessageContents);
case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT:
return Promise.resolve(null);
return handlePreKeyWhisperMessage(fromAddress, getString(blob));
default:
return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); });
return new Promise.reject(new Error("Unknown message type"));
}
},
closeOpenSessionForDevice: function(encodedNumber) {
@ -73,8 +58,18 @@
};
var tryMessageAgain = function(from, encodedMessage) {
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents);
}
return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(function(res) {
var finalMessage = textsecure.protobuf.DataMessage.decode(res[0]);
if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION &&
finalMessage.sync !== null)
res[1]();
return processDecrypted(finalMessage);
});
};
textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION);
})();

View file

@ -0,0 +1,25 @@
/*
* vim: ts=4:sw=4:expandtab
*/
function ContactBuffer(arrayBuffer) {
this.buffer = new dCodeIO.ByteBuffer(arrayBuffer);
}
ContactBuffer.prototype = {
constructor: ContactBuffer,
readContact: function() {
try {
var len = this.buffer.readVarint32();
this.buffer.skip(len);
var contactInfo = textsecure.protobuf.ContactDetails.decode(
this.buffer.slice(this.buffer.offset, len)
);
var attachmentLen = contactInfo.avatar.length;
contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, attachmentLen).toArrayBuffer(true /* copy? */);
this.buffer.skip(attachmentLen);
return contactInfo;
} catch(e) {
console.log(e);
}
}
};

View file

@ -125,8 +125,7 @@ window.textsecure.utils = function() {
return self;
}();
var handleAttachment = function(attachment) {
function handleAttachment(attachment) {
function getAttachment() {
return TextSecureServer.getAttachment(attachment.id.toString());
}
@ -143,11 +142,11 @@ var handleAttachment = function(attachment) {
}
return getAttachment().
then(decryptAttachment).
then(updateAttachment);
};
then(decryptAttachment).
then(updateAttachment);
}
textsecure.processDecrypted = function(decrypted, source) {
function processDecrypted(decrypted, source) {
// 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
@ -156,21 +155,11 @@ textsecure.processDecrypted = function(decrypted, source) {
if (decrypted.flags == null)
decrypted.flags = 0;
if (decrypted.sync !== null && textsecure.storage.user.getNumber() != source) {
// Ignore erroneous or malicious sync context from different number
decrypted.sync = null;
}
if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION)
== textsecure.protobuf.PushMessageContent.Flags.END_SESSION) {
if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION) {
decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
if (decrypted.sync !== null) {
// We didn't actually close the session - see axolotl_wrapper
// so just throw an error since this message makes no sense
throw new Error("Got a sync END_SESSION message");
}
return Promise.resolve(decrypted);
}
if (decrypted.flags != 0) {
@ -183,7 +172,7 @@ textsecure.processDecrypted = function(decrypted, source) {
decrypted.group.id = getString(decrypted.group.id);
promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
if (existingGroup === undefined) {
if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) {
if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
throw new Error("Got message for unknown group");
}
if (decrypted.group.avatar !== null) {
@ -199,7 +188,7 @@ textsecure.processDecrypted = function(decrypted, source) {
}
switch(decrypted.group.type) {
case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE:
case textsecure.protobuf.GroupContext.Type.UPDATE:
if (decrypted.group.avatar !== null)
promises.push(handleAttachment(decrypted.group.avatar));
@ -226,11 +215,11 @@ textsecure.processDecrypted = function(decrypted, source) {
});
break;
case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT:
case textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER:
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.members = [];
decrypted.group.avatar = null;
@ -249,4 +238,4 @@ textsecure.processDecrypted = function(decrypted, source) {
return Promise.all(promises).then(function() {
return decrypted;
});
};
}

View file

@ -31,28 +31,36 @@
connect: function() {
// initialize the socket and start listening for messages
this.socket = TextSecureServer.getMessageWebsocket();
var eventTarget = this.target;
new WebSocketResource(this.socket, function(request) {
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {
var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(plaintext);
// After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
request.respond(200, 'OK');
new WebSocketResource(this.socket, this.handleRequest.bind(this));
},
handleRequest: function(request) {
// TODO: handle different types of requests. for now we only expect
// PUT /messages <encrypted IncomingPushMessageSignal>
textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) {
var envelope = textsecure.protobuf.Envelope.decode(plaintext);
// After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
request.respond(200, 'OK');
var ev = new Event('signal');
ev.proto = proto;
eventTarget.dispatchEvent(ev);
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
this.onDeliveryReceipt(envelope);
} else if (envelope.content) {
this.handleContentMessage(envelope);
} else if (envelope.legacyMessage) {
this.handleLegacyMessage(envelope);
} else {
throw new Error('Received message with no content and no legacyMessage');
}
}).catch(function(e) {
console.log("Error handling incoming message:", e);
extension.trigger('error', e);
request.respond(500, 'Bad encrypted websocket message');
});
});
}.bind(this)).catch(function(e) {
request.respond(500, 'Bad encrypted websocket message');
console.log("Error handling incoming message:", e);
var ev = new Event('error');
ev.error = e;
this.target.dispatchEvent(ev);
}.bind(this));
},
getStatus: function() {
if (this.socket) {
@ -60,9 +68,118 @@
} else {
return -1;
}
},
onDeliveryReceipt: function (envelope) {
var ev = new Event('receipt');
ev.proto = envelope;
this.target.dispatchEvent(ev);
},
decrypt: function(envelope, ciphertext) {
return textsecure.protocol_wrapper.decrypt(
envelope.source,
envelope.sourceDevice,
envelope.type,
ciphertext
).catch(function(error) {
var ev = new Event('error');
ev.error = error;
ev.proto = envelope;
this.target.dispatchEvent(ev);
}.bind(this));
},
handleSentMessage: function(destination, timestamp, message) {
var source = textsecure.storage.user.getNumber();
return processDecrypted(message, source).then(function(message) {
var ev = new Event('sent');
ev.data = {
destination : destination,
timestamp : timestamp.toNumber(),
message : message
};
this.target.dispatchEvent(ev);
}.bind(this));
},
handleDataMessage: function(envelope, message, close_session) {
if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
== textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
close_session();
}
return processDecrypted(message, envelope.source).then(function(message) {
var ev = new Event('message');
ev.data = {
source : envelope.source,
timestamp : envelope.timestamp.toNumber(),
message : message
};
this.target.dispatchEvent(ev);
}.bind(this));
},
handleLegacyMessage: function (envelope) {
return this.decrypt(envelope, envelope.legacyMessage).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var message = textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message, close_session);
}.bind(this));
},
handleContentMessage: function (envelope) {
return this.decrypt(envelope, envelope.content).then(function(result) {
var plaintext = result[0]; // array buffer
var close_session = result[1]; // function
var content = textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
} else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage, close_session);
} else {
throw new Error('Got Content message with no dataMessage and no syncMessage');
}
}.bind(this));
},
handleSyncMessage: function(envelope, syncMessage) {
if (envelope.source !== textsecure.storage.user.getNumber()) {
throw new Error('Received sync message from another number');
}
if (envelope.sourceDevice == textsecure.storage.user.getDeviceId()) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.sent) {
var sentMessage = syncMessage.sent;
return this.handleSentMessage(
sentMessage.destination,
sentMessage.timestamp,
sentMessage.message
);
} else if (syncMessage.contacts) {
this.handleContacts(syncMessage.contacts);
} else if (syncMessage.group) {
this.handleGroup(syncMessage.group);
} else {
throw new Error('Got SyncMessage with no sent, contacts, or group');
}
},
handleContacts: function(contacts) {
var eventTarget = this.target;
var attachmentPointer = contacts.blob;
return handleAttachment(attachmentPointer).then(function() {
var contactBuffer = new ContactBuffer(attachmentPointer.data);
var contactInfo = contactBuffer.readContact();
while (contactInfo !== undefined) {
var ev = new Event('contact');
ev.contactInfo = contactInfo;
eventTarget.dispatchEvent(ev);
contactInfo = contactBuffer.readContact();
}
});
},
handleGroup: function(envelope) {
var ev = new Event('group');
ev.group = envelope.group;
this.target.dispatchEvent(ev);
}
};
textsecure.MessageReceiver = MessageReceiver;
}());

View file

@ -113,11 +113,11 @@ window.textsecure.messaging = function() {
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.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(group.id);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = group.numbers;
proto.group.name = group.name === undefined ? null : group.name;
@ -134,7 +134,7 @@ window.textsecure.messaging = function() {
}
var tryMessageAgain = function(number, encodedMessage, timestamp) {
var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage);
var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
return new Promise(function(resolve, reject) {
sendMessageProto(timestamp, [number], proto, function(res) {
if (res.failure.length > 0)
@ -180,7 +180,7 @@ window.textsecure.messaging = function() {
doSendMessage = function(number, devicesForNumber, recurse) {
var groupUpdate = Promise.resolve(true);
if (message.group && message.group.id && message.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT)
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) {
@ -246,7 +246,7 @@ window.textsecure.messaging = function() {
}
makeAttachmentPointer = function(attachment) {
var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer();
var proto = new textsecure.protobuf.AttachmentPointer();
proto.key = textsecure.crypto.getRandomBytes(64);
var iv = textsecure.crypto.getRandomBytes(16);
@ -274,13 +274,18 @@ window.textsecure.messaging = function() {
var myNumber = textsecure.storage.user.getNumber();
var myDevice = textsecure.storage.user.getDeviceId();
if (myDevice != 1) {
var sync_message = textsecure.protobuf.PushMessageContent.decode(message.encode());
sync_message.sync = new textsecure.protobuf.PushMessageContent.SyncMessageContext();
var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
sentMessage.timestamp = timestamp;
sentMessage.message = message;
if (destination) {
sync_message.sync.destination = destination;
sentMessage.destination = destination;
}
sync_message.sync.timestamp = timestamp;
return sendIndividualProto(myNumber, sync_message, Date.now());
var syncMessage = new textsecure.protobuf.SyncMessage();
syncMessage.sent = sentMessage;
var contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
return sendIndividualProto(myNumber, contentMessage, Date.now());
}
}
@ -302,7 +307,7 @@ window.textsecure.messaging = function() {
}
self.sendMessageToNumber = function(number, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
var promises = [];
@ -317,9 +322,9 @@ window.textsecure.messaging = function() {
}
self.closeSession = function(number) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = "TERMINATE";
proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
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) {
@ -332,11 +337,11 @@ window.textsecure.messaging = function() {
}
self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) {
var proto = new textsecure.protobuf.PushMessageContent();
var proto = new textsecure.protobuf.DataMessage();
proto.body = messageText;
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER;
proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
@ -353,14 +358,14 @@ window.textsecure.messaging = function() {
}
self.createGroup = function(numbers, name, avatar) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
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.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = numbers;
proto.group.name = name;
@ -380,11 +385,11 @@ window.textsecure.messaging = function() {
}
self.updateGroup = function(groupId, name, avatar, numbers) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) {
@ -409,10 +414,10 @@ window.textsecure.messaging = function() {
}
self.addNumberToGroup = function(groupId, number) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) {
if (numbers === undefined)
@ -424,10 +429,10 @@ window.textsecure.messaging = function() {
}
self.setGroupName = function(groupId, name) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
@ -440,10 +445,10 @@ window.textsecure.messaging = function() {
}
self.setGroupAvatar = function(groupId, avatar) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE;
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)
@ -458,10 +463,10 @@ window.textsecure.messaging = function() {
}
self.leaveGroup = function(groupId) {
var proto = new textsecure.protobuf.PushMessageContent();
proto.group = new textsecure.protobuf.PushMessageContent.GroupContext();
var proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = toArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT;
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
if (numbers === undefined)

View file

@ -0,0 +1,50 @@
/* 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/>.
*/
describe('MessageReceiver', function() {
var WebSocket = window.WebSocket;
before(function() { window.WebSocket = MockSocket; });
after (function() { window.WebSocket = WebSocket; });
it('connects', function(done) {
var mockServer = new MockServer('ws://localhost:8080');
var attrs = {
type: textsecure.protobuf.Envelope.Type.PLAINTEXT,
source: '+19999999999',
sourceDevice: '1',
timestamp: Date.now(),
};
mockServer.on('connection', function(server) {
var signal = new textsecure.protobuf.Envelope(attrs);
signal.message = new textsecure.protobuf.DataMessage({ body: 'hello' });
server.send(
new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/messages', body: signal }
}).encode().toArrayBuffer()
);
});
window.addEventListener('signal', function(ev) {
var signal = ev.proto;
for (var key in attrs) {
assert.strictEqual(attrs[key], signal[key]);
}
assert.strictEqual(signal.message.body, 'hello');
});
var messageReceiver = new textsecure.MessageReceiver(window);
messageReceiver.connect();
});
});

View file

@ -22,7 +22,7 @@ describe('Protocol', function() {
it('works', function(done) {
localStorage.clear();
var text_message = new textsecure.protobuf.PushMessageContent();
var text_message = new textsecure.protobuf.DataMessage();
text_message.body = "Hi Mom";
var server_message = {
type: 4, // unencrypted
@ -31,8 +31,12 @@ describe('Protocol', function() {
message: text_message.encode()
};
return textsecure.protocol_wrapper.handleIncomingPushMessageProto(server_message).
then(function(message) {
return textsecure.protocol_wrapper.handleEncryptedMessage(
server_message.source,
server_message.source_device,
server_message.type,
server_message.message
).then(function(message) {
assert.equal(message.body, text_message.body);
assert.equal(message.attachments.length, text_message.attachments.length);
assert.equal(text_message.attachments.length, 0);

View file

@ -1,52 +1,32 @@
package textsecure;
option java_package = "org.whispersystems.textsecure.push";
option java_outer_classname = "PushMessageProtos";
option java_package = "org.whispersystems.textsecure.internal.push";
option java_outer_classname = "TextSecureProtos";
message IncomingPushMessageSignal {
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
PLAINTEXT = 4;
RECEIPT = 5;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes message = 6; // Contains an encrypted PushMessageContent
// repeated string destinations = 4; // No longer supported
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage
optional bytes content = 8; // Contains an encrypted Content
}
message PushMessageContent {
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
message SyncMessageContext {
optional string destination = 1;
optional uint64 timestamp = 2;
}
message Content {
optional DataMessage dataMessage = 1;
optional SyncMessage syncMessage = 2;
}
message DataMessage {
enum Flags {
END_SESSION = 1;
}
@ -55,5 +35,55 @@ message PushMessageContent {
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
optional uint32 flags = 4;
optional SyncMessageContext sync = 5;
}
message SyncMessage {
message Sent {
optional string destination = 1;
optional uint64 timestamp = 2;
optional DataMessage message = 3;
}
message Contacts {
optional AttachmentPointer blob = 1;
}
message Group {
optional GroupContext group = 1;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Group group = 3;
}
message AttachmentPointer {
optional fixed64 id = 1;
optional string contentType = 2;
optional bytes key = 3;
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string members = 4;
optional AttachmentPointer avatar = 5;
}
message ContactDetails {
message Avatar {
optional string contentType = 1;
optional uint64 length = 2;
}
optional string number = 1;
optional string name = 2;
optional Avatar avatar = 3;
}