omemo.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. (function (root, factory) {
  2. define(["jasmine", "mock", "test-utils"], factory);
  3. } (this, function (jasmine, mock, test_utils) {
  4. var Strophe = converse.env.Strophe;
  5. var b64_sha1 = converse.env.b64_sha1;
  6. var $iq = converse.env.$iq;
  7. var $msg = converse.env.$msg;
  8. var _ = converse.env._;
  9. var u = converse.env.utils;
  10. describe("The OMEMO module", function() {
  11. it("adds methods for encrypting and decrypting messages via AES GCM",
  12. mock.initConverseWithPromises(
  13. null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  14. function (done, _converse) {
  15. let iq_stanza, view, sent_stanza;
  16. test_utils.createContacts(_converse, 'current', 1);
  17. _converse.emit('rosterContactsFetched');
  18. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  19. test_utils.openChatBoxFor(_converse, contact_jid)
  20. .then((view) => view.model.encryptMessage('This message will be encrypted'))
  21. .then((payload) => {
  22. debugger;
  23. return view.model.decryptMessage(payload);
  24. }).then(done);
  25. }));
  26. it("enables encrypted messages to be sent and received",
  27. mock.initConverseWithPromises(
  28. null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  29. function (done, _converse) {
  30. let iq_stanza, view, sent_stanza;
  31. test_utils.createContacts(_converse, 'current', 1);
  32. _converse.emit('rosterContactsFetched');
  33. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  34. // First, fetch own device list
  35. return test_utils.waitUntil(() => {
  36. return _.filter(
  37. _converse.connection.IQ_stanzas,
  38. (iq) => {
  39. const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  40. if (node) { iq_stanza = iq.nodeTree;}
  41. return node;
  42. }).length;
  43. }).then(() => {
  44. const stanza = $iq({
  45. 'from': contact_jid,
  46. 'id': iq_stanza.getAttribute('id'),
  47. 'to': _converse.bare_jid,
  48. 'type': 'result',
  49. }).c('query', {
  50. 'xmlns': 'http://jabber.org/protocol/disco#items',
  51. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  52. }).c('device', {'id': '482886413b977930064a5888b92134fe'}).up()
  53. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  54. _converse.emit('OMEMOInitialized');
  55. // Check that device list for contact is fetched when chat is opened.
  56. return test_utils.openChatBoxFor(_converse, contact_jid);
  57. }).then(() => {
  58. return test_utils.waitUntil(() => {
  59. return _.filter(
  60. _converse.connection.IQ_stanzas,
  61. (iq) => {
  62. const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  63. if (node) { iq_stanza = iq.nodeTree; }
  64. return node;
  65. }).length;
  66. });
  67. }).then(() => {
  68. const stanza = $iq({
  69. 'from': contact_jid,
  70. 'id': iq_stanza.getAttribute('id'),
  71. 'to': _converse.bare_jid,
  72. 'type': 'result',
  73. }).c('query', {
  74. 'xmlns': 'http://jabber.org/protocol/disco#items',
  75. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  76. }).c('device', {'id': '555'}).up()
  77. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  78. const devicelist = _converse.devicelists.create({'jid': contact_jid});
  79. expect(devicelist.devices.length).toBe(1);
  80. view = _converse.chatboxviews.get(contact_jid);
  81. view.model.set('omemo_active', true);
  82. const textarea = view.el.querySelector('.chat-textarea');
  83. textarea.value = 'This message will be encrypted';
  84. view.keyPressed({
  85. target: textarea,
  86. preventDefault: _.noop,
  87. keyCode: 13 // Enter
  88. });
  89. return test_utils.waitUntil(() => {
  90. return _.filter(
  91. _converse.connection.IQ_stanzas,
  92. (iq) => {
  93. const node = iq.nodeTree.querySelector(
  94. 'iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]'
  95. );
  96. if (node) { iq_stanza = iq.nodeTree; }
  97. return node;
  98. }).length;
  99. });
  100. }).then(() => {
  101. const stanza = $iq({
  102. 'from': contact_jid,
  103. 'id': iq_stanza.getAttribute('id'),
  104. 'to': _converse.bare_jid,
  105. 'type': 'result',
  106. }).c('pubsub', {
  107. 'xmlns': 'http://jabber.org/protocol/pubsub'
  108. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
  109. .c('item')
  110. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  111. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  112. .c('signedPreKeySignature').t(btoa('2222')).up()
  113. .c('identityKey').t(btoa('3333')).up()
  114. .c('prekeys')
  115. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  116. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  117. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  118. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  119. return test_utils.waitUntil(() => {
  120. return _.filter(
  121. _converse.connection.IQ_stanzas,
  122. (iq) => {
  123. const node = iq.nodeTree.querySelector(
  124. 'iq[to="'+_converse.bare_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"]'
  125. );
  126. if (node) { iq_stanza = iq.nodeTree; }
  127. return node;
  128. }).length;
  129. });
  130. }).then(() => {
  131. const stanza = $iq({
  132. 'from': _converse.bare_jid,
  133. 'id': iq_stanza.getAttribute('id'),
  134. 'to': _converse.bare_jid,
  135. 'type': 'result',
  136. }).c('pubsub', {
  137. 'xmlns': 'http://jabber.org/protocol/pubsub'
  138. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
  139. .c('item')
  140. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  141. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
  142. .c('signedPreKeySignature').t(btoa('200000')).up()
  143. .c('identityKey').t(btoa('300000')).up()
  144. .c('prekeys')
  145. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
  146. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
  147. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
  148. spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza });
  149. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  150. return test_utils.waitUntil(() => sent_stanza);
  151. }).then(() => {
  152. expect(sent_stanza.toLocaleString()).toBe(
  153. `<message from='dummy@localhost/resource' to='max.frankfurter@localhost' `+
  154. `type='chat' id='${sent_stanza.nodeTree.getAttribute('id')}' xmlns='jabber:client'>`+
  155. `<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
  156. `<encrypted xmlns='eu.siacs.conversations.axolotl'>`+
  157. `<header sid='123456789'>`+
  158. `<key rid='482886413b977930064a5888b92134fe'>eyJ0eXBlIjoxLCJib2R5IjoiYzFwaDNSNzNYNyIsInJlZ2lzdHJhdGlvbklkIjoiMTMzNyJ9</key>`+
  159. `<key rid='555'>eyJ0eXBlIjoxLCJib2R5IjoiYzFwaDNSNzNYNyIsInJlZ2lzdHJhdGlvbklkIjoiMTMzNyJ9</key>`+
  160. `<iv>${sent_stanza.nodeTree.querySelector('iv').textContent}</iv>`+
  161. `</header>`+
  162. `<payload>${sent_stanza.nodeTree.querySelector('payload').textContent}</payload>`+
  163. `</encrypted>`+
  164. `</message>`);
  165. // Test reception of an encrypted message
  166. return view.model.encryptMessage('This is an encrypted message from the contact')
  167. }).then((payload) => {
  168. // XXX: Normally the key will be encrypted via libsignal.
  169. // However, we're mocking libsignal in the tests, so we include
  170. // it as plaintext in the message.
  171. const key = btoa(JSON.stringify({
  172. 'type': 1,
  173. 'body': payload.key_str+payload.tag,
  174. 'registrationId': '1337'
  175. }));
  176. const stanza = $msg({
  177. 'from': contact_jid,
  178. 'to': _converse.connection.jid,
  179. 'type': 'chat',
  180. 'id': 'qwerty'
  181. }).c('body').t('This is a fallback message').up()
  182. .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
  183. .c('header', {'sid': '555'})
  184. .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(key).up()
  185. .c('iv').t(payload.iv)
  186. .up().up()
  187. .c('payload').t(payload.ciphertext);
  188. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  189. return test_utils.waitUntil(() => view.model.messages.length > 1);
  190. }).then(() => {
  191. expect(view.model.messages.length).toBe(2);
  192. const last_msg = view.model.messages.at(1),
  193. encrypted = last_msg.get('encrypted');
  194. expect(encrypted instanceof Object).toBe(true);
  195. expect(encrypted.device_id).toBe('555');
  196. expect(encrypted.iv).toBe(btoa('1234'));
  197. expect(encrypted.key).toBe(btoa('c1ph3R73X7'));
  198. expect(encrypted.payload).toBe(btoa('M04R-c1ph3R73X7'));
  199. done();
  200. });
  201. }));
  202. it("will add processing hints to sent out encrypted <message> stanzas",
  203. mock.initConverseWithPromises(
  204. null, ['rosterGroupsFetched'], {},
  205. function (done, _converse) {
  206. // TODO
  207. done();
  208. }));
  209. it("updates device lists based on PEP messages",
  210. mock.initConverseWithPromises(
  211. null, ['rosterGroupsFetched'], {},
  212. function (done, _converse) {
  213. let iq_stanza;
  214. test_utils.createContacts(_converse, 'current', 1);
  215. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  216. test_utils.waitUntil(function () {
  217. return _.filter(
  218. _converse.connection.IQ_stanzas,
  219. (iq) => {
  220. const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  221. if (node) { iq_stanza = iq.nodeTree;}
  222. return node;
  223. }).length;
  224. }).then(function () {
  225. expect(iq_stanza.outerHTML).toBe(
  226. '<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
  227. '<query xmlns="http://jabber.org/protocol/disco#items" '+
  228. 'node="eu.siacs.conversations.axolotl.devicelist"/>'+
  229. '</iq>');
  230. const stanza = $iq({
  231. 'from': contact_jid,
  232. 'id': iq_stanza.getAttribute('id'),
  233. 'to': _converse.bare_jid,
  234. 'type': 'result',
  235. }).c('query', {
  236. 'xmlns': 'http://jabber.org/protocol/disco#items',
  237. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  238. }).c('device', {'id': '555'}).up()
  239. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  240. expect(_converse.devicelists.length).toBe(1);
  241. const devicelist = _converse.devicelists.get(_converse.bare_jid);
  242. expect(devicelist.devices.length).toBe(1);
  243. expect(devicelist.devices.at(0).get('id')).toBe('555');
  244. return test_utils.waitUntil(() => _converse.devicelists);
  245. }).then(function () {
  246. // We simply emit, to avoid doing all the setup work
  247. _converse.emit('OMEMOInitialized');
  248. let stanza = $msg({
  249. 'from': contact_jid,
  250. 'to': _converse.bare_jid,
  251. 'type': 'headline',
  252. 'id': 'update_01',
  253. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  254. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  255. .c('item')
  256. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  257. .c('device', {'id': '1234'})
  258. .c('device', {'id': '4223'})
  259. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  260. expect(_converse.devicelists.length).toBe(2);
  261. let devices = _converse.devicelists.get(contact_jid).devices;
  262. expect(devices.length).toBe(2);
  263. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223');
  264. expect(devices.get('1234').get('active')).toBe(true);
  265. expect(devices.get('4223').get('active')).toBe(true);
  266. stanza = $msg({
  267. 'from': contact_jid,
  268. 'to': _converse.bare_jid,
  269. 'type': 'headline',
  270. 'id': 'update_02',
  271. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  272. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  273. .c('item')
  274. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  275. .c('device', {'id': '4223'})
  276. .c('device', {'id': '4224'})
  277. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  278. expect(_converse.devicelists.length).toBe(2);
  279. expect(devices.length).toBe(3);
  280. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223,4224');
  281. expect(devices.get('1234').get('active')).toBe(false);
  282. expect(devices.get('4223').get('active')).toBe(true);
  283. expect(devices.get('4224').get('active')).toBe(true);
  284. // Check that own devicelist gets updated
  285. stanza = $msg({
  286. 'from': _converse.bare_jid,
  287. 'to': _converse.bare_jid,
  288. 'type': 'headline',
  289. 'id': 'update_03',
  290. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  291. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  292. .c('item')
  293. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  294. .c('device', {'id': '555'})
  295. .c('device', {'id': '777'})
  296. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  297. expect(_converse.devicelists.length).toBe(2);
  298. devices = _converse.devicelists.get(_converse.bare_jid).devices;
  299. expect(devices.length).toBe(3);
  300. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,555,777');
  301. expect(devices.get('123456789').get('active')).toBe(true);
  302. expect(devices.get('555').get('active')).toBe(true);
  303. expect(devices.get('777').get('active')).toBe(true);
  304. _converse.connection.IQ_stanzas = [];
  305. // Check that own device gets re-added
  306. stanza = $msg({
  307. 'from': _converse.bare_jid,
  308. 'to': _converse.bare_jid,
  309. 'type': 'headline',
  310. 'id': 'update_04',
  311. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  312. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  313. .c('item')
  314. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  315. .c('device', {'id': '444'})
  316. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  317. return test_utils.waitUntil(function () {
  318. return _.filter(
  319. _converse.connection.IQ_stanzas,
  320. (iq) => {
  321. const node = iq.nodeTree.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]');
  322. if (node) { iq_stanza = iq.nodeTree;}
  323. return node;
  324. }).length;
  325. });
  326. }).then(function () {
  327. // Check that our own device is added again, but that removed
  328. // devices are not added.
  329. expect(iq_stanza.outerHTML).toBe(
  330. '<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="'+iq_stanza.getAttribute('id')+'">'+
  331. '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
  332. '<publish node="eu.siacs.conversations.axolotl.devicelist">'+
  333. '<item>'+
  334. '<list xmlns="eu.siacs.conversations.axolotl"/>'+
  335. '<device id="123456789"/>'+
  336. '<device id="444"/>'+
  337. '</item>'+
  338. '</publish>'+
  339. '</pubsub>'+
  340. '</iq>');
  341. expect(_converse.devicelists.length).toBe(2);
  342. const devices = _converse.devicelists.get(_converse.bare_jid).devices;
  343. // The device id for this device (123456789) was also generated and added to the list,
  344. // which is why we have 4 devices now.
  345. expect(devices.length).toBe(4);
  346. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,444,555,777');
  347. expect(devices.get('123456789').get('active')).toBe(true);
  348. expect(devices.get('444').get('active')).toBe(true);
  349. expect(devices.get('555').get('active')).toBe(false);
  350. expect(devices.get('777').get('active')).toBe(false);
  351. done();
  352. });
  353. }));
  354. it("updates device bundles based on PEP messages",
  355. mock.initConverseWithPromises(
  356. null, ['rosterGroupsFetched'], {},
  357. function (done, _converse) {
  358. let iq_stanza;
  359. test_utils.createContacts(_converse, 'current');
  360. const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@localhost';
  361. test_utils.waitUntil(function () {
  362. return _.filter(
  363. _converse.connection.IQ_stanzas,
  364. (iq) => {
  365. const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  366. if (node) { iq_stanza = iq.nodeTree;}
  367. return node;
  368. }).length;
  369. }).then(function () {
  370. expect(iq_stanza.outerHTML).toBe(
  371. '<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
  372. '<query xmlns="http://jabber.org/protocol/disco#items" '+
  373. 'node="eu.siacs.conversations.axolotl.devicelist"/>'+
  374. '</iq>');
  375. const stanza = $iq({
  376. 'from': contact_jid,
  377. 'id': iq_stanza.getAttribute('id'),
  378. 'to': _converse.bare_jid,
  379. 'type': 'result',
  380. }).c('query', {
  381. 'xmlns': 'http://jabber.org/protocol/disco#items',
  382. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  383. }).c('device', {'id': '555'}).up()
  384. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  385. expect(_converse.devicelists.length).toBe(1);
  386. return test_utils.waitUntil(() => _converse.devicelists);
  387. }).then(function () {
  388. // We simply emit, to avoid doing all the setup work
  389. expect(_converse.devicelists.length).toBe(1);
  390. let devicelist = _converse.devicelists.get(_converse.bare_jid);
  391. expect(devicelist.devices.length).toBe(2);
  392. expect(devicelist.devices.at(0).get('id')).toBe('555');
  393. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  394. _converse.emit('OMEMOInitialized');
  395. let stanza = $msg({
  396. 'from': contact_jid,
  397. 'to': _converse.bare_jid,
  398. 'type': 'headline',
  399. 'id': 'update_01',
  400. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  401. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'})
  402. .c('item')
  403. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  404. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('1111').up()
  405. .c('signedPreKeySignature').t('2222').up()
  406. .c('identityKey').t('3333').up()
  407. .c('prekeys')
  408. .c('preKeyPublic', {'preKeyId': '1001'}).up()
  409. .c('preKeyPublic', {'preKeyId': '1002'}).up()
  410. .c('preKeyPublic', {'preKeyId': '1003'});
  411. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  412. expect(_converse.devicelists.length).toBe(2);
  413. devicelist = _converse.devicelists.get(contact_jid);
  414. expect(devicelist.devices.length).toBe(1);
  415. let device = devicelist.devices.at(0);
  416. expect(device.get('bundle').identity_key).toBe('3333');
  417. expect(device.get('bundle').signed_prekey.public_key).toBe('1111');
  418. expect(device.get('bundle').signed_prekey.id).toBe(4223);
  419. expect(device.get('bundle').signed_prekey.signature).toBe('2222');
  420. expect(device.get('bundle').prekeys.length).toBe(3);
  421. expect(device.get('bundle').prekeys[0].id).toBe(1001);
  422. expect(device.get('bundle').prekeys[1].id).toBe(1002);
  423. expect(device.get('bundle').prekeys[2].id).toBe(1003);
  424. stanza = $msg({
  425. 'from': contact_jid,
  426. 'to': _converse.bare_jid,
  427. 'type': 'headline',
  428. 'id': 'update_02',
  429. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  430. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'})
  431. .c('item')
  432. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  433. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('5555').up()
  434. .c('signedPreKeySignature').t('6666').up()
  435. .c('identityKey').t('7777').up()
  436. .c('prekeys')
  437. .c('preKeyPublic', {'preKeyId': '2001'}).up()
  438. .c('preKeyPublic', {'preKeyId': '2002'}).up()
  439. .c('preKeyPublic', {'preKeyId': '2003'});
  440. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  441. expect(_converse.devicelists.length).toBe(2);
  442. devicelist = _converse.devicelists.get(contact_jid);
  443. expect(devicelist.devices.length).toBe(1);
  444. device = devicelist.devices.at(0);
  445. expect(device.get('bundle').identity_key).toBe('7777');
  446. expect(device.get('bundle').signed_prekey.public_key).toBe('5555');
  447. expect(device.get('bundle').signed_prekey.id).toBe(4223);
  448. expect(device.get('bundle').signed_prekey.signature).toBe('6666');
  449. expect(device.get('bundle').prekeys.length).toBe(3);
  450. expect(device.get('bundle').prekeys[0].id).toBe(2001);
  451. expect(device.get('bundle').prekeys[1].id).toBe(2002);
  452. expect(device.get('bundle').prekeys[2].id).toBe(2003);
  453. stanza = $msg({
  454. 'from': _converse.bare_jid,
  455. 'to': _converse.bare_jid,
  456. 'type': 'headline',
  457. 'id': 'update_03',
  458. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  459. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:123456789'})
  460. .c('item')
  461. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  462. .c('signedPreKeyPublic', {'signedPreKeyId': '9999'}).t('8888').up()
  463. .c('signedPreKeySignature').t('3333').up()
  464. .c('identityKey').t('1111').up()
  465. .c('prekeys')
  466. .c('preKeyPublic', {'preKeyId': '3001'}).up()
  467. .c('preKeyPublic', {'preKeyId': '3002'}).up()
  468. .c('preKeyPublic', {'preKeyId': '3003'});
  469. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  470. expect(_converse.devicelists.length).toBe(2);
  471. devicelist = _converse.devicelists.get(_converse.bare_jid);
  472. expect(devicelist.devices.length).toBe(2);
  473. expect(devicelist.devices.at(0).get('id')).toBe('555');
  474. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  475. device = devicelist.devices.at(1);
  476. expect(device.get('bundle').identity_key).toBe('1111');
  477. expect(device.get('bundle').signed_prekey.public_key).toBe('8888');
  478. expect(device.get('bundle').signed_prekey.id).toBe(9999);
  479. expect(device.get('bundle').signed_prekey.signature).toBe('3333');
  480. expect(device.get('bundle').prekeys.length).toBe(3);
  481. expect(device.get('bundle').prekeys[0].id).toBe(3001);
  482. expect(device.get('bundle').prekeys[1].id).toBe(3002);
  483. expect(device.get('bundle').prekeys[2].id).toBe(3003);
  484. done();
  485. });
  486. }));
  487. it("publishes a bundle with which an encrypted session can be created",
  488. mock.initConverseWithPromises(
  489. null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  490. function (done, _converse) {
  491. _converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test
  492. let iq_stanza;
  493. test_utils.createContacts(_converse, 'current', 1);
  494. _converse.emit('rosterContactsFetched');
  495. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  496. test_utils.waitUntil(function () {
  497. return _.filter(
  498. _converse.connection.IQ_stanzas,
  499. (iq) => {
  500. const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  501. if (node) { iq_stanza = iq.nodeTree;}
  502. return node;
  503. }).length;
  504. }).then(function () {
  505. const stanza = $iq({
  506. 'from': contact_jid,
  507. 'id': iq_stanza.getAttribute('id'),
  508. 'to': _converse.bare_jid,
  509. 'type': 'result',
  510. }).c('query', {
  511. 'xmlns': 'http://jabber.org/protocol/disco#items',
  512. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  513. }).c('device', {'id': '482886413b977930064a5888b92134fe'}).up()
  514. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  515. expect(_converse.devicelists.length).toBe(1);
  516. return test_utils.openChatBoxFor(_converse, contact_jid);
  517. }).then(() => {
  518. return test_utils.waitUntil(() => {
  519. return _.filter(_converse.connection.IQ_stanzas, function (iq) {
  520. const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.devicelist"]');
  521. if (node) { iq_stanza = iq.nodeTree; }
  522. return node;
  523. }).length;
  524. });
  525. }).then(function () {
  526. const stanza = $iq({
  527. 'from': _converse.bare_jid,
  528. 'id': iq_stanza.getAttribute('id'),
  529. 'to': _converse.bare_jid,
  530. 'type': 'result'});
  531. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  532. return test_utils.waitUntil(() => {
  533. return _.filter(_converse.connection.IQ_stanzas, function (iq) {
  534. const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
  535. if (node) { iq_stanza = iq.nodeTree; }
  536. return node;
  537. }).length;
  538. });
  539. }).then(function () {
  540. expect(iq_stanza.outerHTML).toBe(
  541. `<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
  542. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  543. `<publish node="eu.siacs.conversations.axolotl.bundles:123456789">`+
  544. `<item>`+
  545. `<bundle xmlns="eu.siacs.conversations.axolotl">`+
  546. `<signedPreKeyPublic signedPreKeyId="0">${btoa('1234')}</signedPreKeyPublic>`+
  547. `<signedPreKeySignature>${btoa('11112222333344445555')}</signedPreKeySignature>`+
  548. `<identityKey>${btoa('1234')}</identityKey>`+
  549. `<prekeys>`+
  550. `<preKeyPublic preKeyId="0">${btoa('1234')}</preKeyPublic>`+
  551. `<preKeyPublic preKeyId="1">${btoa('1234')}</preKeyPublic>`+
  552. `</prekeys>`+
  553. `</bundle>`+
  554. `</item>`+
  555. `</publish>`+
  556. `</pubsub>`+
  557. `</iq>`)
  558. const stanza = $iq({
  559. 'from': _converse.bare_jid,
  560. 'id': iq_stanza.getAttribute('id'),
  561. 'to': _converse.bare_jid,
  562. 'type': 'result'});
  563. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  564. return _converse.api.waitUntil('OMEMOInitialized');
  565. }).then(done).catch(_.partial(console.error, _));
  566. }));
  567. it("adds a toolbar button for starting an encrypted chat session",
  568. mock.initConverseWithPromises(
  569. null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  570. function (done, _converse) {
  571. let iq_stanza, modal;
  572. test_utils.createContacts(_converse, 'current', 1);
  573. _converse.emit('rosterContactsFetched');
  574. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  575. test_utils.waitUntil(function () {
  576. return _.filter(
  577. _converse.connection.IQ_stanzas,
  578. (iq) => {
  579. const node = iq.nodeTree.querySelector('iq[to="'+_converse.bare_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  580. if (node) { iq_stanza = iq.nodeTree;}
  581. return node;
  582. }).length;
  583. }).then(function () {
  584. expect(iq_stanza.outerHTML).toBe(
  585. '<iq type="get" from="dummy@localhost" to="dummy@localhost" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
  586. '<query xmlns="http://jabber.org/protocol/disco#items" '+
  587. 'node="eu.siacs.conversations.axolotl.devicelist"/>'+
  588. '</iq>');
  589. const stanza = $iq({
  590. 'from': contact_jid,
  591. 'id': iq_stanza.getAttribute('id'),
  592. 'to': _converse.bare_jid,
  593. 'type': 'result',
  594. }).c('query', {
  595. 'xmlns': 'http://jabber.org/protocol/disco#items',
  596. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  597. }).c('device', {'id': '482886413b977930064a5888b92134fe'}).up()
  598. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  599. expect(_converse.devicelists.length).toBe(1);
  600. const devicelist = _converse.devicelists.get(_converse.bare_jid);
  601. expect(devicelist.devices.length).toBe(1);
  602. expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
  603. return test_utils.openChatBoxFor(_converse, contact_jid);
  604. }).then(() => {
  605. return test_utils.waitUntil(() => {
  606. return _.filter(_converse.connection.IQ_stanzas, function (iq) {
  607. const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.devicelist"]');
  608. if (node) { iq_stanza = iq.nodeTree; }
  609. return node;
  610. }).length;
  611. });
  612. }).then(function () {
  613. expect(iq_stanza.outerHTML).toBe(
  614. '<iq from="dummy@localhost" type="set" xmlns="jabber:client" id="'+iq_stanza.getAttribute('id')+'">'+
  615. '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
  616. '<publish node="eu.siacs.conversations.axolotl.devicelist">'+
  617. '<item>'+
  618. '<list xmlns="eu.siacs.conversations.axolotl"/>'+
  619. '<device id="482886413b977930064a5888b92134fe"/>'+
  620. '<device id="123456789"/>'+
  621. '</item>'+
  622. '</publish>'+
  623. '</pubsub>'+
  624. '</iq>');
  625. const stanza = $iq({
  626. 'from': _converse.bare_jid,
  627. 'id': iq_stanza.getAttribute('id'),
  628. 'to': _converse.bare_jid,
  629. 'type': 'result'});
  630. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  631. return test_utils.waitUntil(() => {
  632. return _.filter(_converse.connection.IQ_stanzas, function (iq) {
  633. const node = iq.nodeTree.querySelector('publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]');
  634. if (node) { iq_stanza = iq.nodeTree; }
  635. return node;
  636. }).length;
  637. });
  638. }).then(function () {
  639. expect(iq_stanza.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join());
  640. expect(iq_stanza.querySelector('prekeys').childNodes.length).toBe(100);
  641. const signed_prekeys = iq_stanza.querySelectorAll('signedPreKeyPublic');
  642. expect(signed_prekeys.length).toBe(1);
  643. const signed_prekey = signed_prekeys[0];
  644. expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0')
  645. expect(iq_stanza.querySelectorAll('signedPreKeySignature').length).toBe(1);
  646. expect(iq_stanza.querySelectorAll('identityKey').length).toBe(1);
  647. const stanza = $iq({
  648. 'from': _converse.bare_jid,
  649. 'id': iq_stanza.getAttribute('id'),
  650. 'to': _converse.bare_jid,
  651. 'type': 'result'});
  652. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  653. return test_utils.waitUntil(() => {
  654. return _.filter(
  655. _converse.connection.IQ_stanzas,
  656. (iq) => {
  657. const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  658. if (node) { iq_stanza = iq.nodeTree; }
  659. return node;
  660. }).length;});
  661. }).then(function () {
  662. expect(iq_stanza.outerHTML).toBe(
  663. '<iq type="get" from="dummy@localhost" to="'+contact_jid+'" xmlns="jabber:client" id="'+iq_stanza.getAttribute("id")+'">'+
  664. '<query xmlns="http://jabber.org/protocol/disco#items" '+
  665. 'node="eu.siacs.conversations.axolotl.devicelist"/>'+
  666. '</iq>');
  667. const stanza = $iq({
  668. 'from': contact_jid,
  669. 'id': iq_stanza.getAttribute('id'),
  670. 'to': _converse.bare_jid,
  671. 'type': 'result',
  672. }).c('query', {
  673. 'xmlns': 'http://jabber.org/protocol/disco#items',
  674. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  675. }).c('device', {'id': '368866411b877c30064a5f62b917cffe'}).up()
  676. .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up()
  677. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  678. .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
  679. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  680. expect(_converse.devicelists.length).toBe(2);
  681. const devicelist = _converse.devicelists.get(contact_jid);
  682. expect(devicelist.devices.length).toBe(4);
  683. expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe');
  684. expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
  685. expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  686. expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
  687. return test_utils.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar'));
  688. }).then(function () {
  689. const view = _converse.chatboxviews.get(contact_jid);
  690. const toolbar = view.el.querySelector('.chat-toolbar');
  691. expect(view.model.get('omemo_active')).toBe(undefined);
  692. const toggle = toolbar.querySelector('.toggle-omemo');
  693. expect(_.isNull(toggle)).toBe(false);
  694. expect(u.hasClass('fa-unlock', toggle)).toBe(true);
  695. expect(u.hasClass('fa-lock', toggle)).toBe(false);
  696. spyOn(view, 'toggleOMEMO').and.callThrough();
  697. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  698. toolbar.querySelector('.toggle-omemo').click();
  699. expect(view.toggleOMEMO).toHaveBeenCalled();
  700. expect(view.model.get('omemo_active')).toBe(true);
  701. return test_utils.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo')));
  702. }).then(function () {
  703. const view = _converse.chatboxviews.get(contact_jid);
  704. const toolbar = view.el.querySelector('.chat-toolbar');
  705. const toggle = toolbar.querySelector('.toggle-omemo');
  706. expect(u.hasClass('fa-unlock', toggle)).toBe(false);
  707. expect(u.hasClass('fa-lock', toggle)).toBe(true);
  708. const textarea = view.el.querySelector('.chat-textarea');
  709. textarea.value = 'This message will be sent encrypted';
  710. view.keyPressed({
  711. target: textarea,
  712. preventDefault: _.noop,
  713. keyCode: 13
  714. });
  715. done();
  716. }).catch(_.partial(console.error, _));
  717. }));
  718. it("shows OMEMO device fingerprints in the user details modal",
  719. mock.initConverseWithPromises(
  720. null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
  721. function (done, _converse) {
  722. let iq_stanza, modal;
  723. test_utils.createContacts(_converse, 'current', 1);
  724. _converse.emit('rosterContactsFetched');
  725. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
  726. test_utils.openChatBoxFor(_converse, contact_jid)
  727. .then(() => {
  728. // We simply emit, to avoid doing all the setup work
  729. _converse.emit('OMEMOInitialized');
  730. const view = _converse.chatboxviews.get(contact_jid);
  731. const show_modal_button = view.el.querySelector('.show-user-details-modal');
  732. show_modal_button.click();
  733. modal = view.user_details_modal;
  734. return test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(() => {
  735. return test_utils.waitUntil(() => {
  736. return _.filter(
  737. _converse.connection.IQ_stanzas,
  738. (iq) => {
  739. const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
  740. if (node) { iq_stanza = iq.nodeTree; }
  741. return node;
  742. }).length;});
  743. });
  744. }).then(() => {
  745. iq_stanza;
  746. expect(iq_stanza.outerHTML).toBe(
  747. `<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
  748. `<query xmlns="http://jabber.org/protocol/disco#items" node="eu.siacs.conversations.axolotl.devicelist"/>`+
  749. `</iq>`);
  750. const stanza = $iq({
  751. 'from': contact_jid,
  752. 'id': iq_stanza.getAttribute('id'),
  753. 'to': _converse.bare_jid,
  754. 'type': 'result',
  755. }).c('query', {
  756. 'xmlns': 'http://jabber.org/protocol/disco#items',
  757. 'node': 'eu.siacs.conversations.axolotl.devicelist'
  758. }).c('device', {'id': '555'}).up()
  759. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  760. return test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(function () {
  761. return test_utils.waitUntil(() => {
  762. return _.filter(
  763. _converse.connection.IQ_stanzas,
  764. (iq) => {
  765. const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]');
  766. if (node) { iq_stanza = iq.nodeTree; }
  767. return node;
  768. }).length;});
  769. });
  770. }).then(() => {
  771. expect(iq_stanza.outerHTML).toBe(
  772. `<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
  773. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  774. `<items node="eu.siacs.conversations.axolotl.bundles:555"/>`+
  775. `</pubsub>`+
  776. `</iq>`);
  777. const stanza = $iq({
  778. 'from': contact_jid,
  779. 'id': iq_stanza.getAttribute('id'),
  780. 'to': _converse.bare_jid,
  781. 'type': 'result',
  782. }).c('pubsub', {
  783. 'xmlns': 'http://jabber.org/protocol/pubsub'
  784. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
  785. .c('item')
  786. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  787. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  788. .c('signedPreKeySignature').t(btoa('2222')).up()
  789. .c('identityKey').t(btoa('3333')).up()
  790. .c('prekeys')
  791. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  792. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  793. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  794. _converse.connection._dataRecv(test_utils.createRequest(stanza));
  795. const view = _converse.chatboxviews.get(contact_jid);
  796. const modal = view.user_details_modal;
  797. return test_utils.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
  798. }).then(() => {
  799. const view = _converse.chatboxviews.get(contact_jid);
  800. const modal = view.user_details_modal;
  801. expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
  802. const el = modal.el.querySelector('.fingerprints .fingerprint');
  803. expect(el.textContent).toBe('f56d6351aa71cff0debea014d13525e42036187a');
  804. expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
  805. let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
  806. expect(trusted_radio.checked).toBe(true);
  807. let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
  808. expect(untrusted_radio.checked).toBe(false);
  809. // Test that the device can be set to untrusted
  810. untrusted_radio.click();
  811. trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]');
  812. expect(trusted_radio.hasAttribute('checked')).toBe(false);
  813. untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]');
  814. expect(untrusted_radio.hasAttribute('checked')).toBe(true);
  815. done();
  816. });
  817. }));
  818. });
  819. describe("A chatbox with an active OMEMO session", function() {
  820. it("will not show the spoiler toolbar button",
  821. mock.initConverseWithPromises(
  822. null, ['rosterGroupsFetched'], {},
  823. function (done, _converse) {
  824. // TODO
  825. done()
  826. }));
  827. });
  828. }));