omemo.js 49 KB

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