message_receiver.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. /*
  2. * vim: ts=4:sw=4:expandtab
  3. */
  4. function MessageReceiver(url, ports, username, password, signalingKey) {
  5. this.url = url;
  6. this.signalingKey = signalingKey;
  7. this.username = username;
  8. this.password = password;
  9. this.server = new TextSecureServer(url, ports, username, password);
  10. var address = libsignal.SignalProtocolAddress.fromString(username);
  11. this.number = address.getName();
  12. this.deviceId = address.getDeviceId();
  13. }
  14. MessageReceiver.prototype = new textsecure.EventTarget();
  15. MessageReceiver.prototype.extend({
  16. constructor: MessageReceiver,
  17. connect: function() {
  18. if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
  19. this.socket.close();
  20. }
  21. console.log('opening websocket');
  22. // initialize the socket and start listening for messages
  23. this.socket = this.server.getMessageSocket();
  24. this.socket.onclose = this.onclose.bind(this);
  25. this.socket.onerror = this.onerror.bind(this);
  26. this.socket.onopen = this.onopen.bind(this);
  27. this.wsr = new WebSocketResource(this.socket, {
  28. handleRequest: this.handleRequest.bind(this),
  29. keepalive: { path: '/v1/keepalive', disconnect: true }
  30. });
  31. this.pending = Promise.resolve();
  32. },
  33. close: function() {
  34. this.socket.close(3000, 'called close');
  35. delete this.listeners;
  36. },
  37. onopen: function() {
  38. console.log('websocket open');
  39. },
  40. onerror: function(error) {
  41. console.log('websocket error');
  42. },
  43. onclose: function(ev) {
  44. console.log('websocket closed', ev.code, ev.reason || '');
  45. if (ev.code === 3000) {
  46. return;
  47. }
  48. var eventTarget = this;
  49. // possible 403 or network issue. Make an request to confirm
  50. this.server.getDevices(this.number).
  51. then(this.connect.bind(this)). // No HTTP error? Reconnect
  52. catch(function(e) {
  53. var ev = new Event('error');
  54. ev.error = e;
  55. eventTarget.dispatchEvent(ev);
  56. });
  57. },
  58. handleRequest: function(request) {
  59. // We do the message decryption here, instead of in the ordered pending queue,
  60. // to avoid exposing the time it took us to process messages through the time-to-ack.
  61. // TODO: handle different types of requests. for now we blindly assume
  62. // PUT /messages <encrypted Envelope>
  63. textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).then(function(plaintext) {
  64. var envelope = textsecure.protobuf.Envelope.decode(plaintext);
  65. // After this point, decoding errors are not the server's
  66. // fault, and we should handle them gracefully and tell the
  67. // user they received an invalid message
  68. request.respond(200, 'OK');
  69. if (!this.isBlocked(envelope.source)) {
  70. this.queueEnvelope(envelope);
  71. }
  72. }.bind(this)).catch(function(e) {
  73. request.respond(500, 'Bad encrypted websocket message');
  74. console.log("Error handling incoming message:", e);
  75. var ev = new Event('error');
  76. ev.error = e;
  77. this.dispatchEvent(ev);
  78. }.bind(this));
  79. },
  80. queueEnvelope: function(envelope) {
  81. var handleEnvelope = this.handleEnvelope.bind(this, envelope);
  82. this.pending = this.pending.then(handleEnvelope, handleEnvelope);
  83. },
  84. handleEnvelope: function(envelope) {
  85. if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
  86. return this.onDeliveryReceipt(envelope);
  87. }
  88. if (envelope.content) {
  89. return this.handleContentMessage(envelope);
  90. } else if (envelope.legacyMessage) {
  91. return this.handleLegacyMessage(envelope);
  92. } else {
  93. throw new Error('Received message with no content and no legacyMessage');
  94. }
  95. },
  96. getStatus: function() {
  97. if (this.socket) {
  98. return this.socket.readyState;
  99. } else {
  100. return -1;
  101. }
  102. },
  103. onDeliveryReceipt: function (envelope) {
  104. var ev = new Event('receipt');
  105. ev.proto = envelope;
  106. this.dispatchEvent(ev);
  107. },
  108. unpad: function(paddedPlaintext) {
  109. paddedPlaintext = new Uint8Array(paddedPlaintext);
  110. var plaintext;
  111. for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
  112. if (paddedPlaintext[i] == 0x80) {
  113. plaintext = new Uint8Array(i);
  114. plaintext.set(paddedPlaintext.subarray(0, i));
  115. plaintext = plaintext.buffer;
  116. break;
  117. } else if (paddedPlaintext[i] !== 0x00) {
  118. throw new Error('Invalid padding');
  119. }
  120. }
  121. return plaintext;
  122. },
  123. decrypt: function(envelope, ciphertext) {
  124. var promise;
  125. var address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice);
  126. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  127. switch(envelope.type) {
  128. case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
  129. console.log('message from', envelope.source + '.' + envelope.sourceDevice, envelope.timestamp.toNumber());
  130. promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad);
  131. break;
  132. case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
  133. console.log('prekey message from', envelope.source + '.' + envelope.sourceDevice, envelope.timestamp.toNumber());
  134. promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address);
  135. break;
  136. default:
  137. promise = Promise.reject(new Error("Unknown message type"));
  138. }
  139. return promise.catch(function(error) {
  140. var ev = new Event('error');
  141. ev.error = error;
  142. ev.proto = envelope;
  143. this.dispatchEvent(ev);
  144. return Promise.reject(error);
  145. }.bind(this));
  146. },
  147. decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) {
  148. return sessionCipher.decryptPreKeyWhisperMessage(ciphertext).then(this.unpad).catch(function(e) {
  149. if (e.message === 'Unknown identity key') {
  150. // create an error that the UI will pick up and ask the
  151. // user if they want to re-negotiate
  152. var buffer = dcodeIO.ByteBuffer.wrap(ciphertext);
  153. throw new textsecure.IncomingIdentityKeyError(
  154. address.toString(),
  155. buffer.toArrayBuffer(),
  156. e.identityKey
  157. );
  158. }
  159. throw e;
  160. });
  161. },
  162. handleSentMessage: function(destination, timestamp, message, expirationStartTimestamp) {
  163. var p = Promise.resolve();
  164. if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
  165. textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
  166. p = this.handleEndSession(destination);
  167. }
  168. return p.then(function() {
  169. return this.processDecrypted(message, this.number).then(function(message) {
  170. var ev = new Event('sent');
  171. ev.data = {
  172. destination : destination,
  173. timestamp : timestamp.toNumber(),
  174. message : message
  175. };
  176. if (expirationStartTimestamp) {
  177. ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
  178. }
  179. this.dispatchEvent(ev);
  180. }.bind(this));
  181. }.bind(this));
  182. },
  183. handleDataMessage: function(envelope, message) {
  184. var encodedNumber = envelope.source + '.' + envelope.sourceDevice;
  185. console.log('data message from', encodedNumber, envelope.timestamp.toNumber());
  186. var p = Promise.resolve();
  187. if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) ==
  188. textsecure.protobuf.DataMessage.Flags.END_SESSION ) {
  189. p = this.handleEndSession(envelope.source);
  190. }
  191. return p.then(function() {
  192. return this.processDecrypted(message, envelope.source).then(function(message) {
  193. var ev = new Event('message');
  194. ev.data = {
  195. source : envelope.source,
  196. timestamp : envelope.timestamp.toNumber(),
  197. message : message
  198. };
  199. this.dispatchEvent(ev);
  200. }.bind(this));
  201. }.bind(this));
  202. },
  203. handleLegacyMessage: function (envelope) {
  204. return this.decrypt(envelope, envelope.legacyMessage).then(function(plaintext) {
  205. var message = textsecure.protobuf.DataMessage.decode(plaintext);
  206. return this.handleDataMessage(envelope, message);
  207. }.bind(this));
  208. },
  209. handleContentMessage: function (envelope) {
  210. return this.decrypt(envelope, envelope.content).then(function(plaintext) {
  211. var content = textsecure.protobuf.Content.decode(plaintext);
  212. if (content.syncMessage) {
  213. return this.handleSyncMessage(envelope, content.syncMessage);
  214. } else if (content.dataMessage) {
  215. return this.handleDataMessage(envelope, content.dataMessage);
  216. } else {
  217. throw new Error('Got Content message with no dataMessage and no syncMessage');
  218. }
  219. }.bind(this));
  220. },
  221. handleSyncMessage: function(envelope, syncMessage) {
  222. if (envelope.source !== this.number) {
  223. throw new Error('Received sync message from another number');
  224. }
  225. if (envelope.sourceDevice == this.deviceId) {
  226. throw new Error('Received sync message from our own device');
  227. }
  228. if (syncMessage.sent) {
  229. var sentMessage = syncMessage.sent;
  230. console.log('sent message to',
  231. sentMessage.destination,
  232. sentMessage.timestamp.toNumber(),
  233. 'from', envelope.source + '.' + envelope.sourceDevice
  234. );
  235. return this.handleSentMessage(
  236. sentMessage.destination,
  237. sentMessage.timestamp,
  238. sentMessage.message,
  239. sentMessage.expirationStartTimestamp
  240. );
  241. } else if (syncMessage.contacts) {
  242. this.handleContacts(syncMessage.contacts);
  243. } else if (syncMessage.groups) {
  244. this.handleGroups(syncMessage.groups);
  245. } else if (syncMessage.blocked) {
  246. this.handleBlocked(syncMessage.blocked);
  247. } else if (syncMessage.request) {
  248. console.log('Got SyncMessage Request');
  249. } else if (syncMessage.read) {
  250. console.log('read messages',
  251. 'from', envelope.source + '.' + envelope.sourceDevice);
  252. this.handleRead(syncMessage.read, envelope.timestamp);
  253. } else {
  254. throw new Error('Got empty SyncMessage');
  255. }
  256. },
  257. handleRead: function(read, timestamp) {
  258. for (var i = 0; i < read.length; ++i) {
  259. var ev = new Event('read');
  260. ev.timestamp = timestamp.toNumber();
  261. ev.read = {
  262. timestamp : read[i].timestamp.toNumber(),
  263. sender : read[i].sender
  264. }
  265. this.dispatchEvent(ev);
  266. }
  267. },
  268. handleContacts: function(contacts) {
  269. console.log('contact sync');
  270. var eventTarget = this;
  271. var attachmentPointer = contacts.blob;
  272. return this.handleAttachment(attachmentPointer).then(function() {
  273. var contactBuffer = new ContactBuffer(attachmentPointer.data);
  274. var contactDetails = contactBuffer.next();
  275. while (contactDetails !== undefined) {
  276. var ev = new Event('contact');
  277. ev.contactDetails = contactDetails;
  278. eventTarget.dispatchEvent(ev);
  279. contactDetails = contactBuffer.next();
  280. }
  281. eventTarget.dispatchEvent(new Event('contactsync'));
  282. });
  283. },
  284. handleGroups: function(groups) {
  285. console.log('group sync');
  286. var eventTarget = this;
  287. var attachmentPointer = groups.blob;
  288. return this.handleAttachment(attachmentPointer).then(function() {
  289. var groupBuffer = new GroupBuffer(attachmentPointer.data);
  290. var groupDetails = groupBuffer.next();
  291. var promises = [];
  292. while (groupDetails !== undefined) {
  293. var promise = (function(groupDetails) {
  294. groupDetails.id = groupDetails.id.toBinary();
  295. if (groupDetails.active) {
  296. return textsecure.storage.groups.getGroup(groupDetails.id).
  297. then(function(existingGroup) {
  298. if (existingGroup === undefined) {
  299. return textsecure.storage.groups.createNewGroup(
  300. groupDetails.members, groupDetails.id
  301. );
  302. } else {
  303. return textsecure.storage.groups.updateNumbers(
  304. groupDetails.id, groupDetails.members
  305. );
  306. }
  307. }).then(function() { return groupDetails });
  308. } else {
  309. return Promise.resolve(groupDetails);
  310. }
  311. })(groupDetails).then(function(groupDetails) {
  312. var ev = new Event('group');
  313. ev.groupDetails = groupDetails;
  314. eventTarget.dispatchEvent(ev);
  315. }).catch(function(e) {
  316. console.log('error processing group', e);
  317. });
  318. groupDetails = groupBuffer.next();
  319. promises.push(promise);
  320. }
  321. Promise.all(promises).then(function() {
  322. eventTarget.dispatchEvent(new Event('groupsync'));
  323. });
  324. });
  325. },
  326. handleBlocked: function(blocked) {
  327. textsecure.storage.put('blocked', blocked.numbers);
  328. },
  329. isBlocked: function(number) {
  330. return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
  331. },
  332. handleAttachment: function(attachment) {
  333. var digest = attachment.digest ? attachment.digest.toArrayBuffer() : undefined;
  334. function decryptAttachment(encrypted) {
  335. return textsecure.crypto.decryptAttachment(
  336. encrypted,
  337. attachment.key.toArrayBuffer(),
  338. digest
  339. );
  340. }
  341. function updateAttachment(data) {
  342. attachment.data = data;
  343. }
  344. return this.server.getAttachment(attachment.id.toString()).
  345. then(decryptAttachment).
  346. then(updateAttachment);
  347. },
  348. tryMessageAgain: function(from, ciphertext) {
  349. var address = libsignal.SignalProtocolAddress.fromString(from);
  350. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  351. console.log('retrying prekey whisper message');
  352. return this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address).then(function(plaintext) {
  353. var finalMessage = textsecure.protobuf.DataMessage.decode(plaintext);
  354. var p = Promise.resolve();
  355. if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION)
  356. == textsecure.protobuf.DataMessage.Flags.END_SESSION &&
  357. finalMessage.sync !== null) {
  358. var number = address.getName();
  359. p = this.handleEndSession(number);
  360. }
  361. return p.then(function() {
  362. return this.processDecrypted(finalMessage);
  363. }.bind(this));
  364. }.bind(this));
  365. },
  366. handleEndSession: function(number) {
  367. console.log('got end session');
  368. return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
  369. return Promise.all(deviceIds.map(function(deviceId) {
  370. var address = new libsignal.SignalProtocolAddress(number, deviceId);
  371. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  372. console.log('closing session for', address.toString());
  373. return sessionCipher.closeOpenSessionForDevice();
  374. }));
  375. });
  376. },
  377. processDecrypted: function(decrypted, source) {
  378. // Now that its decrypted, validate the message and clean it up for consumer processing
  379. // Note that messages may (generally) only perform one action and we ignore remaining fields
  380. // after the first action.
  381. if (decrypted.flags == null) {
  382. decrypted.flags = 0;
  383. }
  384. if (decrypted.expireTimer == null) {
  385. decrypted.expireTimer = 0;
  386. }
  387. if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
  388. decrypted.body = null;
  389. decrypted.attachments = [];
  390. decrypted.group = null;
  391. return Promise.resolve(decrypted);
  392. } else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) {
  393. decrypted.body = null;
  394. decrypted.attachments = [];
  395. } else if (decrypted.flags != 0) {
  396. throw new Error("Unknown flags in message");
  397. }
  398. var promises = [];
  399. if (decrypted.group !== null) {
  400. decrypted.group.id = decrypted.group.id.toBinary();
  401. if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) {
  402. if (decrypted.group.avatar !== null) {
  403. promises.push(this.handleAttachment(decrypted.group.avatar));
  404. }
  405. }
  406. promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) {
  407. if (existingGroup === undefined) {
  408. if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) {
  409. decrypted.group.members = [source];
  410. console.log("Got message for unknown group");
  411. }
  412. return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id);
  413. } else {
  414. var fromIndex = existingGroup.indexOf(source);
  415. if (fromIndex < 0) {
  416. //TODO: This could be indication of a race...
  417. console.log("Sender was not a member of the group they were sending from");
  418. }
  419. switch(decrypted.group.type) {
  420. case textsecure.protobuf.GroupContext.Type.UPDATE:
  421. decrypted.body = null;
  422. decrypted.attachments = [];
  423. return textsecure.storage.groups.updateNumbers(
  424. decrypted.group.id, decrypted.group.members
  425. );
  426. break;
  427. case textsecure.protobuf.GroupContext.Type.QUIT:
  428. decrypted.body = null;
  429. decrypted.attachments = [];
  430. if (source === this.number) {
  431. return textsecure.storage.groups.deleteGroup(decrypted.group.id);
  432. } else {
  433. return textsecure.storage.groups.removeNumber(decrypted.group.id, source);
  434. }
  435. case textsecure.protobuf.GroupContext.Type.DELIVER:
  436. decrypted.group.name = null;
  437. decrypted.group.members = [];
  438. decrypted.group.avatar = null;
  439. break;
  440. default:
  441. throw new Error("Unknown group message type");
  442. }
  443. }
  444. }.bind(this)));
  445. }
  446. for (var i in decrypted.attachments) {
  447. promises.push(this.handleAttachment(decrypted.attachments[i]));
  448. }
  449. return Promise.all(promises).then(function() {
  450. return decrypted;
  451. });
  452. }
  453. });
  454. window.textsecure = window.textsecure || {};
  455. textsecure.MessageReceiver = function(url, ports, username, password, signalingKey) {
  456. var messageReceiver = new MessageReceiver(url, ports, username, password, signalingKey);
  457. this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver);
  458. this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
  459. this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
  460. this.close = messageReceiver.close.bind(messageReceiver);
  461. messageReceiver.connect();
  462. textsecure.replay.registerFunction(messageReceiver.tryMessageAgain.bind(messageReceiver), textsecure.replay.Type.INIT_SESSION);
  463. };
  464. textsecure.MessageReceiver.prototype = {
  465. constructor: textsecure.MessageReceiver
  466. };