omemo.js 85 KB

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