outgoing_message.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /*
  2. * vim: ts=4:sw=4:expandtab
  3. */
  4. function OutgoingMessage(server, timestamp, numbers, message, callback) {
  5. this.server = server;
  6. this.timestamp = timestamp;
  7. this.numbers = numbers;
  8. this.message = message; // DataMessage or ContentMessage proto
  9. this.callback = callback;
  10. this.legacy = (message instanceof textsecure.protobuf.DataMessage);
  11. this.numbersCompleted = 0;
  12. this.errors = [];
  13. this.successfulNumbers = [];
  14. }
  15. OutgoingMessage.prototype = {
  16. constructor: OutgoingMessage,
  17. numberCompleted: function() {
  18. this.numbersCompleted++;
  19. if (this.numbersCompleted >= this.numbers.length) {
  20. this.callback({successfulNumbers: this.successfulNumbers, errors: this.errors});
  21. }
  22. },
  23. registerError: function(number, reason, error) {
  24. if (!error || error.name === 'HTTPError' && error.code !== 404) {
  25. error = new textsecure.OutgoingMessageError(number, this.message.toArrayBuffer(), this.timestamp, error);
  26. }
  27. error.number = number;
  28. error.reason = reason;
  29. this.errors[this.errors.length] = error;
  30. this.numberCompleted();
  31. },
  32. reloadDevicesAndSend: function(number, recurse) {
  33. return function() {
  34. return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
  35. if (deviceIds.length == 0) {
  36. return this.registerError(number, "Got empty device list when loading device keys", null);
  37. }
  38. return this.doSendMessage(number, deviceIds, recurse);
  39. }.bind(this));
  40. }.bind(this);
  41. },
  42. getKeysForNumber: function(number, updateDevices) {
  43. var handleResult = function(response) {
  44. return Promise.all(response.devices.map(function(device) {
  45. device.identityKey = response.identityKey;
  46. if (updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1) {
  47. var address = new libsignal.SignalProtocolAddress(number, device.deviceId);
  48. var builder = new libsignal.SessionBuilder(textsecure.storage.protocol, address);
  49. if (device.registrationId === 0) {
  50. console.log("device registrationId 0!");
  51. }
  52. return builder.processPreKey(device).catch(function(error) {
  53. if (error.message === "Identity key changed") {
  54. error = new textsecure.OutgoingIdentityKeyError(
  55. number, this.message.toArrayBuffer(),
  56. this.timestamp, device.identityKey);
  57. this.registerError(number, "Identity key changed", error);
  58. }
  59. throw error;
  60. }.bind(this));
  61. }
  62. }.bind(this)));
  63. }.bind(this);
  64. if (updateDevices === undefined) {
  65. return this.server.getKeysForNumber(number).then(handleResult);
  66. } else {
  67. var promise = Promise.resolve();
  68. updateDevices.forEach(function(device) {
  69. promise = promise.then(function() {
  70. return this.server.getKeysForNumber(number, device).then(handleResult).catch(function(e) {
  71. if (e.name === 'HTTPError' && e.code === 404) {
  72. if (device !== 1) return this.removeDeviceIdsForNumber(number, [device]);
  73. else throw new textsecure.UnregisteredUserError(number, e);
  74. } else {
  75. throw e;
  76. }
  77. }.bind(this));
  78. }.bind(this));
  79. }.bind(this));
  80. return promise;
  81. }
  82. },
  83. transmitMessage: function(number, jsonData, timestamp) {
  84. return this.server.sendMessages(number, jsonData, timestamp).catch(function(e) {
  85. if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
  86. // 409 and 410 should bubble and be handled by doSendMessage
  87. // 404 should throw UnregisteredUserError
  88. // all other network errors can be retried later.
  89. if (e.code === 404) {
  90. throw new textsecure.UnregisteredUserError(number, e);
  91. }
  92. throw new textsecure.SendMessageNetworkError(number, jsonData, e, timestamp);
  93. }
  94. throw e;
  95. });
  96. },
  97. getPaddedMessageLength: function(messageLength) {
  98. var messageLengthWithTerminator = messageLength + 1;
  99. var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
  100. if (messageLengthWithTerminator % 160 !== 0) {
  101. messagePartCount++;
  102. }
  103. return messagePartCount * 160;
  104. },
  105. doSendMessage: function(number, deviceIds, recurse) {
  106. var ciphers = {};
  107. var plaintext = this.message.toArrayBuffer();
  108. var paddedPlaintext = new Uint8Array(
  109. this.getPaddedMessageLength(plaintext.byteLength + 1) - 1
  110. );
  111. paddedPlaintext.set(new Uint8Array(plaintext));
  112. paddedPlaintext[plaintext.byteLength] = 0x80;
  113. return Promise.all(deviceIds.map(function(deviceId) {
  114. var address = new libsignal.SignalProtocolAddress(number, deviceId);
  115. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  116. ciphers[address.getDeviceId()] = sessionCipher;
  117. return this.encryptToDevice(address, paddedPlaintext, sessionCipher);
  118. }.bind(this))).then(function(jsonData) {
  119. return this.transmitMessage(number, jsonData, this.timestamp).then(function() {
  120. this.successfulNumbers[this.successfulNumbers.length] = number;
  121. this.numberCompleted();
  122. }.bind(this));
  123. }.bind(this)).catch(function(error) {
  124. if (error instanceof Error && error.name == "HTTPError" && (error.code == 410 || error.code == 409)) {
  125. if (!recurse)
  126. return this.registerError(number, "Hit retry limit attempting to reload device list", error);
  127. var p;
  128. if (error.code == 409) {
  129. p = this.removeDeviceIdsForNumber(number, error.response.extraDevices);
  130. } else {
  131. p = Promise.all(error.response.staleDevices.map(function(deviceId) {
  132. return ciphers[deviceId].closeOpenSessionForDevice();
  133. }));
  134. }
  135. return p.then(function() {
  136. var resetDevices = ((error.code == 410) ? error.response.staleDevices : error.response.missingDevices);
  137. return this.getKeysForNumber(number, resetDevices)
  138. .then(this.reloadDevicesAndSend(number, (error.code == 409)))
  139. .catch(function(error) {
  140. this.registerError(number, "Failed to reload device keys", error);
  141. }.bind(this));
  142. }.bind(this));
  143. } else {
  144. this.registerError(number, "Failed to create or send message", error);
  145. }
  146. }.bind(this));
  147. },
  148. encryptToDevice: function(address, plaintext, sessionCipher) {
  149. return sessionCipher.encrypt(plaintext).then(function(ciphertext) {
  150. return this.toJSON(address, ciphertext);
  151. }.bind(this));
  152. },
  153. toJSON: function(address, encryptedMsg) {
  154. var json = {
  155. type : encryptedMsg.type,
  156. destinationDeviceId : address.getDeviceId(),
  157. destinationRegistrationId : encryptedMsg.registrationId
  158. };
  159. var content = btoa(encryptedMsg.body);
  160. if (this.legacy) {
  161. json.body = content;
  162. } else {
  163. json.content = content;
  164. }
  165. return json;
  166. },
  167. getStaleDeviceIdsForNumber: function(number) {
  168. return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
  169. if (deviceIds.length === 0) {
  170. return [1];
  171. }
  172. var updateDevices = [];
  173. return Promise.all(deviceIds.map(function(deviceId) {
  174. var address = new libsignal.SignalProtocolAddress(number, deviceId);
  175. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  176. return sessionCipher.hasOpenSession().then(function(hasSession) {
  177. if (!hasSession) {
  178. updateDevices.push(deviceId);
  179. }
  180. });
  181. })).then(function() {
  182. return updateDevices;
  183. });
  184. });
  185. },
  186. removeDeviceIdsForNumber: function(number, deviceIdsToRemove) {
  187. var promise = Promise.resolve();
  188. for (var j in deviceIdsToRemove) {
  189. promise = promise.then(function() {
  190. var encodedNumber = number + "." + deviceIdsToRemove[j];
  191. return textsecure.storage.protocol.removeSession(encodedNumber);
  192. });
  193. }
  194. return promise;
  195. },
  196. sendToNumber: function(number) {
  197. return this.getStaleDeviceIdsForNumber(number).then(function(updateDevices) {
  198. return this.getKeysForNumber(number, updateDevices)
  199. .then(this.reloadDevicesAndSend(number, true))
  200. .catch(function(error) {
  201. this.registerError(number, "Failed to retreive new device keys for number " + number, error);
  202. }.bind(this));
  203. }.bind(this));
  204. }
  205. };