omemo.js 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522
  1. /*global mock, converse, _ */
  2. const { $iq, $pres, $msg, omemo, Strophe } = converse.env;
  3. const u = converse.env.utils;
  4. async function deviceListFetched (_converse, jid) {
  5. const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`;
  6. const stanza = await u.waitUntil(
  7. () => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop()
  8. );
  9. await u.waitUntil(() => _converse.devicelists.get(jid));
  10. return stanza;
  11. }
  12. function ownDeviceHasBeenPublished (_converse) {
  13. return _.filter(
  14. Array.from(_converse.connection.IQ_stanzas),
  15. iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]')
  16. ).pop();
  17. }
  18. function bundleHasBeenPublished (_converse) {
  19. const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]';
  20. return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop();
  21. }
  22. function bundleFetched (_converse, jid, device_id) {
  23. return _.filter(
  24. Array.from(_converse.connection.IQ_stanzas),
  25. iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`)
  26. ).pop();
  27. }
  28. async function initializedOMEMO (_converse) {
  29. await mock.waitUntilDiscoConfirmed(
  30. _converse, _converse.bare_jid,
  31. [{'category': 'pubsub', 'type': 'pep'}],
  32. ['http://jabber.org/protocol/pubsub#publish-options']
  33. );
  34. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
  35. let stanza = $iq({
  36. 'from': _converse.bare_jid,
  37. 'id': iq_stanza.getAttribute('id'),
  38. 'to': _converse.bare_jid,
  39. 'type': 'result',
  40. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  41. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  42. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  43. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  44. .c('device', {'id': '482886413b977930064a5888b92134fe'});
  45. _converse.connection._dataRecv(mock.createRequest(stanza));
  46. iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse))
  47. stanza = $iq({
  48. 'from': _converse.bare_jid,
  49. 'id': iq_stanza.getAttribute('id'),
  50. 'to': _converse.bare_jid,
  51. 'type': 'result'});
  52. _converse.connection._dataRecv(mock.createRequest(stanza));
  53. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse))
  54. stanza = $iq({
  55. 'from': _converse.bare_jid,
  56. 'id': iq_stanza.getAttribute('id'),
  57. 'to': _converse.bare_jid,
  58. 'type': 'result'});
  59. _converse.connection._dataRecv(mock.createRequest(stanza));
  60. await _converse.api.waitUntil('OMEMOInitialized');
  61. }
  62. describe("The OMEMO module", function() {
  63. it("adds methods for encrypting and decrypting messages via AES GCM",
  64. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  65. const message = 'This message will be encrypted'
  66. await mock.waitForRoster(_converse, 'current', 1);
  67. const payload = await omemo.encryptMessage(message);
  68. const result = await omemo.decryptMessage(payload);
  69. expect(result).toBe(message);
  70. done();
  71. }));
  72. it("enables encrypted messages to be sent and received",
  73. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  74. let sent_stanza;
  75. await mock.waitForRoster(_converse, 'current', 1);
  76. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  77. await u.waitUntil(() => initializedOMEMO(_converse));
  78. await mock.openChatBoxFor(_converse, contact_jid);
  79. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  80. let stanza = $iq({
  81. 'from': contact_jid,
  82. 'id': iq_stanza.getAttribute('id'),
  83. 'to': _converse.connection.jid,
  84. 'type': 'result',
  85. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  86. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  87. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  88. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  89. .c('device', {'id': '555'});
  90. _converse.connection._dataRecv(mock.createRequest(stanza));
  91. await u.waitUntil(() => _converse.omemo_store);
  92. const devicelist = _converse.devicelists.get({'jid': contact_jid});
  93. await u.waitUntil(() => devicelist.devices.length === 1);
  94. const view = _converse.chatboxviews.get(contact_jid);
  95. view.model.set('omemo_active', true);
  96. const textarea = view.querySelector('.chat-textarea');
  97. textarea.value = 'This message will be encrypted';
  98. const bottom_panel = view.querySelector('converse-chat-bottom-panel');
  99. bottom_panel.onKeyDown({
  100. target: textarea,
  101. preventDefault: function preventDefault () {},
  102. keyCode: 13 // Enter
  103. });
  104. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555'));
  105. stanza = $iq({
  106. 'from': contact_jid,
  107. 'id': iq_stanza.getAttribute('id'),
  108. 'to': _converse.bare_jid,
  109. 'type': 'result',
  110. }).c('pubsub', {
  111. 'xmlns': 'http://jabber.org/protocol/pubsub'
  112. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
  113. .c('item')
  114. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  115. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  116. .c('signedPreKeySignature').t(btoa('2222')).up()
  117. .c('identityKey').t(btoa('3333')).up()
  118. .c('prekeys')
  119. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  120. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  121. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  122. _converse.connection._dataRecv(mock.createRequest(stanza));
  123. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
  124. stanza = $iq({
  125. 'from': _converse.bare_jid,
  126. 'id': iq_stanza.getAttribute('id'),
  127. 'to': _converse.bare_jid,
  128. 'type': 'result',
  129. }).c('pubsub', {
  130. 'xmlns': 'http://jabber.org/protocol/pubsub'
  131. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
  132. .c('item')
  133. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  134. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
  135. .c('signedPreKeySignature').t(btoa('200000')).up()
  136. .c('identityKey').t(btoa('300000')).up()
  137. .c('prekeys')
  138. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
  139. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
  140. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
  141. spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza });
  142. _converse.connection._dataRecv(mock.createRequest(stanza));
  143. await u.waitUntil(() => sent_stanza);
  144. expect(sent_stanza.toLocaleString()).toBe(
  145. `<message from="romeo@montague.lit/orchard" id="${sent_stanza.nodeTree.getAttribute("id")}" `+
  146. `to="mercutio@montague.lit" `+
  147. `type="chat" xmlns="jabber:client">`+
  148. `<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
  149. `<request xmlns="urn:xmpp:receipts"/>`+
  150. `<encrypted xmlns="eu.siacs.conversations.axolotl">`+
  151. `<header sid="123456789">`+
  152. `<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
  153. `<key rid="555">YzFwaDNSNzNYNw==</key>`+
  154. `<iv>${sent_stanza.nodeTree.querySelector("iv").textContent}</iv>`+
  155. `</header>`+
  156. `<payload>${sent_stanza.nodeTree.querySelector("payload").textContent}</payload>`+
  157. `</encrypted>`+
  158. `<store xmlns="urn:xmpp:hints"/>`+
  159. `</message>`);
  160. // Test reception of an encrypted message
  161. let obj = await omemo.encryptMessage('This is an encrypted message from the contact')
  162. // XXX: Normally the key will be encrypted via libsignal.
  163. // However, we're mocking libsignal in the tests, so we include it as plaintext in the message.
  164. stanza = $msg({
  165. 'from': contact_jid,
  166. 'to': _converse.connection.jid,
  167. 'type': 'chat',
  168. 'id': _converse.connection.getUniqueId()
  169. }).c('body').t('This is a fallback message').up()
  170. .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
  171. .c('header', {'sid': '555'})
  172. .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
  173. .c('iv').t(obj.iv)
  174. .up().up()
  175. .c('payload').t(obj.payload);
  176. _converse.connection._dataRecv(mock.createRequest(stanza));
  177. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  178. expect(view.model.messages.length).toBe(2);
  179. expect(view.querySelectorAll('.chat-msg__body')[1].textContent.trim())
  180. .toBe('This is an encrypted message from the contact');
  181. // #1193 Check for a received message without <body> tag
  182. obj = await omemo.encryptMessage('Another received encrypted message without fallback')
  183. stanza = $msg({
  184. 'from': contact_jid,
  185. 'to': _converse.connection.jid,
  186. 'type': 'chat',
  187. 'id': _converse.connection.getUniqueId()
  188. }).c('encrypted', {'xmlns': Strophe.NS.OMEMO})
  189. .c('header', {'sid': '555'})
  190. .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
  191. .c('iv').t(obj.iv)
  192. .up().up()
  193. .c('payload').t(obj.payload);
  194. _converse.connection._dataRecv(mock.createRequest(stanza));
  195. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  196. await u.waitUntil(() => view.model.messages.length > 1);
  197. expect(view.model.messages.length).toBe(3);
  198. expect(view.querySelectorAll('.chat-msg__body')[2].textContent.trim())
  199. .toBe('Another received encrypted message without fallback');
  200. done();
  201. }));
  202. it("enables encrypted groupchat messages to be sent and received",
  203. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  204. // MEMO encryption works only in members only conferences
  205. // that are non-anonymous.
  206. const features = [
  207. 'http://jabber.org/protocol/muc',
  208. 'jabber:iq:register',
  209. 'muc_passwordprotected',
  210. 'muc_hidden',
  211. 'muc_temporary',
  212. 'muc_membersonly',
  213. 'muc_unmoderated',
  214. 'muc_nonanonymous'
  215. ];
  216. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
  217. const view = _converse.chatboxviews.get('lounge@montague.lit');
  218. await u.waitUntil(() => initializedOMEMO(_converse));
  219. const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
  220. const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
  221. el.click();
  222. expect(view.model.get('omemo_active')).toBe(true);
  223. // newguy enters the room
  224. const contact_jid = 'newguy@montague.lit';
  225. let stanza = $pres({
  226. 'to': 'romeo@montague.lit/orchard',
  227. 'from': 'lounge@montague.lit/newguy'
  228. })
  229. .c('x', {xmlns: Strophe.NS.MUC_USER})
  230. .c('item', {
  231. 'affiliation': 'none',
  232. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  233. 'role': 'participant'
  234. }).tree();
  235. _converse.connection._dataRecv(mock.createRequest(stanza));
  236. // Wait for Converse to fetch newguy's device list
  237. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  238. expect(Strophe.serialize(iq_stanza)).toBe(
  239. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  240. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  241. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  242. `</pubsub>`+
  243. `</iq>`);
  244. // The server returns his device list
  245. stanza = $iq({
  246. 'from': contact_jid,
  247. 'id': iq_stanza.getAttribute('id'),
  248. 'to': _converse.bare_jid,
  249. 'type': 'result',
  250. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  251. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  252. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  253. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  254. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  255. _converse.connection._dataRecv(mock.createRequest(stanza));
  256. await u.waitUntil(() => _converse.omemo_store);
  257. expect(_converse.devicelists.length).toBe(2);
  258. await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  259. const devicelist = _converse.devicelists.get(contact_jid);
  260. expect(devicelist.devices.length).toBe(1);
  261. expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  262. expect(view.model.get('omemo_active')).toBe(true);
  263. const icon = toolbar.querySelector('.toggle-omemo converse-icon');
  264. expect(u.hasClass('fa-unlock', icon)).toBe(false);
  265. expect(u.hasClass('fa-lock', icon)).toBe(true);
  266. const textarea = view.querySelector('.chat-textarea');
  267. textarea.value = 'This message will be encrypted';
  268. const bottom_panel = view.querySelector('converse-muc-bottom-panel');
  269. bottom_panel.onKeyDown({
  270. target: textarea,
  271. preventDefault: function preventDefault () {},
  272. keyCode: 13 // Enter
  273. });
  274. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000);
  275. console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228");
  276. stanza = $iq({
  277. 'from': contact_jid,
  278. 'id': iq_stanza.getAttribute('id'),
  279. 'to': _converse.bare_jid,
  280. 'type': 'result',
  281. }).c('pubsub', {
  282. 'xmlns': 'http://jabber.org/protocol/pubsub'
  283. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"})
  284. .c('item')
  285. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  286. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  287. .c('signedPreKeySignature').t(btoa('2222')).up()
  288. .c('identityKey').t(btoa('3333')).up()
  289. .c('prekeys')
  290. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  291. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  292. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  293. _converse.connection._dataRecv(mock.createRequest(stanza));
  294. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000);
  295. console.log("Bundle fetched 482886413b977930064a5888b92134fe");
  296. stanza = $iq({
  297. 'from': _converse.bare_jid,
  298. 'id': iq_stanza.getAttribute('id'),
  299. 'to': _converse.bare_jid,
  300. 'type': 'result',
  301. }).c('pubsub', {
  302. 'xmlns': 'http://jabber.org/protocol/pubsub'
  303. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
  304. .c('item')
  305. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  306. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
  307. .c('signedPreKeySignature').t(btoa('200000')).up()
  308. .c('identityKey').t(btoa('300000')).up()
  309. .c('prekeys')
  310. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
  311. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
  312. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
  313. spyOn(_converse.connection, 'send');
  314. _converse.connection._dataRecv(mock.createRequest(stanza));
  315. await u.waitUntil(() => _converse.connection.send.calls.count(), 1000);
  316. const sent_stanza = _converse.connection.send.calls.all()[0].args[0];
  317. expect(Strophe.serialize(sent_stanza)).toBe(
  318. `<message from="romeo@montague.lit/orchard" `+
  319. `id="${sent_stanza.nodeTree.getAttribute("id")}" `+
  320. `to="lounge@montague.lit" `+
  321. `type="groupchat" `+
  322. `xmlns="jabber:client">`+
  323. `<body>This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo</body>`+
  324. `<encrypted xmlns="eu.siacs.conversations.axolotl">`+
  325. `<header sid="123456789">`+
  326. `<key rid="482886413b977930064a5888b92134fe">YzFwaDNSNzNYNw==</key>`+
  327. `<key rid="4e30f35051b7b8b42abe083742187228">YzFwaDNSNzNYNw==</key>`+
  328. `<iv>${sent_stanza.nodeTree.querySelector("iv").textContent}</iv>`+
  329. `</header>`+
  330. `<payload>${sent_stanza.nodeTree.querySelector("payload").textContent}</payload>`+
  331. `</encrypted>`+
  332. `<store xmlns="urn:xmpp:hints"/>`+
  333. `</message>`);
  334. done();
  335. }));
  336. it("will create a new device based on a received carbon message",
  337. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  338. await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]);
  339. await mock.waitForRoster(_converse, 'current', 1);
  340. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  341. await u.waitUntil(() => initializedOMEMO(_converse));
  342. await mock.openChatBoxFor(_converse, contact_jid);
  343. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  344. const my_devicelist = _converse.devicelists.get({'jid': _converse.bare_jid});
  345. expect(my_devicelist.devices.length).toBe(2);
  346. const stanza = $iq({
  347. 'from': contact_jid,
  348. 'id': iq_stanza.getAttribute('id'),
  349. 'to': _converse.connection.jid,
  350. 'type': 'result',
  351. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  352. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  353. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  354. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  355. .c('device', {'id': '555'});
  356. _converse.connection._dataRecv(mock.createRequest(stanza));
  357. await u.waitUntil(() => _converse.omemo_store);
  358. const contact_devicelist = _converse.devicelists.get({'jid': contact_jid});
  359. await u.waitUntil(() => contact_devicelist.devices.length === 1);
  360. const view = _converse.chatboxviews.get(contact_jid);
  361. view.model.set('omemo_active', true);
  362. // Test reception of an encrypted carbon message
  363. const obj = await omemo.encryptMessage('This is an encrypted carbon message from another device of mine')
  364. const carbon = u.toStanza(`
  365. <message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="romeo@montague.lit" type="chat">
  366. <sent xmlns="urn:xmpp:carbons:2">
  367. <forwarded xmlns="urn:xmpp:forward:0">
  368. <message xmlns="jabber:client"
  369. from="romeo@montague.lit/gajim.HE02SW1L"
  370. xml:lang="en"
  371. to="${contact_jid}/gajim.0LATM5V2"
  372. type="chat" id="87141781-61d6-4eb3-9a31-429935a61b76">
  373. <archived xmlns="urn:xmpp:mam:tmp" by="romeo@montague.lit" id="1554033877043470"/>
  374. <stanza-id xmlns="urn:xmpp:sid:0" by="romeo@montague.lit" id="1554033877043470"/>
  375. <request xmlns="urn:xmpp:receipts"/>
  376. <active xmlns="http://jabber.org/protocol/chatstates"/>
  377. <origin-id xmlns="urn:xmpp:sid:0" id="87141781-61d6-4eb3-9a31-429935a61b76"/>
  378. <encrypted xmlns="eu.siacs.conversations.axolotl">
  379. <header sid="988349631">
  380. <key rid="${_converse.omemo_store.get('device_id')}"
  381. prekey="true">${u.arrayBufferToBase64(obj.key_and_tag)}</key>
  382. <iv>${obj.iv}</iv>
  383. </header>
  384. <payload>${obj.payload}</payload>
  385. </encrypted>
  386. <encryption xmlns="urn:xmpp:eme:0" namespace="eu.siacs.conversations.axolotl" name="OMEMO"/>
  387. <store xmlns="urn:xmpp:hints"/>
  388. </message>
  389. </forwarded>
  390. </sent>
  391. </message>
  392. `);
  393. _converse.connection.IQ_stanzas = [];
  394. _converse.connection._dataRecv(mock.createRequest(carbon));
  395. // The message received is a prekey message, so missing prekeys are
  396. // generated and a new bundle published.
  397. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse));
  398. const result_iq = $iq({
  399. 'from': _converse.bare_jid,
  400. 'id': iq_stanza.getAttribute('id'),
  401. 'to': _converse.bare_jid,
  402. 'type': 'result'});
  403. _converse.connection._dataRecv(mock.createRequest(result_iq));
  404. await new Promise(resolve => view.model.messages.once('rendered', resolve));
  405. expect(view.model.messages.length).toBe(1);
  406. expect(view.querySelector('.chat-msg__text').textContent.trim())
  407. .toBe('This is an encrypted carbon message from another device of mine');
  408. expect(contact_devicelist.devices.length).toBe(1);
  409. // Check that the new device id has been added to my devices
  410. expect(my_devicelist.devices.length).toBe(3);
  411. expect(my_devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
  412. expect(my_devicelist.devices.at(1).get('id')).toBe('123456789');
  413. expect(my_devicelist.devices.at(2).get('id')).toBe('988349631');
  414. expect(my_devicelist.devices.get('988349631').get('active')).toBe(true);
  415. const textarea = view.querySelector('.chat-textarea');
  416. textarea.value = 'This is an encrypted message from this device';
  417. const bottom_panel = view.querySelector('converse-chat-bottom-panel');
  418. bottom_panel.onKeyDown({
  419. target: textarea,
  420. preventDefault: function preventDefault () {},
  421. keyCode: 13 // Enter
  422. });
  423. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '988349631'));
  424. expect(Strophe.serialize(iq_stanza)).toBe(
  425. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${_converse.bare_jid}" type="get" xmlns="jabber:client">`+
  426. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  427. `<items node="eu.siacs.conversations.axolotl.bundles:988349631"/>`+
  428. `</pubsub>`+
  429. `</iq>`);
  430. done();
  431. }));
  432. it("gracefully handles auth errors when trying to send encrypted groupchat messages",
  433. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  434. // MEMO encryption works only in members only conferences
  435. // that are non-anonymous.
  436. const features = [
  437. 'http://jabber.org/protocol/muc',
  438. 'jabber:iq:register',
  439. 'muc_passwordprotected',
  440. 'muc_hidden',
  441. 'muc_temporary',
  442. 'muc_membersonly',
  443. 'muc_unmoderated',
  444. 'muc_nonanonymous'
  445. ];
  446. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
  447. const view = _converse.chatboxviews.get('lounge@montague.lit');
  448. await u.waitUntil(() => initializedOMEMO(_converse));
  449. const contact_jid = 'newguy@montague.lit';
  450. let stanza = $pres({
  451. 'to': 'romeo@montague.lit/orchard',
  452. 'from': 'lounge@montague.lit/newguy'
  453. })
  454. .c('x', {xmlns: Strophe.NS.MUC_USER})
  455. .c('item', {
  456. 'affiliation': 'none',
  457. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  458. 'role': 'participant'
  459. }).tree();
  460. _converse.connection._dataRecv(mock.createRequest(stanza));
  461. const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
  462. const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
  463. toggle.click();
  464. expect(view.model.get('omemo_active')).toBe(true);
  465. expect(view.model.get('omemo_supported')).toBe(true);
  466. const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
  467. textarea.value = 'This message will be encrypted';
  468. const bottom_panel = view.querySelector('converse-muc-bottom-panel');
  469. bottom_panel.onKeyDown({
  470. target: textarea,
  471. preventDefault: function preventDefault () {},
  472. keyCode: 13 // Enter
  473. });
  474. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  475. expect(Strophe.serialize(iq_stanza)).toBe(
  476. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  477. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  478. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  479. `</pubsub>`+
  480. `</iq>`);
  481. stanza = $iq({
  482. 'from': contact_jid,
  483. 'id': iq_stanza.getAttribute('id'),
  484. 'to': _converse.bare_jid,
  485. 'type': 'result',
  486. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  487. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  488. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  489. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  490. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  491. _converse.connection._dataRecv(mock.createRequest(stanza));
  492. await u.waitUntil(() => _converse.omemo_store);
  493. expect(_converse.devicelists.length).toBe(2);
  494. const devicelist = _converse.devicelists.get(contact_jid);
  495. await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  496. expect(devicelist.devices.length).toBe(1);
  497. expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  498. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'));
  499. stanza = $iq({
  500. 'from': _converse.bare_jid,
  501. 'id': iq_stanza.getAttribute('id'),
  502. 'to': _converse.bare_jid,
  503. 'type': 'result',
  504. }).c('pubsub', {
  505. 'xmlns': 'http://jabber.org/protocol/pubsub'
  506. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"})
  507. .c('item')
  508. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  509. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up()
  510. .c('signedPreKeySignature').t(btoa('200000')).up()
  511. .c('identityKey').t(btoa('300000')).up()
  512. .c('prekeys')
  513. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up()
  514. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up()
  515. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993'));
  516. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'));
  517. /* <iq xmlns="jabber:client" to="jc@opkode.com/converse.js-34183907" type="error" id="945c8ab3-b561-4d8a-92da-77c226bb1689:sendIQ" from="joris@konuro.net">
  518. * <pubsub xmlns="http://jabber.org/protocol/pubsub">
  519. * <items node="eu.siacs.conversations.axolotl.bundles:7580"/>
  520. * </pubsub>
  521. * <error code="401" type="auth">
  522. * <presence-subscription-required xmlns="http://jabber.org/protocol/pubsub#errors"/>
  523. * <not-authorized xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
  524. * </error>
  525. * </iq>
  526. */
  527. stanza = $iq({
  528. 'from': contact_jid,
  529. 'id': iq_stanza.getAttribute('id'),
  530. 'to': _converse.bare_jid,
  531. 'type': 'result',
  532. }).c('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub'})
  533. .c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}).up().up()
  534. .c('error', {'code': '401', 'type': 'auth'})
  535. .c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up()
  536. .c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
  537. _converse.connection._dataRecv(mock.createRequest(stanza));
  538. await u.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000);
  539. const header = document.querySelector('.alert-danger .modal-title');
  540. expect(header.textContent).toBe("Error");
  541. expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim())
  542. .toBe("Sorry, we're unable to send an encrypted message because newguy@montague.lit requires you "+
  543. "to be subscribed to their presence in order to see their OMEMO information");
  544. expect(view.model.get('omemo_supported')).toBe(false);
  545. expect(view.querySelector('.chat-textarea').value).toBe('This message will be encrypted');
  546. done();
  547. }));
  548. it("can receive a PreKeySignalMessage",
  549. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  550. _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test
  551. await mock.waitForRoster(_converse, 'current', 1);
  552. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  553. await u.waitUntil(() => initializedOMEMO(_converse));
  554. const obj = await omemo.encryptMessage('This is an encrypted message from the contact');
  555. // XXX: Normally the key will be encrypted via libsignal.
  556. // However, we're mocking libsignal in the tests, so we include
  557. // it as plaintext in the message.
  558. let stanza = $msg({
  559. 'from': contact_jid,
  560. 'to': _converse.connection.jid,
  561. 'type': 'chat',
  562. 'id': 'qwerty'
  563. }).c('body').t('This is a fallback message').up()
  564. .c('encrypted', {'xmlns': Strophe.NS.OMEMO})
  565. .c('header', {'sid': '555'})
  566. .c('key', {
  567. 'prekey': 'true',
  568. 'rid': _converse.omemo_store.get('device_id')
  569. }).t(u.arrayBufferToBase64(obj.key_and_tag)).up()
  570. .c('iv').t(obj.iv)
  571. .up().up()
  572. .c('payload').t(obj.payload);
  573. const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys;
  574. spyOn(_converse.omemo_store, 'generateMissingPreKeys').and.callFake(() => {
  575. // Since it's difficult to override
  576. // decryptPreKeyWhisperMessage, where a prekey will be
  577. // removed from the store, we do it here, before the
  578. // missing prekeys are generated.
  579. _converse.omemo_store.removePreKey(1);
  580. return generateMissingPreKeys.apply(_converse.omemo_store, arguments);
  581. });
  582. _converse.connection._dataRecv(mock.createRequest(stanza));
  583. let iq_stanza = await deviceListFetched(_converse, contact_jid);
  584. stanza = $iq({
  585. 'from': contact_jid,
  586. 'id': iq_stanza.getAttribute('id'),
  587. 'to': _converse.connection.jid,
  588. 'type': 'result',
  589. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  590. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  591. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  592. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  593. .c('device', {'id': '555'});
  594. // XXX: the bundle gets published twice, we want to make sure
  595. // that we wait for the 2nd, so we clear all the already sent
  596. // stanzas.
  597. _converse.connection.IQ_stanzas = [];
  598. _converse.connection._dataRecv(mock.createRequest(stanza));
  599. await u.waitUntil(() => _converse.omemo_store);
  600. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse), 1000);
  601. expect(Strophe.serialize(iq_stanza)).toBe(
  602. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">`+
  603. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  604. `<publish node="eu.siacs.conversations.axolotl.bundles:123456789">`+
  605. `<item>`+
  606. `<bundle xmlns="eu.siacs.conversations.axolotl">`+
  607. `<signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>`+
  608. `<signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>`+
  609. `<identityKey>${btoa("1234")}</identityKey>`+
  610. `<prekeys>`+
  611. `<preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>`+
  612. `<preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>`+
  613. `<preKeyPublic preKeyId="2">${btoa("1234")}</preKeyPublic>`+
  614. `<preKeyPublic preKeyId="3">${btoa("1234")}</preKeyPublic>`+
  615. `<preKeyPublic preKeyId="4">${btoa("1234")}</preKeyPublic>`+
  616. `</prekeys>`+
  617. `</bundle>`+
  618. `</item>`+
  619. `</publish>`+
  620. `<publish-options>`+
  621. `<x type="submit" xmlns="jabber:x:data">`+
  622. `<field type="hidden" var="FORM_TYPE">`+
  623. `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
  624. `</field>`+
  625. `<field var="pubsub#access_model">`+
  626. `<value>open</value>`+
  627. `</field>`+
  628. `</x>`+
  629. `</publish-options>`+
  630. `</pubsub>`+
  631. `</iq>`)
  632. const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id'));
  633. expect(own_device.get('bundle').prekeys.length).toBe(5);
  634. expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled();
  635. done();
  636. }));
  637. it("updates device lists based on PEP messages",
  638. mock.initConverse([], {'allow_non_roster_messaging': true}, async function (done, _converse) {
  639. await mock.waitUntilDiscoConfirmed(
  640. _converse, _converse.bare_jid,
  641. [{'category': 'pubsub', 'type': 'pep'}],
  642. ['http://jabber.org/protocol/pubsub#publish-options']
  643. );
  644. await mock.waitForRoster(_converse, 'current', 1);
  645. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  646. // Wait until own devices are fetched
  647. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
  648. expect(Strophe.serialize(iq_stanza)).toBe(
  649. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
  650. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  651. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  652. `</pubsub>`+
  653. `</iq>`);
  654. let stanza = $iq({
  655. 'from': _converse.bare_jid,
  656. 'id': iq_stanza.getAttribute('id'),
  657. 'to': _converse.bare_jid,
  658. 'type': 'result',
  659. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  660. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  661. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  662. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  663. .c('device', {'id': '555'});
  664. _converse.connection._dataRecv(mock.createRequest(stanza));
  665. await u.waitUntil(() => _converse.omemo_store);
  666. expect(_converse.chatboxes.length).toBe(1);
  667. expect(_converse.devicelists.length).toBe(1);
  668. const devicelist = _converse.devicelists.get(_converse.bare_jid);
  669. expect(devicelist.devices.length).toBe(2);
  670. expect(devicelist.devices.at(0).get('id')).toBe('555');
  671. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  672. iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse));
  673. stanza = $iq({
  674. 'from': _converse.bare_jid,
  675. 'id': iq_stanza.getAttribute('id'),
  676. 'to': _converse.bare_jid,
  677. 'type': 'result'});
  678. _converse.connection._dataRecv(mock.createRequest(stanza));
  679. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse));
  680. stanza = $iq({
  681. 'from': _converse.bare_jid,
  682. 'id': iq_stanza.getAttribute('id'),
  683. 'to': _converse.bare_jid,
  684. 'type': 'result'});
  685. _converse.connection._dataRecv(mock.createRequest(stanza));
  686. await _converse.api.waitUntil('OMEMOInitialized');
  687. stanza = $msg({
  688. 'from': contact_jid,
  689. 'to': _converse.bare_jid,
  690. 'type': 'headline',
  691. 'id': 'update_01',
  692. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  693. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  694. .c('item')
  695. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  696. .c('device', {'id': '1234'})
  697. .c('device', {'id': '4223'})
  698. _converse.connection._dataRecv(mock.createRequest(stanza));
  699. expect(_converse.devicelists.length).toBe(2);
  700. let devices = _converse.devicelists.get(contact_jid).devices;
  701. expect(devices.length).toBe(2);
  702. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223');
  703. stanza = $msg({
  704. 'from': contact_jid,
  705. 'to': _converse.bare_jid,
  706. 'type': 'headline',
  707. 'id': 'update_02',
  708. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  709. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  710. .c('item')
  711. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  712. .c('device', {'id': '4223'})
  713. .c('device', {'id': '4224'})
  714. _converse.connection._dataRecv(mock.createRequest(stanza));
  715. expect(_converse.devicelists.length).toBe(2);
  716. expect(devices.length).toBe(3);
  717. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223,4224');
  718. expect(devices.get('1234').get('active')).toBe(false);
  719. expect(devices.get('4223').get('active')).toBe(true);
  720. expect(devices.get('4224').get('active')).toBe(true);
  721. // Check that own devicelist gets updated
  722. stanza = $msg({
  723. 'from': _converse.bare_jid,
  724. 'to': _converse.bare_jid,
  725. 'type': 'headline',
  726. 'id': 'update_03',
  727. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  728. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  729. .c('item')
  730. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  731. .c('device', {'id': '123456789'})
  732. .c('device', {'id': '555'})
  733. .c('device', {'id': '777'})
  734. _converse.connection._dataRecv(mock.createRequest(stanza));
  735. expect(_converse.devicelists.length).toBe(2);
  736. devices = _converse.devicelists.get(_converse.bare_jid).devices;
  737. expect(devices.length).toBe(3);
  738. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,555,777');
  739. expect(devices.get('123456789').get('active')).toBe(true);
  740. expect(devices.get('555').get('active')).toBe(true);
  741. expect(devices.get('777').get('active')).toBe(true);
  742. _converse.connection.IQ_stanzas = [];
  743. // Check that own device gets re-added
  744. stanza = $msg({
  745. 'from': _converse.bare_jid,
  746. 'to': _converse.bare_jid,
  747. 'type': 'headline',
  748. 'id': 'update_04',
  749. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  750. .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'})
  751. .c('item')
  752. .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'})
  753. .c('device', {'id': '444'})
  754. _converse.connection._dataRecv(mock.createRequest(stanza));
  755. iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse));
  756. // Check that our own device is added again, but that removed
  757. // devices are not added.
  758. expect(Strophe.serialize(iq_stanza)).toBe(
  759. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">`+
  760. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  761. `<publish node="eu.siacs.conversations.axolotl.devicelist">`+
  762. `<item id="current">`+
  763. `<list xmlns="eu.siacs.conversations.axolotl">`+
  764. `<device id="123456789"/>`+
  765. `<device id="444"/>`+
  766. `</list>`+
  767. `</item>`+
  768. `</publish>`+
  769. `<publish-options>`+
  770. `<x type="submit" xmlns="jabber:x:data">`+
  771. `<field type="hidden" var="FORM_TYPE">`+
  772. `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
  773. `</field>`+
  774. `<field var="pubsub#access_model">`+
  775. `<value>open</value>`+
  776. `</field>`+
  777. `</x>`+
  778. `</publish-options>`+
  779. `</pubsub>`+
  780. `</iq>`);
  781. expect(_converse.devicelists.length).toBe(2);
  782. devices = _converse.devicelists.get(_converse.bare_jid).devices;
  783. // The device id for this device (123456789) was also generated and added to the list,
  784. // which is why we have 2 devices now.
  785. expect(devices.length).toBe(4);
  786. expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,444,555,777');
  787. expect(devices.get('123456789').get('active')).toBe(true);
  788. expect(devices.get('444').get('active')).toBe(true);
  789. expect(devices.get('555').get('active')).toBe(false);
  790. expect(devices.get('777').get('active')).toBe(false);
  791. done();
  792. }));
  793. it("updates device bundles based on PEP messages",
  794. mock.initConverse([], {}, async function (done, _converse) {
  795. await mock.waitUntilDiscoConfirmed(
  796. _converse, _converse.bare_jid,
  797. [{'category': 'pubsub', 'type': 'pep'}],
  798. ['http://jabber.org/protocol/pubsub#publish-options']
  799. );
  800. await mock.waitForRoster(_converse, 'current');
  801. const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  802. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
  803. expect(Strophe.serialize(iq_stanza)).toBe(
  804. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
  805. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  806. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  807. `</pubsub>`+
  808. `</iq>`);
  809. let stanza = $iq({
  810. 'from': contact_jid,
  811. 'id': iq_stanza.getAttribute('id'),
  812. 'to': _converse.bare_jid,
  813. 'type': 'result',
  814. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  815. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  816. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  817. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  818. .c('device', {'id': '555'});
  819. _converse.connection._dataRecv(mock.createRequest(stanza));
  820. await await u.waitUntil(() => _converse.omemo_store);
  821. expect(_converse.devicelists.length).toBe(1);
  822. let devicelist = _converse.devicelists.get(_converse.bare_jid);
  823. expect(devicelist.devices.length).toBe(2);
  824. expect(devicelist.devices.at(0).get('id')).toBe('555');
  825. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  826. iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse));
  827. stanza = $iq({
  828. 'from': _converse.bare_jid,
  829. 'id': iq_stanza.getAttribute('id'),
  830. 'to': _converse.bare_jid,
  831. 'type': 'result'});
  832. _converse.connection._dataRecv(mock.createRequest(stanza));
  833. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse));
  834. stanza = $iq({
  835. 'from': _converse.bare_jid,
  836. 'id': iq_stanza.getAttribute('id'),
  837. 'to': _converse.bare_jid,
  838. 'type': 'result'});
  839. _converse.connection._dataRecv(mock.createRequest(stanza));
  840. await _converse.api.waitUntil('OMEMOInitialized');
  841. stanza = $msg({
  842. 'from': contact_jid,
  843. 'to': _converse.bare_jid,
  844. 'type': 'headline',
  845. 'id': 'update_01',
  846. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  847. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'})
  848. .c('item')
  849. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  850. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('1111').up()
  851. .c('signedPreKeySignature').t('2222').up()
  852. .c('identityKey').t('3333').up()
  853. .c('prekeys')
  854. .c('preKeyPublic', {'preKeyId': '1001'}).up()
  855. .c('preKeyPublic', {'preKeyId': '1002'}).up()
  856. .c('preKeyPublic', {'preKeyId': '1003'});
  857. _converse.connection._dataRecv(mock.createRequest(stanza));
  858. expect(_converse.devicelists.length).toBe(2);
  859. devicelist = _converse.devicelists.get(contact_jid);
  860. expect(devicelist.devices.length).toBe(1);
  861. let device = devicelist.devices.at(0);
  862. expect(device.get('bundle').identity_key).toBe('3333');
  863. expect(device.get('bundle').signed_prekey.public_key).toBe('1111');
  864. expect(device.get('bundle').signed_prekey.id).toBe(4223);
  865. expect(device.get('bundle').signed_prekey.signature).toBe('2222');
  866. expect(device.get('bundle').prekeys.length).toBe(3);
  867. expect(device.get('bundle').prekeys[0].id).toBe(1001);
  868. expect(device.get('bundle').prekeys[1].id).toBe(1002);
  869. expect(device.get('bundle').prekeys[2].id).toBe(1003);
  870. stanza = $msg({
  871. 'from': contact_jid,
  872. 'to': _converse.bare_jid,
  873. 'type': 'headline',
  874. 'id': 'update_02',
  875. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  876. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'})
  877. .c('item')
  878. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  879. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('5555').up()
  880. .c('signedPreKeySignature').t('6666').up()
  881. .c('identityKey').t('7777').up()
  882. .c('prekeys')
  883. .c('preKeyPublic', {'preKeyId': '2001'}).up()
  884. .c('preKeyPublic', {'preKeyId': '2002'}).up()
  885. .c('preKeyPublic', {'preKeyId': '2003'});
  886. _converse.connection._dataRecv(mock.createRequest(stanza));
  887. expect(_converse.devicelists.length).toBe(2);
  888. devicelist = _converse.devicelists.get(contact_jid);
  889. expect(devicelist.devices.length).toBe(1);
  890. device = devicelist.devices.at(0);
  891. expect(device.get('bundle').identity_key).toBe('7777');
  892. expect(device.get('bundle').signed_prekey.public_key).toBe('5555');
  893. expect(device.get('bundle').signed_prekey.id).toBe(4223);
  894. expect(device.get('bundle').signed_prekey.signature).toBe('6666');
  895. expect(device.get('bundle').prekeys.length).toBe(3);
  896. expect(device.get('bundle').prekeys[0].id).toBe(2001);
  897. expect(device.get('bundle').prekeys[1].id).toBe(2002);
  898. expect(device.get('bundle').prekeys[2].id).toBe(2003);
  899. stanza = $msg({
  900. 'from': _converse.bare_jid,
  901. 'to': _converse.bare_jid,
  902. 'type': 'headline',
  903. 'id': 'update_03',
  904. }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
  905. .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:123456789'})
  906. .c('item')
  907. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  908. .c('signedPreKeyPublic', {'signedPreKeyId': '9999'}).t('8888').up()
  909. .c('signedPreKeySignature').t('3333').up()
  910. .c('identityKey').t('1111').up()
  911. .c('prekeys')
  912. .c('preKeyPublic', {'preKeyId': '3001'}).up()
  913. .c('preKeyPublic', {'preKeyId': '3002'}).up()
  914. .c('preKeyPublic', {'preKeyId': '3003'});
  915. _converse.connection._dataRecv(mock.createRequest(stanza));
  916. expect(_converse.devicelists.length).toBe(2);
  917. devicelist = _converse.devicelists.get(_converse.bare_jid);
  918. expect(devicelist.devices.length).toBe(2);
  919. expect(devicelist.devices.at(0).get('id')).toBe('555');
  920. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  921. device = devicelist.devices.at(1);
  922. expect(device.get('bundle').identity_key).toBe('1111');
  923. expect(device.get('bundle').signed_prekey.public_key).toBe('8888');
  924. expect(device.get('bundle').signed_prekey.id).toBe(9999);
  925. expect(device.get('bundle').signed_prekey.signature).toBe('3333');
  926. expect(device.get('bundle').prekeys.length).toBe(3);
  927. expect(device.get('bundle').prekeys[0].id).toBe(3001);
  928. expect(device.get('bundle').prekeys[1].id).toBe(3002);
  929. expect(device.get('bundle').prekeys[2].id).toBe(3003);
  930. done();
  931. }));
  932. it("publishes a bundle with which an encrypted session can be created",
  933. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  934. await mock.waitUntilDiscoConfirmed(
  935. _converse, _converse.bare_jid,
  936. [{'category': 'pubsub', 'type': 'pep'}],
  937. ['http://jabber.org/protocol/pubsub#publish-options']
  938. );
  939. _converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test
  940. await mock.waitForRoster(_converse, 'current', 1);
  941. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  942. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
  943. let stanza = $iq({
  944. 'from': contact_jid,
  945. 'id': iq_stanza.getAttribute('id'),
  946. 'to': _converse.bare_jid,
  947. 'type': 'result',
  948. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  949. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  950. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  951. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  952. .c('device', {'id': '482886413b977930064a5888b92134fe'});
  953. _converse.connection._dataRecv(mock.createRequest(stanza));
  954. expect(_converse.devicelists.length).toBe(1);
  955. await mock.openChatBoxFor(_converse, contact_jid);
  956. iq_stanza = await ownDeviceHasBeenPublished(_converse);
  957. stanza = $iq({
  958. 'from': _converse.bare_jid,
  959. 'id': iq_stanza.getAttribute('id'),
  960. 'to': _converse.bare_jid,
  961. 'type': 'result'});
  962. _converse.connection._dataRecv(mock.createRequest(stanza));
  963. iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse));
  964. expect(Strophe.serialize(iq_stanza)).toBe(
  965. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" type="set" xmlns="jabber:client">`+
  966. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  967. `<publish node="eu.siacs.conversations.axolotl.bundles:123456789">`+
  968. `<item>`+
  969. `<bundle xmlns="eu.siacs.conversations.axolotl">`+
  970. `<signedPreKeyPublic signedPreKeyId="0">${btoa("1234")}</signedPreKeyPublic>`+
  971. `<signedPreKeySignature>${btoa("11112222333344445555")}</signedPreKeySignature>`+
  972. `<identityKey>${btoa("1234")}</identityKey>`+
  973. `<prekeys>`+
  974. `<preKeyPublic preKeyId="0">${btoa("1234")}</preKeyPublic>`+
  975. `<preKeyPublic preKeyId="1">${btoa("1234")}</preKeyPublic>`+
  976. `</prekeys>`+
  977. `</bundle>`+
  978. `</item>`+
  979. `</publish>`+
  980. `<publish-options>`+
  981. `<x type="submit" xmlns="jabber:x:data">`+
  982. `<field type="hidden" var="FORM_TYPE">`+
  983. `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
  984. `</field>`+
  985. `<field var="pubsub#access_model">`+
  986. `<value>open</value>`+
  987. `</field>`+
  988. `</x>`+
  989. `</publish-options>`+
  990. `</pubsub>`+
  991. `</iq>`)
  992. stanza = $iq({
  993. 'from': _converse.bare_jid,
  994. 'id': iq_stanza.getAttribute('id'),
  995. 'to': _converse.bare_jid,
  996. 'type': 'result'});
  997. _converse.connection._dataRecv(mock.createRequest(stanza));
  998. await _converse.api.waitUntil('OMEMOInitialized');
  999. done();
  1000. }));
  1001. it("adds a toolbar button for starting an encrypted chat session",
  1002. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  1003. await mock.waitUntilDiscoConfirmed(
  1004. _converse, _converse.bare_jid,
  1005. [{'category': 'pubsub', 'type': 'pep'}],
  1006. ['http://jabber.org/protocol/pubsub#publish-options']
  1007. );
  1008. await mock.waitForRoster(_converse, 'current', 1);
  1009. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1010. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid));
  1011. expect(Strophe.serialize(iq_stanza)).toBe(
  1012. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
  1013. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1014. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1015. `</pubsub>`+
  1016. `</iq>`);
  1017. let stanza = $iq({
  1018. 'from': _converse.bare_jid,
  1019. 'id': iq_stanza.getAttribute('id'),
  1020. 'to': _converse.bare_jid,
  1021. 'type': 'result',
  1022. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1023. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1024. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1025. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1026. .c('device', {'id': '482886413b977930064a5888b92134fe'});
  1027. _converse.connection._dataRecv(mock.createRequest(stanza));
  1028. await u.waitUntil(() => _converse.omemo_store);
  1029. expect(_converse.devicelists.length).toBe(1);
  1030. let devicelist = _converse.devicelists.get(_converse.bare_jid);
  1031. expect(devicelist.devices.length).toBe(2);
  1032. expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe');
  1033. expect(devicelist.devices.at(1).get('id')).toBe('123456789');
  1034. // Check that own device was published
  1035. iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse));
  1036. expect(Strophe.serialize(iq_stanza)).toBe(
  1037. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute(`id`)}" type="set" xmlns="jabber:client">`+
  1038. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1039. `<publish node="eu.siacs.conversations.axolotl.devicelist">`+
  1040. `<item id="current">`+
  1041. `<list xmlns="eu.siacs.conversations.axolotl">`+
  1042. `<device id="482886413b977930064a5888b92134fe"/>`+
  1043. `<device id="123456789"/>`+
  1044. `</list>`+
  1045. `</item>`+
  1046. `</publish>`+
  1047. `<publish-options>`+
  1048. `<x type="submit" xmlns="jabber:x:data">`+
  1049. `<field type="hidden" var="FORM_TYPE">`+
  1050. `<value>http://jabber.org/protocol/pubsub#publish-options</value>`+
  1051. `</field>`+
  1052. `<field var="pubsub#access_model">`+
  1053. `<value>open</value>`+
  1054. `</field>`+
  1055. `</x>`+
  1056. `</publish-options>`+
  1057. `</pubsub>`+
  1058. `</iq>`);
  1059. stanza = $iq({
  1060. 'from': _converse.bare_jid,
  1061. 'id': iq_stanza.getAttribute('id'),
  1062. 'to': _converse.bare_jid,
  1063. 'type': 'result'});
  1064. _converse.connection._dataRecv(mock.createRequest(stanza));
  1065. const iq_el = await u.waitUntil(() => bundleHasBeenPublished(_converse));
  1066. expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join());
  1067. expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100);
  1068. const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic');
  1069. expect(signed_prekeys.length).toBe(1);
  1070. const signed_prekey = signed_prekeys[0];
  1071. expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0')
  1072. expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1);
  1073. expect(iq_el.querySelectorAll('identityKey').length).toBe(1);
  1074. stanza = $iq({
  1075. 'from': _converse.bare_jid,
  1076. 'id': iq_el.getAttribute('id'),
  1077. 'to': _converse.bare_jid,
  1078. 'type': 'result'});
  1079. _converse.connection._dataRecv(mock.createRequest(stanza));
  1080. await _converse.api.waitUntil('OMEMOInitialized', 1000);
  1081. await mock.openChatBoxFor(_converse, contact_jid);
  1082. iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1083. expect(Strophe.serialize(iq_stanza)).toBe(
  1084. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  1085. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1086. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1087. `</pubsub>`+
  1088. `</iq>`);
  1089. stanza = $iq({
  1090. 'from': contact_jid,
  1091. 'id': iq_stanza.getAttribute('id'),
  1092. 'to': _converse.bare_jid,
  1093. 'type': 'result',
  1094. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1095. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1096. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1097. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1098. .c('device', {'id': '368866411b877c30064a5f62b917cffe'}).up()
  1099. .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up()
  1100. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  1101. .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
  1102. _converse.connection._dataRecv(mock.createRequest(stanza));
  1103. devicelist = _converse.devicelists.get(contact_jid);
  1104. await u.waitUntil(() => devicelist.devices.length);
  1105. expect(_converse.devicelists.length).toBe(2);
  1106. devicelist = _converse.devicelists.get(contact_jid);
  1107. expect(devicelist.devices.length).toBe(4);
  1108. expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe');
  1109. expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c');
  1110. expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  1111. expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
  1112. await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).querySelector('.chat-toolbar'));
  1113. const view = _converse.chatboxviews.get(contact_jid);
  1114. const toolbar = view.querySelector('.chat-toolbar');
  1115. expect(view.model.get('omemo_active')).toBe(undefined);
  1116. const toggle = toolbar.querySelector('.toggle-omemo');
  1117. expect(toggle === null).toBe(false);
  1118. expect(u.hasClass('fa-unlock', toggle.querySelector('converse-icon'))).toBe(true);
  1119. expect(u.hasClass('fa-lock', toggle.querySelector('.converse-icon'))).toBe(false);
  1120. view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
  1121. toolbar.querySelector('.toggle-omemo').click();
  1122. expect(view.model.get('omemo_active')).toBe(true);
  1123. await u.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1124. let icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1125. expect(u.hasClass('fa-unlock', icon)).toBe(false);
  1126. expect(u.hasClass('fa-lock', icon)).toBe(true);
  1127. const textarea = view.querySelector('.chat-textarea');
  1128. textarea.value = 'This message will be sent encrypted';
  1129. const bottom_panel = view.querySelector('converse-chat-bottom-panel');
  1130. bottom_panel.onKeyDown({
  1131. target: textarea,
  1132. preventDefault: function preventDefault () {},
  1133. keyCode: 13
  1134. });
  1135. view.model.save({'omemo_supported': false});
  1136. await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
  1137. icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1138. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1139. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1140. view.model.save({'omemo_supported': true});
  1141. await u.waitUntil(() => !toolbar.querySelector('.toggle-omemo').disabled);
  1142. icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1143. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1144. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1145. done();
  1146. }));
  1147. it("adds a toolbar button for starting an encrypted groupchat session",
  1148. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  1149. await mock.waitForRoster(_converse, 'current', 0);
  1150. await mock.waitUntilDiscoConfirmed(
  1151. _converse, _converse.bare_jid,
  1152. [{'category': 'pubsub', 'type': 'pep'}],
  1153. ['http://jabber.org/protocol/pubsub#publish-options']
  1154. );
  1155. // MEMO encryption works only in members-only conferences that are non-anonymous.
  1156. const features = [
  1157. 'http://jabber.org/protocol/muc',
  1158. 'jabber:iq:register',
  1159. 'muc_passwordprotected',
  1160. 'muc_hidden',
  1161. 'muc_temporary',
  1162. 'muc_membersonly',
  1163. 'muc_unmoderated',
  1164. 'muc_nonanonymous'
  1165. ];
  1166. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
  1167. const view = _converse.chatboxviews.get('lounge@montague.lit');
  1168. await u.waitUntil(() => initializedOMEMO(_converse));
  1169. const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
  1170. let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
  1171. expect(view.model.get('omemo_active')).toBe(undefined);
  1172. expect(view.model.get('omemo_supported')).toBe(true);
  1173. await u.waitUntil(() => !toggle.disabled);
  1174. let icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1175. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1176. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1177. toggle.click();
  1178. toggle = toolbar.querySelector('.toggle-omemo');
  1179. expect(!!toggle.disabled).toBe(false);
  1180. expect(view.model.get('omemo_active')).toBe(true);
  1181. expect(view.model.get('omemo_supported')).toBe(true);
  1182. await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1183. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1184. let contact_jid = 'newguy@montague.lit';
  1185. let stanza = $pres({
  1186. to: 'romeo@montague.lit/orchard',
  1187. from: 'lounge@montague.lit/newguy'
  1188. })
  1189. .c('x', {xmlns: Strophe.NS.MUC_USER})
  1190. .c('item', {
  1191. 'affiliation': 'none',
  1192. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  1193. 'role': 'participant'
  1194. }).tree();
  1195. _converse.connection._dataRecv(mock.createRequest(stanza));
  1196. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1197. expect(Strophe.serialize(iq_stanza)).toBe(
  1198. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  1199. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1200. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1201. `</pubsub>`+
  1202. `</iq>`);
  1203. stanza = $iq({
  1204. 'from': contact_jid,
  1205. 'id': iq_stanza.getAttribute('id'),
  1206. 'to': _converse.bare_jid,
  1207. 'type': 'result',
  1208. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1209. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1210. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1211. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1212. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  1213. .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
  1214. _converse.connection._dataRecv(mock.createRequest(stanza));
  1215. await u.waitUntil(() => _converse.omemo_store);
  1216. expect(_converse.devicelists.length).toBe(2);
  1217. await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1218. const devicelist = _converse.devicelists.get(contact_jid);
  1219. expect(devicelist.devices.length).toBe(2);
  1220. expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  1221. expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
  1222. expect(view.model.get('omemo_active')).toBe(true);
  1223. toggle = toolbar.querySelector('.toggle-omemo');
  1224. expect(toggle === null).toBe(false);
  1225. expect(!!toggle.disabled).toBe(false);
  1226. expect(view.model.get('omemo_supported')).toBe(true);
  1227. await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1228. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1229. // Test that the button gets disabled when the room becomes
  1230. // anonymous or semi-anonymous
  1231. view.model.features.save({'nonanonymous': false, 'semianonymous': true});
  1232. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1233. await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
  1234. view.model.features.save({'nonanonymous': true, 'semianonymous': false});
  1235. await u.waitUntil(() => view.model.get('omemo_supported'));
  1236. await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
  1237. expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1238. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
  1239. expect(!!view.querySelector('.toggle-omemo').disabled).toBe(false);
  1240. // Test that the button gets disabled when the room becomes open
  1241. view.model.features.save({'membersonly': false, 'open': true});
  1242. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1243. await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
  1244. view.model.features.save({'membersonly': true, 'open': false});
  1245. await u.waitUntil(() => view.model.get('omemo_supported'));
  1246. await u.waitUntil(() => !view.querySelector('.toggle-omemo').disabled);
  1247. expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1248. expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
  1249. expect(view.model.get('omemo_supported')).toBe(true);
  1250. expect(view.model.get('omemo_active')).toBe(false);
  1251. view.querySelector('.toggle-omemo').click();
  1252. expect(view.model.get('omemo_active')).toBe(true);
  1253. // Someone enters the room who doesn't have OMEMO support, while we
  1254. // have OMEMO activated...
  1255. contact_jid = 'oldguy@montague.lit';
  1256. stanza = $pres({
  1257. to: 'romeo@montague.lit/orchard',
  1258. from: 'lounge@montague.lit/oldguy'
  1259. })
  1260. .c('x', {xmlns: Strophe.NS.MUC_USER})
  1261. .c('item', {
  1262. 'affiliation': 'none',
  1263. 'jid': `${contact_jid}/_converse.js-290929788`,
  1264. 'role': 'participant'
  1265. }).tree();
  1266. _converse.connection._dataRecv(mock.createRequest(stanza));
  1267. iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1268. expect(Strophe.serialize(iq_stanza)).toBe(
  1269. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  1270. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1271. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1272. `</pubsub>`+
  1273. `</iq>`);
  1274. stanza = $iq({
  1275. 'from': contact_jid,
  1276. 'id': iq_stanza.getAttribute('id'),
  1277. 'to': _converse.bare_jid,
  1278. 'type': 'error'
  1279. }).c('error', {'type': 'cancel'})
  1280. .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
  1281. _converse.connection._dataRecv(mock.createRequest(stanza));
  1282. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1283. await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
  1284. "oldguy doesn't appear to have a client that supports OMEMO. "+
  1285. "Encrypted chat will no longer be possible in this grouchat."
  1286. );
  1287. await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
  1288. icon = view.querySelector('.toggle-omemo converse-icon');
  1289. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1290. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1291. expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages');
  1292. done();
  1293. }));
  1294. it("shows OMEMO device fingerprints in the user details modal",
  1295. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  1296. await mock.waitUntilDiscoConfirmed(
  1297. _converse, _converse.bare_jid,
  1298. [{'category': 'pubsub', 'type': 'pep'}],
  1299. ['http://jabber.org/protocol/pubsub#publish-options']
  1300. );
  1301. await mock.waitForRoster(_converse, 'current', 1);
  1302. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1303. await mock.openChatBoxFor(_converse, contact_jid)
  1304. // We simply emit, to avoid doing all the setup work
  1305. _converse.api.trigger('OMEMOInitialized');
  1306. const view = _converse.chatboxviews.get(contact_jid);
  1307. const show_modal_button = view.querySelector('.show-user-details-modal');
  1308. show_modal_button.click();
  1309. const modal = _converse.api.modal.get('user-details-modal');
  1310. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  1311. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1312. expect(Strophe.serialize(iq_stanza)).toBe(
  1313. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="mercutio@montague.lit" type="get" xmlns="jabber:client">`+
  1314. `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub>`+
  1315. `</iq>`);
  1316. let stanza = $iq({
  1317. 'from': contact_jid,
  1318. 'id': iq_stanza.getAttribute('id'),
  1319. 'to': _converse.bare_jid,
  1320. 'type': 'result',
  1321. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1322. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1323. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1324. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1325. .c('device', {'id': '555'});
  1326. _converse.connection._dataRecv(mock.createRequest(stanza));
  1327. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  1328. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555'));
  1329. expect(Strophe.serialize(iq_stanza)).toBe(
  1330. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="mercutio@montague.lit" type="get" xmlns="jabber:client">`+
  1331. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1332. `<items node="eu.siacs.conversations.axolotl.bundles:555"/>`+
  1333. `</pubsub>`+
  1334. `</iq>`);
  1335. stanza = $iq({
  1336. 'from': contact_jid,
  1337. 'id': iq_stanza.getAttribute('id'),
  1338. 'to': _converse.bare_jid,
  1339. 'type': 'result',
  1340. }).c('pubsub', {
  1341. 'xmlns': 'http://jabber.org/protocol/pubsub'
  1342. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
  1343. .c('item')
  1344. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  1345. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  1346. .c('signedPreKeySignature').t(btoa('2222')).up()
  1347. .c('identityKey').t('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI').up()
  1348. .c('prekeys')
  1349. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  1350. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  1351. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  1352. _converse.connection._dataRecv(mock.createRequest(stanza));
  1353. await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
  1354. expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
  1355. const el = modal.el.querySelector('.fingerprints .fingerprint');
  1356. expect(el.textContent.trim()).toBe(
  1357. u.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI')))
  1358. );
  1359. expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
  1360. const devicelist = _converse.devicelists.get(contact_jid);
  1361. expect(devicelist.devices.get('555').get('trusted')).toBe(0);
  1362. let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
  1363. expect(trusted_radio.checked).toBe(true);
  1364. let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
  1365. expect(untrusted_radio.checked).toBe(false);
  1366. // Test that the device can be set to untrusted
  1367. untrusted_radio.click();
  1368. trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]');
  1369. expect(trusted_radio.hasAttribute('checked')).toBe(false);
  1370. expect(devicelist.devices.get('555').get('trusted')).toBe(-1);
  1371. untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]');
  1372. expect(untrusted_radio.hasAttribute('checked')).toBe(true);
  1373. trusted_radio.click();
  1374. expect(devicelist.devices.get('555').get('trusted')).toBe(1);
  1375. done();
  1376. }));
  1377. });