123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- /*
- * vim: ts=4:sw=4:expandtab
- */
- function MessageReceiver(url, ports, username, password, signalingKey) {
- this.url = url;
- this.signalingKey = signalingKey;
- this.username = username;
- this.password = password;
- this.server = new TextSecureServer(url, ports, username, password);
- var address = libsignal.SignalProtocolAddress.fromString(username);
- this.number = address.getName();
- this.deviceId = address.getDeviceId();
- }
- MessageReceiver.prototype = new textsecure.EventTarget();
- MessageReceiver.prototype.extend({
- constructor: MessageReceiver,
- connect: function() {
- if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
- this.socket.close();
- }
- console.log('opening websocket');
- // initialize the socket and start listening for messages
- this.socket = this.server.getMessageSocket();
- this.socket.onclose = this.onclose.bind(this);
- this.socket.onerror = this.onerror.bind(this);
- this.socket.onopen = this.onopen.bind(this);
- this.wsr = new WebSocketResource(this.socket, {
- handleRequest: this.handleRequest.bind(this),
- keepalive: { path: '/v1/keepalive', disconnect: true }
- });
- this.pending = Promise.resolve();
- },
- close: function() {
- this.socket.close(3000, 'called close');
- delete this.listeners;
- },
- onopen: function() {
- console.log('websocket open');
- },
- onerror: function(error) {
- console.log('websocket error');
- },
- onclose: function(ev) {
- console.log('websocket closed', ev.code, ev.reason || '');
- if (ev.code === 3000) {
- return;
- }
- var eventTarget = this;
- // possible 403 or network issue. Make an request to confirm
- this.server.getDevices(this.number).
- then(this.connect.bind(this)). // No HTTP error? Reconnect
- catch(function(e) {
- var ev = new Event('error');
- ev.error = e;
- eventTarget.dispatchEvent(ev);
- });
- },
- handleRequest: function(request) {
- // We do the message decryption here, instead of in the ordered pending queue,
- // to avoid exposing the time it took us to process messages through the time-to-ack.
- // TODO: handle different types of requests. for now we blindly assume
- // PUT /messages <encrypted Envelope>
- textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).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');
- if (!this.isBlocked(envelope.source)) {
- this.queueEnvelope(envelope);
- }
- }.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.dispatchEvent(ev);
- }.bind(this));
- },
- queueEnvelope: function(envelope) {
- var handleEnvelope = this.handleEnvelope.bind(this, envelope);
- this.pending = this.pending.then(handleEnvelope, handleEnvelope);
- },
- handleEnvelope: function(envelope) {
- if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
- return this.onDeliveryReceipt(envelope);
- }
- if (envelope.content) {
- return this.handleContentMessage(envelope);
- } else if (envelope.legacyMessage) {
- return this.handleLegacyMessage(envelope);
- } else {
- throw new Error('Received message with no content and no legacyMessage');
- }
- },
- getStatus: function() {
- if (this.socket) {
- return this.socket.readyState;
- } else {
- return -1;
- }
- },
- onDeliveryReceipt: function (envelope) {
- var ev = new Event('receipt');
- ev.proto = envelope;
- this.dispatchEvent(ev);
- },
- unpad: function(paddedPlaintext) {
- paddedPlaintext = new Uint8Array(paddedPlaintext);
- var plaintext;
- for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
- if (paddedPlaintext[i] == 0x80) {
- plaintext = new Uint8Array(i);
- plaintext.set(paddedPlaintext.subarray(0, i));
- plaintext = plaintext.buffer;
- break;
- } else if (paddedPlaintext[i] !== 0x00) {
- throw new Error('Invalid padding');
- }
- }
- return plaintext;
- },
- decrypt: function(envelope, ciphertext) {
- var promise;
- var address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice);
- var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
- switch(envelope.type) {
- case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
- console.log('message from', envelope.source + '.' + envelope.sourceDevice, envelope.timestamp.toNumber());
- promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad);
- break;
- case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
- console.log('prekey message from', envelope.source + '.' + envelope.sourceDevice, envelope.timestamp.toNumber());
- promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address);
- break;
- default:
- promise = Promise.reject(new Error("Unknown message type"));
- }
- return promise.catch(function(error) {
- var ev = new Event('error');
- ev.error = error;
- ev.proto = envelope;
- this.dispatchEvent(ev);
- return Promise.reject(error);
- }.bind(this));
- },
- decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) {
- return sessionCipher.decryptPreKeyWhisperMessage(ciphertext).then(this.unpad).catch(function(e) {
- if (e.message === 'Unknown identity key') {
- // create an error that the UI will pick up and ask the
- // user if they want to re-negotiate
- var buffer = dcodeIO.ByteBuffer.wrap(ciphertext);
- throw new textsecure.IncomingIdentityKeyError(
- address.toString(),
- buffer.toArrayBuffer(),
- e.identityKey
- );
- }
- throw e;
- });
- },
- handleSentMessage: function(destination, timestamp, message, expirationStartTimestamp) {
- var p = Promise.resolve();
- if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
- textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
- p = this.handleEndSession(destination);
- }
- return p.then(function() {
- return this.processDecrypted(message, this.number).then(function(message) {
- var ev = new Event('sent');
- ev.data = {
- destination : destination,
- timestamp : timestamp.toNumber(),
- message : message
- };
- if (expirationStartTimestamp) {
- ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
- }
- this.dispatchEvent(ev);
- }.bind(this));
- }.bind(this));
- },
- handleDataMessage: function(envelope, message) {
- var encodedNumber = envelope.source + '.' + envelope.sourceDevice;
- console.log('data message from', encodedNumber, envelope.timestamp.toNumber());
- var p = Promise.resolve();
- if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
- textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
- p = this.handleEndSession(envelope.source);
- }
- return p.then(function() {
- return this.processDecrypted(message, envelope.source).then(function(message) {
- var ev = new Event('message');
- ev.data = {
- source : envelope.source,
- timestamp : envelope.timestamp.toNumber(),
- message : message
- };
- this.dispatchEvent(ev);
- }.bind(this));
- }.bind(this));
- },
- handleLegacyMessage: function (envelope) {
- return this.decrypt(envelope, envelope.legacyMessage).then(function(plaintext) {
- var message = textsecure.protobuf.DataMessage.decode(plaintext);
- return this.handleDataMessage(envelope, message);
- }.bind(this));
- },
- handleContentMessage: function (envelope) {
- return this.decrypt(envelope, envelope.content).then(function(plaintext) {
- 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);
- } else {
- throw new Error('Got Content message with no dataMessage and no syncMessage');
- }
- }.bind(this));
- },
- handleSyncMessage: function(envelope, syncMessage) {
- if (envelope.source !== this.number) {
- throw new Error('Received sync message from another number');
- }
- if (envelope.sourceDevice == this.deviceId) {
- throw new Error('Received sync message from our own device');
- }
- if (syncMessage.sent) {
- var sentMessage = syncMessage.sent;
- console.log('sent message to',
- sentMessage.destination,
- sentMessage.timestamp.toNumber(),
- 'from', envelope.source + '.' + envelope.sourceDevice
- );
- return this.handleSentMessage(
- sentMessage.destination,
- sentMessage.timestamp,
- sentMessage.message,
- sentMessage.expirationStartTimestamp
- );
- } else if (syncMessage.contacts) {
- this.handleContacts(syncMessage.contacts);
- } else if (syncMessage.groups) {
- this.handleGroups(syncMessage.groups);
- } else if (syncMessage.blocked) {
- this.handleBlocked(syncMessage.blocked);
- } else if (syncMessage.request) {
- console.log('Got SyncMessage Request');
- } else if (syncMessage.read) {
- console.log('read messages',
- 'from', envelope.source + '.' + envelope.sourceDevice);
- this.handleRead(syncMessage.read, envelope.timestamp);
- } else {
- throw new Error('Got empty SyncMessage');
- }
- },
- handleRead: function(read, timestamp) {
- for (var i = 0; i < read.length; ++i) {
- var ev = new Event('read');
- ev.timestamp = timestamp.toNumber();
- ev.read = {
- timestamp : read[i].timestamp.toNumber(),
- sender : read[i].sender
- }
- this.dispatchEvent(ev);
- }
- },
- handleContacts: function(contacts) {
- console.log('contact sync');
- var eventTarget = this;
- var attachmentPointer = contacts.blob;
- return this.handleAttachment(attachmentPointer).then(function() {
- var contactBuffer = new ContactBuffer(attachmentPointer.data);
- var contactDetails = contactBuffer.next();
- while (contactDetails !== undefined) {
- var ev = new Event('contact');
- ev.contactDetails = contactDetails;
- eventTarget.dispatchEvent(ev);
- contactDetails = contactBuffer.next();
- }
- eventTarget.dispatchEvent(new Event('contactsync'));
- });
- },
- handleGroups: function(groups) {
- console.log('group sync');
- var eventTarget = this;
- var attachmentPointer = groups.blob;
- return this.handleAttachment(attachmentPointer).then(function() {
- var groupBuffer = new GroupBuffer(attachmentPointer.data);
- var groupDetails = groupBuffer.next();
- var promises = [];
- while (groupDetails !== undefined) {
- var promise = (function(groupDetails) {
- groupDetails.id = groupDetails.id.toBinary();
- if (groupDetails.active) {
- return textsecure.storage.groups.getGroup(groupDetails.id).
- then(function(existingGroup) {
- if (existingGroup === undefined) {
- return textsecure.storage.groups.createNewGroup(
- groupDetails.members, groupDetails.id
- );
- } else {
- return textsecure.storage.groups.updateNumbers(
- groupDetails.id, groupDetails.members
- );
- }
- }).then(function() { return groupDetails });
- } else {
- return Promise.resolve(groupDetails);
- }
- })(groupDetails).then(function(groupDetails) {
- var ev = new Event('group');
- ev.groupDetails = groupDetails;
- eventTarget.dispatchEvent(ev);
- }).catch(function(e) {
- console.log('error processing group', e);
- });
- groupDetails = groupBuffer.next();
- promises.push(promise);
- }
- Promise.all(promises).then(function() {
- eventTarget.dispatchEvent(new Event('groupsync'));
- });
- });
- },
- handleBlocked: function(blocked) {
- textsecure.storage.put('blocked', blocked.numbers);
- },
- isBlocked: function(number) {
- return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
- },
- handleAttachment: function(attachment) {
- var digest = attachment.digest ? attachment.digest.toArrayBuffer() : undefined;
- function decryptAttachment(encrypted) {
- return textsecure.crypto.decryptAttachment(
- encrypted,
- attachment.key.toArrayBuffer(),
- digest
- );
- }
- function updateAttachment(data) {
- attachment.data = data;
- }
- return this.server.getAttachment(attachment.id.toString()).
- then(decryptAttachment).
- then(updateAttachment);
- },
- tryMessageAgain: function(from, ciphertext) {
- var address = libsignal.SignalProtocolAddress.fromString(from);
- var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
- console.log('retrying prekey whisper message');
- return this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address).then(function(plaintext) {
- var finalMessage = textsecure.protobuf.DataMessage.decode(plaintext);
- var p = Promise.resolve();
- if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
- == textsecure.protobuf.DataMessage.Flags.END_SESSION &&
- finalMessage.sync !== null) {
- var number = address.getName();
- p = this.handleEndSession(number);
- }
- return p.then(function() {
- return this.processDecrypted(finalMessage);
- }.bind(this));
- }.bind(this));
- },
- handleEndSession: function(number) {
- console.log('got end session');
- return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
- return Promise.all(deviceIds.map(function(deviceId) {
- var address = new libsignal.SignalProtocolAddress(number, deviceId);
- var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
- console.log('closing session for', address.toString());
- return sessionCipher.closeOpenSessionForDevice();
- }));
- });
- },
- processDecrypted: function(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
- // after the first action.
- if (decrypted.flags == null) {
- decrypted.flags = 0;
- }
- if (decrypted.expireTimer == null) {
- decrypted.expireTimer = 0;
- }
- if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
- decrypted.body = null;
- decrypted.attachments = [];
- decrypted.group = null;
- return Promise.resolve(decrypted);
- } else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) {
- decrypted.body = null;
- decrypted.attachments = [];
- } else if (decrypted.flags != 0) {
- throw new Error("Unknown flags in message");
- }
- var promises = [];
- if (decrypted.group !== null) {
- decrypted.group.id = decrypted.group.id.toBinary();
- if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
- if (decrypted.group.avatar !== null) {
- promises.push(this.handleAttachment(decrypted.group.avatar));
- }
- }
- promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
- if (existingGroup === undefined) {
- if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
- decrypted.group.members = [source];
- console.log("Got message for unknown group");
- }
- return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
- } else {
- var fromIndex = existingGroup.indexOf(source);
- if (fromIndex < 0) {
- //TODO: This could be indication of a race...
- console.log("Sender was not a member of the group they were sending from");
- }
- switch(decrypted.group.type) {
- case textsecure.protobuf.GroupContext.Type.UPDATE:
- decrypted.body = null;
- decrypted.attachments = [];
- return textsecure.storage.groups.updateNumbers(
- decrypted.group.id, decrypted.group.members
- );
- break;
- case textsecure.protobuf.GroupContext.Type.QUIT:
- decrypted.body = null;
- decrypted.attachments = [];
- if (source === this.number) {
- return textsecure.storage.groups.deleteGroup(decrypted.group.id);
- } else {
- return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
- }
- case textsecure.protobuf.GroupContext.Type.DELIVER:
- decrypted.group.name = null;
- decrypted.group.members = [];
- decrypted.group.avatar = null;
- break;
- default:
- throw new Error("Unknown group message type");
- }
- }
- }.bind(this)));
- }
- for (var i in decrypted.attachments) {
- promises.push(this.handleAttachment(decrypted.attachments[i]));
- }
- return Promise.all(promises).then(function() {
- return decrypted;
- });
- }
- });
- window.textsecure = window.textsecure || {};
- textsecure.MessageReceiver = function(url, ports, username, password, signalingKey) {
- var messageReceiver = new MessageReceiver(url, ports, username, password, signalingKey);
- this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver);
- this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
- this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
- this.close = messageReceiver.close.bind(messageReceiver);
- messageReceiver.connect();
- textsecure.replay.registerFunction(messageReceiver.tryMessageAgain.bind(messageReceiver), textsecure.replay.Type.INIT_SESSION);
- };
- textsecure.MessageReceiver.prototype = {
- constructor: textsecure.MessageReceiver
- };
|