omemo.js 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521
  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. toolbar.querySelector('.toggle-omemo').click();
  1121. expect(view.model.get('omemo_active')).toBe(true);
  1122. await u.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1123. let icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1124. expect(u.hasClass('fa-unlock', icon)).toBe(false);
  1125. expect(u.hasClass('fa-lock', icon)).toBe(true);
  1126. const textarea = view.querySelector('.chat-textarea');
  1127. textarea.value = 'This message will be sent encrypted';
  1128. const bottom_panel = view.querySelector('converse-chat-bottom-panel');
  1129. bottom_panel.onKeyDown({
  1130. target: textarea,
  1131. preventDefault: function preventDefault () {},
  1132. keyCode: 13
  1133. });
  1134. view.model.save({'omemo_supported': false});
  1135. await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
  1136. icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1137. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1138. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1139. view.model.save({'omemo_supported': true});
  1140. await u.waitUntil(() => !toolbar.querySelector('.toggle-omemo').disabled);
  1141. icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1142. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1143. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1144. done();
  1145. }));
  1146. it("adds a toolbar button for starting an encrypted groupchat session",
  1147. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  1148. await mock.waitForRoster(_converse, 'current', 0);
  1149. await mock.waitUntilDiscoConfirmed(
  1150. _converse, _converse.bare_jid,
  1151. [{'category': 'pubsub', 'type': 'pep'}],
  1152. ['http://jabber.org/protocol/pubsub#publish-options']
  1153. );
  1154. // MEMO encryption works only in members-only conferences that are non-anonymous.
  1155. const features = [
  1156. 'http://jabber.org/protocol/muc',
  1157. 'jabber:iq:register',
  1158. 'muc_passwordprotected',
  1159. 'muc_hidden',
  1160. 'muc_temporary',
  1161. 'muc_membersonly',
  1162. 'muc_unmoderated',
  1163. 'muc_nonanonymous'
  1164. ];
  1165. await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
  1166. const view = _converse.chatboxviews.get('lounge@montague.lit');
  1167. await u.waitUntil(() => initializedOMEMO(_converse));
  1168. const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
  1169. let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
  1170. expect(view.model.get('omemo_active')).toBe(undefined);
  1171. expect(view.model.get('omemo_supported')).toBe(true);
  1172. await u.waitUntil(() => !toggle.disabled);
  1173. let icon = toolbar.querySelector('.toggle-omemo converse-icon');
  1174. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1175. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1176. toggle.click();
  1177. toggle = toolbar.querySelector('.toggle-omemo');
  1178. expect(!!toggle.disabled).toBe(false);
  1179. expect(view.model.get('omemo_active')).toBe(true);
  1180. expect(view.model.get('omemo_supported')).toBe(true);
  1181. await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1182. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1183. let contact_jid = 'newguy@montague.lit';
  1184. let stanza = $pres({
  1185. to: 'romeo@montague.lit/orchard',
  1186. from: 'lounge@montague.lit/newguy'
  1187. })
  1188. .c('x', {xmlns: Strophe.NS.MUC_USER})
  1189. .c('item', {
  1190. 'affiliation': 'none',
  1191. 'jid': 'newguy@montague.lit/_converse.js-290929789',
  1192. 'role': 'participant'
  1193. }).tree();
  1194. _converse.connection._dataRecv(mock.createRequest(stanza));
  1195. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1196. expect(Strophe.serialize(iq_stanza)).toBe(
  1197. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  1198. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1199. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1200. `</pubsub>`+
  1201. `</iq>`);
  1202. stanza = $iq({
  1203. 'from': contact_jid,
  1204. 'id': iq_stanza.getAttribute('id'),
  1205. 'to': _converse.bare_jid,
  1206. 'type': 'result',
  1207. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1208. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1209. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1210. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1211. .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up()
  1212. .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'});
  1213. _converse.connection._dataRecv(mock.createRequest(stanza));
  1214. await u.waitUntil(() => _converse.omemo_store);
  1215. expect(_converse.devicelists.length).toBe(2);
  1216. await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1217. const devicelist = _converse.devicelists.get(contact_jid);
  1218. expect(devicelist.devices.length).toBe(2);
  1219. expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228');
  1220. expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901');
  1221. expect(view.model.get('omemo_active')).toBe(true);
  1222. toggle = toolbar.querySelector('.toggle-omemo');
  1223. expect(toggle === null).toBe(false);
  1224. expect(!!toggle.disabled).toBe(false);
  1225. expect(view.model.get('omemo_supported')).toBe(true);
  1226. await u.waitUntil(() => !u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon')));
  1227. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1228. // Test that the button gets disabled when the room becomes
  1229. // anonymous or semi-anonymous
  1230. view.model.features.save({'nonanonymous': false, 'semianonymous': true});
  1231. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1232. await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
  1233. view.model.features.save({'nonanonymous': true, 'semianonymous': false});
  1234. await u.waitUntil(() => view.model.get('omemo_supported'));
  1235. await u.waitUntil(() => view.querySelector('.toggle-omemo') !== null);
  1236. expect(u.hasClass('fa-unlock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1237. expect(u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo converse-icon'))).toBe(false);
  1238. expect(!!view.querySelector('.toggle-omemo').disabled).toBe(false);
  1239. // Test that the button gets disabled when the room becomes open
  1240. view.model.features.save({'membersonly': false, 'open': true});
  1241. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1242. await u.waitUntil(() => view.querySelector('.toggle-omemo').disabled);
  1243. view.model.features.save({'membersonly': true, 'open': false});
  1244. await u.waitUntil(() => view.model.get('omemo_supported'));
  1245. await u.waitUntil(() => !view.querySelector('.toggle-omemo').disabled);
  1246. expect(u.hasClass('fa-unlock', view.querySelector('.toggle-omemo converse-icon'))).toBe(true);
  1247. expect(u.hasClass('fa-lock', view.querySelector('.toggle-omemo converse-icon'))).toBe(false);
  1248. expect(view.model.get('omemo_supported')).toBe(true);
  1249. expect(view.model.get('omemo_active')).toBe(false);
  1250. view.querySelector('.toggle-omemo').click();
  1251. expect(view.model.get('omemo_active')).toBe(true);
  1252. // Someone enters the room who doesn't have OMEMO support, while we
  1253. // have OMEMO activated...
  1254. contact_jid = 'oldguy@montague.lit';
  1255. stanza = $pres({
  1256. to: 'romeo@montague.lit/orchard',
  1257. from: 'lounge@montague.lit/oldguy'
  1258. })
  1259. .c('x', {xmlns: Strophe.NS.MUC_USER})
  1260. .c('item', {
  1261. 'affiliation': 'none',
  1262. 'jid': `${contact_jid}/_converse.js-290929788`,
  1263. 'role': 'participant'
  1264. }).tree();
  1265. _converse.connection._dataRecv(mock.createRequest(stanza));
  1266. iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1267. expect(Strophe.serialize(iq_stanza)).toBe(
  1268. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="${contact_jid}" type="get" xmlns="jabber:client">`+
  1269. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1270. `<items node="eu.siacs.conversations.axolotl.devicelist"/>`+
  1271. `</pubsub>`+
  1272. `</iq>`);
  1273. stanza = $iq({
  1274. 'from': contact_jid,
  1275. 'id': iq_stanza.getAttribute('id'),
  1276. 'to': _converse.bare_jid,
  1277. 'type': 'error'
  1278. }).c('error', {'type': 'cancel'})
  1279. .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"});
  1280. _converse.connection._dataRecv(mock.createRequest(stanza));
  1281. await u.waitUntil(() => !view.model.get('omemo_supported'));
  1282. await u.waitUntil(() => view.querySelector('.chat-error .chat-info__message')?.textContent.trim() ===
  1283. "oldguy doesn't appear to have a client that supports OMEMO. "+
  1284. "Encrypted chat will no longer be possible in this grouchat."
  1285. );
  1286. await u.waitUntil(() => toolbar.querySelector('.toggle-omemo').disabled);
  1287. icon = view.querySelector('.toggle-omemo converse-icon');
  1288. expect(u.hasClass('fa-unlock', icon)).toBe(true);
  1289. expect(u.hasClass('fa-lock', icon)).toBe(false);
  1290. expect(toolbar.querySelector('.toggle-omemo').title).toBe('This groupchat needs to be members-only and non-anonymous in order to support OMEMO encrypted messages');
  1291. done();
  1292. }));
  1293. it("shows OMEMO device fingerprints in the user details modal",
  1294. mock.initConverse(['chatBoxesFetched'], {}, async function (done, _converse) {
  1295. await mock.waitUntilDiscoConfirmed(
  1296. _converse, _converse.bare_jid,
  1297. [{'category': 'pubsub', 'type': 'pep'}],
  1298. ['http://jabber.org/protocol/pubsub#publish-options']
  1299. );
  1300. await mock.waitForRoster(_converse, 'current', 1);
  1301. const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
  1302. await mock.openChatBoxFor(_converse, contact_jid)
  1303. // We simply emit, to avoid doing all the setup work
  1304. _converse.api.trigger('OMEMOInitialized');
  1305. const view = _converse.chatboxviews.get(contact_jid);
  1306. const show_modal_button = view.querySelector('.show-user-details-modal');
  1307. show_modal_button.click();
  1308. const modal = _converse.api.modal.get('user-details-modal');
  1309. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  1310. let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid));
  1311. expect(Strophe.serialize(iq_stanza)).toBe(
  1312. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="mercutio@montague.lit" type="get" xmlns="jabber:client">`+
  1313. `<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub>`+
  1314. `</iq>`);
  1315. let stanza = $iq({
  1316. 'from': contact_jid,
  1317. 'id': iq_stanza.getAttribute('id'),
  1318. 'to': _converse.bare_jid,
  1319. 'type': 'result',
  1320. }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"})
  1321. .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"})
  1322. .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute
  1323. .c('list', {'xmlns': "eu.siacs.conversations.axolotl"})
  1324. .c('device', {'id': '555'});
  1325. _converse.connection._dataRecv(mock.createRequest(stanza));
  1326. await u.waitUntil(() => u.isVisible(modal.el), 1000);
  1327. iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555'));
  1328. expect(Strophe.serialize(iq_stanza)).toBe(
  1329. `<iq from="romeo@montague.lit" id="${iq_stanza.getAttribute("id")}" to="mercutio@montague.lit" type="get" xmlns="jabber:client">`+
  1330. `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
  1331. `<items node="eu.siacs.conversations.axolotl.bundles:555"/>`+
  1332. `</pubsub>`+
  1333. `</iq>`);
  1334. stanza = $iq({
  1335. 'from': contact_jid,
  1336. 'id': iq_stanza.getAttribute('id'),
  1337. 'to': _converse.bare_jid,
  1338. 'type': 'result',
  1339. }).c('pubsub', {
  1340. 'xmlns': 'http://jabber.org/protocol/pubsub'
  1341. }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
  1342. .c('item')
  1343. .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
  1344. .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
  1345. .c('signedPreKeySignature').t(btoa('2222')).up()
  1346. .c('identityKey').t('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI').up()
  1347. .c('prekeys')
  1348. .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
  1349. .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
  1350. .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
  1351. _converse.connection._dataRecv(mock.createRequest(stanza));
  1352. await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
  1353. expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
  1354. const el = modal.el.querySelector('.fingerprints .fingerprint');
  1355. expect(el.textContent.trim()).toBe(
  1356. u.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI')))
  1357. );
  1358. expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
  1359. const devicelist = _converse.devicelists.get(contact_jid);
  1360. expect(devicelist.devices.get('555').get('trusted')).toBe(0);
  1361. let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
  1362. expect(trusted_radio.checked).toBe(true);
  1363. let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
  1364. expect(untrusted_radio.checked).toBe(false);
  1365. // Test that the device can be set to untrusted
  1366. untrusted_radio.click();
  1367. trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]');
  1368. expect(trusted_radio.hasAttribute('checked')).toBe(false);
  1369. expect(devicelist.devices.get('555').get('trusted')).toBe(-1);
  1370. untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]');
  1371. expect(untrusted_radio.hasAttribute('checked')).toBe(true);
  1372. trusted_radio.click();
  1373. expect(devicelist.devices.get('555').get('trusted')).toBe(1);
  1374. done();
  1375. }));
  1376. });