sendmessage.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. /*
  2. * vim: ts=4:sw=4:expandtab
  3. */
  4. function stringToArrayBuffer(str) {
  5. if (typeof str !== 'string') {
  6. throw new Error('Passed non-string to stringToArrayBuffer');
  7. }
  8. var res = new ArrayBuffer(str.length);
  9. var uint = new Uint8Array(res);
  10. for (var i = 0; i < str.length; i++) {
  11. uint[i] = str.charCodeAt(i);
  12. }
  13. return res;
  14. }
  15. function Message(options) {
  16. this.body = options.body;
  17. this.attachments = options.attachments || [];
  18. this.group = options.group;
  19. this.flags = options.flags;
  20. this.recipients = options.recipients;
  21. this.timestamp = options.timestamp;
  22. this.needsSync = options.needsSync;
  23. this.expireTimer = options.expireTimer;
  24. if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
  25. throw new Error('Invalid recipient list');
  26. }
  27. if (!this.group && this.recipients.length > 1) {
  28. throw new Error('Invalid recipient list for non-group');
  29. }
  30. if (typeof this.timestamp !== 'number') {
  31. throw new Error('Invalid timestamp');
  32. }
  33. if (this.expireTimer !== undefined && this.expireTimer !== null) {
  34. if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) {
  35. throw new Error('Invalid expireTimer');
  36. }
  37. }
  38. if (this.attachments) {
  39. if (!(this.attachments instanceof Array)) {
  40. throw new Error('Invalid message attachments');
  41. }
  42. }
  43. if (this.flags !== undefined) {
  44. if (typeof this.flags !== 'number') {
  45. throw new Error('Invalid message flags');
  46. }
  47. }
  48. if (this.isEndSession()) {
  49. if (this.body !== null || this.group !== null || this.attachments.length !== 0) {
  50. throw new Error('Invalid end session message');
  51. }
  52. } else {
  53. if ( (typeof this.timestamp !== 'number') ||
  54. (this.body && typeof this.body !== 'string') ) {
  55. throw new Error('Invalid message body');
  56. }
  57. if (this.group) {
  58. if ( (typeof this.group.id !== 'string') ||
  59. (typeof this.group.type !== 'number') ) {
  60. throw new Error('Invalid group context');
  61. }
  62. }
  63. }
  64. }
  65. Message.prototype = {
  66. constructor: Message,
  67. isEndSession: function() {
  68. return (this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION);
  69. },
  70. toProto: function() {
  71. if (this.dataMessage instanceof textsecure.protobuf.DataMessage) {
  72. return this.dataMessage;
  73. }
  74. var proto = new textsecure.protobuf.DataMessage();
  75. if (this.body) {
  76. proto.body = this.body;
  77. }
  78. proto.attachments = this.attachmentPointers;
  79. if (this.flags) {
  80. proto.flags = this.flags;
  81. }
  82. if (this.group) {
  83. proto.group = new textsecure.protobuf.GroupContext();
  84. proto.group.id = stringToArrayBuffer(this.group.id);
  85. proto.group.type = this.group.type
  86. }
  87. if (this.expireTimer) {
  88. proto.expireTimer = this.expireTimer;
  89. }
  90. this.dataMessage = proto;
  91. return proto;
  92. },
  93. toArrayBuffer: function() {
  94. return this.toProto().toArrayBuffer();
  95. }
  96. };
  97. function MessageSender(url, ports, username, password) {
  98. this.server = new TextSecureServer(url, ports, username, password);
  99. this.pendingMessages = {};
  100. }
  101. MessageSender.prototype = {
  102. constructor: MessageSender,
  103. makeAttachmentPointer: function(attachment) {
  104. if (typeof attachment !== 'object' || attachment == null) {
  105. return Promise.resolve(undefined);
  106. }
  107. var proto = new textsecure.protobuf.AttachmentPointer();
  108. proto.key = libsignal.crypto.getRandomBytes(64);
  109. var iv = libsignal.crypto.getRandomBytes(16);
  110. return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(result) {
  111. return this.server.putAttachment(result.ciphertext).then(function(id) {
  112. proto.id = id;
  113. proto.contentType = attachment.contentType;
  114. proto.digest = result.digest;
  115. return proto;
  116. });
  117. }.bind(this));
  118. },
  119. retransmitMessage: function(number, jsonData, timestamp) {
  120. var outgoing = new OutgoingMessage(this.server);
  121. return outgoing.transmitMessage(number, jsonData, timestamp);
  122. },
  123. tryMessageAgain: function(number, encodedMessage, timestamp) {
  124. var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
  125. return this.sendIndividualProto(number, proto, timestamp);
  126. },
  127. queueJobForNumber: function(number, runJob) {
  128. var runPrevious = this.pendingMessages[number] || Promise.resolve();
  129. var runCurrent = this.pendingMessages[number] = runPrevious.then(runJob, runJob);
  130. runCurrent.then(function() {
  131. if (this.pendingMessages[number] === runCurrent) {
  132. delete this.pendingMessages[number];
  133. }
  134. }.bind(this));
  135. },
  136. uploadMedia: function(message) {
  137. return Promise.all(
  138. message.attachments.map(this.makeAttachmentPointer.bind(this))
  139. ).then(function(attachmentPointers) {
  140. message.attachmentPointers = attachmentPointers;
  141. }).catch(function(error) {
  142. if (error instanceof Error && error.name === 'HTTPError') {
  143. throw new textsecure.MessageError(message, error);
  144. } else {
  145. throw error;
  146. }
  147. });
  148. },
  149. sendMessage: function(attrs) {
  150. var message = new Message(attrs);
  151. return this.uploadMedia(message).then(function() {
  152. return new Promise(function(resolve, reject) {
  153. this.sendMessageProto(
  154. message.timestamp,
  155. message.recipients,
  156. message.toProto(),
  157. function(res) {
  158. res.dataMessage = message.toArrayBuffer();
  159. if (res.errors.length > 0) {
  160. reject(res);
  161. } else {
  162. resolve(res);
  163. }
  164. }
  165. );
  166. }.bind(this));
  167. }.bind(this));
  168. },
  169. sendMessageProto: function(timestamp, numbers, message, callback) {
  170. var rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
  171. if (rejections > 5) {
  172. throw new textsecure.SignedPreKeyRotationError(numbers, message.toArrayBuffer(), timestamp);
  173. }
  174. var outgoing = new OutgoingMessage(this.server, timestamp, numbers, message, callback);
  175. numbers.forEach(function(number) {
  176. this.queueJobForNumber(number, function() {
  177. return outgoing.sendToNumber(number);
  178. });
  179. }.bind(this));
  180. },
  181. retrySendMessageProto: function(numbers, encodedMessage, timestamp) {
  182. var proto = textsecure.protobuf.DataMessage.decode(encodedMessage);
  183. return new Promise(function(resolve, reject) {
  184. this.sendMessageProto(timestamp, numbers, proto, function(res) {
  185. if (res.errors.length > 0)
  186. reject(res);
  187. else
  188. resolve(res);
  189. });
  190. }.bind(this));
  191. },
  192. sendIndividualProto: function(number, proto, timestamp) {
  193. return new Promise(function(resolve, reject) {
  194. this.sendMessageProto(timestamp, [number], proto, function(res) {
  195. if (res.errors.length > 0)
  196. reject(res);
  197. else
  198. resolve(res);
  199. });
  200. }.bind(this));
  201. },
  202. sendSyncMessage: function(encodedDataMessage, timestamp, destination, expirationStartTimestamp) {
  203. var myNumber = textsecure.storage.user.getNumber();
  204. var myDevice = textsecure.storage.user.getDeviceId();
  205. if (myDevice == 1) {
  206. return Promise.resolve();
  207. }
  208. var dataMessage = textsecure.protobuf.DataMessage.decode(encodedDataMessage);
  209. var sentMessage = new textsecure.protobuf.SyncMessage.Sent();
  210. sentMessage.timestamp = timestamp;
  211. sentMessage.message = dataMessage;
  212. if (destination) {
  213. sentMessage.destination = destination;
  214. }
  215. if (expirationStartTimestamp) {
  216. sentMessage.expirationStartTimestamp = expirationStartTimestamp;
  217. }
  218. var syncMessage = new textsecure.protobuf.SyncMessage();
  219. syncMessage.sent = sentMessage;
  220. var contentMessage = new textsecure.protobuf.Content();
  221. contentMessage.syncMessage = syncMessage;
  222. return this.sendIndividualProto(myNumber, contentMessage, Date.now());
  223. },
  224. sendRequestGroupSyncMessage: function() {
  225. var myNumber = textsecure.storage.user.getNumber();
  226. var myDevice = textsecure.storage.user.getDeviceId();
  227. if (myDevice != 1) {
  228. var request = new textsecure.protobuf.SyncMessage.Request();
  229. request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
  230. var syncMessage = new textsecure.protobuf.SyncMessage();
  231. syncMessage.request = request;
  232. var contentMessage = new textsecure.protobuf.Content();
  233. contentMessage.syncMessage = syncMessage;
  234. return this.sendIndividualProto(myNumber, contentMessage, Date.now());
  235. }
  236. },
  237. sendRequestContactSyncMessage: function() {
  238. var myNumber = textsecure.storage.user.getNumber();
  239. var myDevice = textsecure.storage.user.getDeviceId();
  240. if (myDevice != 1) {
  241. var request = new textsecure.protobuf.SyncMessage.Request();
  242. request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
  243. var syncMessage = new textsecure.protobuf.SyncMessage();
  244. syncMessage.request = request;
  245. var contentMessage = new textsecure.protobuf.Content();
  246. contentMessage.syncMessage = syncMessage;
  247. return this.sendIndividualProto(myNumber, contentMessage, Date.now());
  248. }
  249. },
  250. syncReadMessages: function(reads) {
  251. var myNumber = textsecure.storage.user.getNumber();
  252. var myDevice = textsecure.storage.user.getDeviceId();
  253. if (myDevice != 1) {
  254. var syncMessage = new textsecure.protobuf.SyncMessage();
  255. syncMessage.read = [];
  256. for (var i = 0; i < reads.length; ++i) {
  257. var read = new textsecure.protobuf.SyncMessage.Read();
  258. read.timestamp = reads[i].timestamp;
  259. read.sender = reads[i].sender;
  260. syncMessage.read.push(read);
  261. }
  262. var contentMessage = new textsecure.protobuf.Content();
  263. contentMessage.syncMessage = syncMessage;
  264. return this.sendIndividualProto(myNumber, contentMessage, Date.now());
  265. }
  266. },
  267. sendGroupProto: function(numbers, proto, timestamp) {
  268. timestamp = timestamp || Date.now();
  269. var me = textsecure.storage.user.getNumber();
  270. numbers = numbers.filter(function(number) { return number != me; });
  271. if (numbers.length === 0) {
  272. return Promise.reject(new Error('No other members in the group'));
  273. }
  274. return new Promise(function(resolve, reject) {
  275. this.sendMessageProto(timestamp, numbers, proto, function(res) {
  276. res.dataMessage = proto.toArrayBuffer();
  277. if (res.errors.length > 0)
  278. reject(res);
  279. else
  280. resolve(res);
  281. }.bind(this));
  282. }.bind(this));
  283. },
  284. sendMessageToNumber: function(number, messageText, attachments, timestamp, expireTimer) {
  285. return this.sendMessage({
  286. recipients : [number],
  287. body : messageText,
  288. timestamp : timestamp,
  289. attachments : attachments,
  290. needsSync : true,
  291. expireTimer : expireTimer
  292. });
  293. },
  294. closeSession: function(number, timestamp) {
  295. console.log('sending end session');
  296. var proto = new textsecure.protobuf.DataMessage();
  297. proto.body = "TERMINATE";
  298. proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION;
  299. return this.sendIndividualProto(number, proto, timestamp).then(function(res) {
  300. return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) {
  301. return Promise.all(deviceIds.map(function(deviceId) {
  302. var address = new libsignal.SignalProtocolAddress(number, deviceId);
  303. console.log('closing session for', address.toString());
  304. var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
  305. return sessionCipher.closeOpenSessionForDevice();
  306. })).then(function() {
  307. return res;
  308. });
  309. });
  310. });
  311. },
  312. sendMessageToGroup: function(groupId, messageText, attachments, timestamp, expireTimer) {
  313. return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
  314. if (numbers === undefined)
  315. return Promise.reject(new Error("Unknown Group"));
  316. var me = textsecure.storage.user.getNumber();
  317. numbers = numbers.filter(function(number) { return number != me; });
  318. if (numbers.length === 0) {
  319. return Promise.reject(new Error('No other members in the group'));
  320. }
  321. return this.sendMessage({
  322. recipients : numbers,
  323. body : messageText,
  324. timestamp : timestamp,
  325. attachments : attachments,
  326. needsSync : true,
  327. expireTimer : expireTimer,
  328. group: {
  329. id: groupId,
  330. type: textsecure.protobuf.GroupContext.Type.DELIVER
  331. }
  332. });
  333. }.bind(this));
  334. },
  335. createGroup: function(numbers, name, avatar) {
  336. var proto = new textsecure.protobuf.DataMessage();
  337. proto.group = new textsecure.protobuf.GroupContext();
  338. return textsecure.storage.groups.createNewGroup(numbers).then(function(group) {
  339. proto.group.id = stringToArrayBuffer(group.id);
  340. var numbers = group.numbers;
  341. proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
  342. proto.group.members = numbers;
  343. proto.group.name = name;
  344. return this.makeAttachmentPointer(avatar).then(function(attachment) {
  345. proto.group.avatar = attachment;
  346. return this.sendGroupProto(numbers, proto).then(function() {
  347. return proto.group.id;
  348. });
  349. }.bind(this));
  350. }.bind(this));
  351. },
  352. updateGroup: function(groupId, name, avatar, numbers) {
  353. var proto = new textsecure.protobuf.DataMessage();
  354. proto.group = new textsecure.protobuf.GroupContext();
  355. proto.group.id = stringToArrayBuffer(groupId);
  356. proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
  357. proto.group.name = name;
  358. return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) {
  359. if (numbers === undefined) {
  360. return Promise.reject(new Error("Unknown Group"));
  361. }
  362. proto.group.members = numbers;
  363. return this.makeAttachmentPointer(avatar).then(function(attachment) {
  364. proto.group.avatar = attachment;
  365. return this.sendGroupProto(numbers, proto).then(function() {
  366. return proto.group.id;
  367. });
  368. }.bind(this));
  369. }.bind(this));
  370. },
  371. addNumberToGroup: function(groupId, number) {
  372. var proto = new textsecure.protobuf.DataMessage();
  373. proto.group = new textsecure.protobuf.GroupContext();
  374. proto.group.id = stringToArrayBuffer(groupId);
  375. proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
  376. return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) {
  377. if (numbers === undefined)
  378. return Promise.reject(new Error("Unknown Group"));
  379. proto.group.members = numbers;
  380. return this.sendGroupProto(numbers, proto);
  381. }.bind(this));
  382. },
  383. setGroupName: function(groupId, name) {
  384. var proto = new textsecure.protobuf.DataMessage();
  385. proto.group = new textsecure.protobuf.GroupContext();
  386. proto.group.id = stringToArrayBuffer(groupId);
  387. proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
  388. proto.group.name = name;
  389. return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
  390. if (numbers === undefined)
  391. return Promise.reject(new Error("Unknown Group"));
  392. proto.group.members = numbers;
  393. return this.sendGroupProto(numbers, proto);
  394. }.bind(this));
  395. },
  396. setGroupAvatar: function(groupId, avatar) {
  397. var proto = new textsecure.protobuf.DataMessage();
  398. proto.group = new textsecure.protobuf.GroupContext();
  399. proto.group.id = stringToArrayBuffer(groupId);
  400. proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
  401. return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
  402. if (numbers === undefined)
  403. return Promise.reject(new Error("Unknown Group"));
  404. proto.group.members = numbers;
  405. return this.makeAttachmentPointer(avatar).then(function(attachment) {
  406. proto.group.avatar = attachment;
  407. return this.sendGroupProto(numbers, proto);
  408. }.bind(this));
  409. }.bind(this));
  410. },
  411. leaveGroup: function(groupId) {
  412. var proto = new textsecure.protobuf.DataMessage();
  413. proto.group = new textsecure.protobuf.GroupContext();
  414. proto.group.id = stringToArrayBuffer(groupId);
  415. proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
  416. return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
  417. if (numbers === undefined)
  418. return Promise.reject(new Error("Unknown Group"));
  419. return textsecure.storage.groups.deleteGroup(groupId).then(function() {
  420. return this.sendGroupProto(numbers, proto);
  421. }.bind(this));
  422. });
  423. },
  424. sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp) {
  425. return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
  426. if (numbers === undefined)
  427. return Promise.reject(new Error("Unknown Group"));
  428. var me = textsecure.storage.user.getNumber();
  429. numbers = numbers.filter(function(number) { return number != me; });
  430. if (numbers.length === 0) {
  431. return Promise.reject(new Error('No other members in the group'));
  432. }
  433. return this.sendMessage({
  434. recipients : numbers,
  435. timestamp : timestamp,
  436. needsSync : true,
  437. expireTimer : expireTimer,
  438. flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
  439. group: {
  440. id: groupId,
  441. type: textsecure.protobuf.GroupContext.Type.DELIVER
  442. }
  443. });
  444. }.bind(this));
  445. },
  446. sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp) {
  447. var proto = new textsecure.protobuf.DataMessage();
  448. return this.sendMessage({
  449. recipients : [number],
  450. timestamp : timestamp,
  451. needsSync : true,
  452. expireTimer : expireTimer,
  453. flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
  454. });
  455. }
  456. };
  457. window.textsecure = window.textsecure || {};
  458. textsecure.MessageSender = function(url, ports, username, password) {
  459. var sender = new MessageSender(url, ports, username, password);
  460. textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE);
  461. textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE);
  462. textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE);
  463. textsecure.replay.registerFunction(sender.retrySendMessageProto.bind(sender), textsecure.replay.Type.RETRY_SEND_MESSAGE_PROTO);
  464. this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(sender);
  465. this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup .bind(sender);
  466. this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage .bind(sender);
  467. this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage .bind(sender);
  468. this.sendMessageToNumber = sender.sendMessageToNumber .bind(sender);
  469. this.closeSession = sender.closeSession .bind(sender);
  470. this.sendMessageToGroup = sender.sendMessageToGroup .bind(sender);
  471. this.createGroup = sender.createGroup .bind(sender);
  472. this.updateGroup = sender.updateGroup .bind(sender);
  473. this.addNumberToGroup = sender.addNumberToGroup .bind(sender);
  474. this.setGroupName = sender.setGroupName .bind(sender);
  475. this.setGroupAvatar = sender.setGroupAvatar .bind(sender);
  476. this.leaveGroup = sender.leaveGroup .bind(sender);
  477. this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
  478. this.syncReadMessages = sender.syncReadMessages .bind(sender);
  479. };
  480. textsecure.MessageSender.prototype = {
  481. constructor: textsecure.MessageSender
  482. };