utils.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. /* global libsignal */
  2. import difference from 'lodash-es/difference';
  3. import log from '@converse/headless/log';
  4. import tpl_audio from 'templates/audio.js';
  5. import tpl_file from 'templates/file.js';
  6. import tpl_image from 'templates/image.js';
  7. import tpl_video from 'templates/video.js';
  8. import { MIMETYPES_MAP } from 'utils/file.js';
  9. import { KEY_ALGO, UNTRUSTED, TAG_LENGTH } from './consts.js';
  10. import { __ } from 'i18n';
  11. import { _converse, converse, api } from '@converse/headless/core';
  12. import { html } from 'lit';
  13. import { initStorage } from '@converse/headless/utils/storage.js';
  14. import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js';
  15. import concat from 'lodash-es/concat';
  16. import { until } from 'lit/directives/until.js';
  17. import {
  18. appendArrayBuffer,
  19. arrayBufferToBase64,
  20. arrayBufferToHex,
  21. arrayBufferToString,
  22. base64ToArrayBuffer,
  23. hexToArrayBuffer,
  24. stringToArrayBuffer
  25. } from '@converse/headless/utils/arraybuffer.js';
  26. const { $msg, Strophe, URI, sizzle, u } = converse.env;
  27. export function formatFingerprint (fp) {
  28. fp = fp.replace(/^05/, '');
  29. for (let i=1; i<8; i++) {
  30. const idx = i*8+i-1;
  31. fp = fp.slice(0, idx) + ' ' + fp.slice(idx);
  32. }
  33. return fp;
  34. }
  35. async function encryptMessage (plaintext) {
  36. // The client MUST use fresh, randomly generated key/IV pairs
  37. // with AES-128 in Galois/Counter Mode (GCM).
  38. // For GCM a 12 byte IV is strongly suggested as other IV lengths
  39. // will require additional calculations. In principle any IV size
  40. // can be used as long as the IV doesn't ever repeat. NIST however
  41. // suggests that only an IV size of 12 bytes needs to be supported
  42. // by implementations.
  43. //
  44. // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
  45. const iv = crypto.getRandomValues(new window.Uint8Array(12));
  46. const key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']);
  47. const algo = {
  48. 'name': 'AES-GCM',
  49. 'iv': iv,
  50. 'tagLength': TAG_LENGTH
  51. };
  52. const encrypted = await crypto.subtle.encrypt(algo, key, stringToArrayBuffer(plaintext));
  53. const length = encrypted.byteLength - ((128 + 7) >> 3);
  54. const ciphertext = encrypted.slice(0, length);
  55. const tag = encrypted.slice(length);
  56. const exported_key = await crypto.subtle.exportKey('raw', key);
  57. return {
  58. 'key': exported_key,
  59. 'tag': tag,
  60. 'key_and_tag': appendArrayBuffer(exported_key, tag),
  61. 'payload': arrayBufferToBase64(ciphertext),
  62. 'iv': arrayBufferToBase64(iv)
  63. };
  64. }
  65. async function decryptMessage (obj) {
  66. const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']);
  67. const cipher = appendArrayBuffer(base64ToArrayBuffer(obj.payload), obj.tag);
  68. const algo = {
  69. 'name': 'AES-GCM',
  70. 'iv': base64ToArrayBuffer(obj.iv),
  71. 'tagLength': TAG_LENGTH
  72. };
  73. return arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
  74. }
  75. export async function encryptFile (file) {
  76. const iv = crypto.getRandomValues(new Uint8Array(12));
  77. const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt']);
  78. const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, }, key, await file.arrayBuffer());
  79. const exported_key = await window.crypto.subtle.exportKey('raw', key);
  80. const encrypted_file = new File([encrypted], file.name, { type: file.type, lastModified: file.lastModified });
  81. encrypted_file.xep454_ivkey = arrayBufferToHex(iv) + arrayBufferToHex(exported_key);
  82. return encrypted_file;
  83. }
  84. export function setEncryptedFileURL (message, attrs) {
  85. const url = attrs.oob_url.replace(/^https?:/, 'aesgcm:') + '#' + message.file.xep454_ivkey;
  86. return Object.assign(attrs, {
  87. 'oob_url': null, // Since only the body gets encrypted, we don't set the oob_url
  88. 'message': url,
  89. 'body': url
  90. });
  91. }
  92. async function decryptFile (iv, key, cipher) {
  93. const key_obj = await crypto.subtle.importKey('raw', hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
  94. const algo = {
  95. 'name': 'AES-GCM',
  96. 'iv': hexToArrayBuffer(iv),
  97. };
  98. return crypto.subtle.decrypt(algo, key_obj, cipher);
  99. }
  100. async function downloadFile(url) {
  101. let response;
  102. try {
  103. response = await fetch(url)
  104. } catch(e) {
  105. log.error(`${e.name}: Failed to download encrypted media: ${url}`);
  106. log.error(e);
  107. return null;
  108. }
  109. if (response.status >= 200 && response.status < 400) {
  110. return response.arrayBuffer();
  111. }
  112. }
  113. async function getAndDecryptFile (uri) {
  114. const hash = uri.hash().slice(1);
  115. const protocol = window.location.hostname === 'localhost' ? 'http' : 'https';
  116. const http_url = uri.toString().replace(/^aesgcm/, protocol);
  117. const cipher = await downloadFile(http_url);
  118. if (cipher === null) {
  119. log.error(`Could not decrypt file ${uri.toString()} since it could not be downloaded`);
  120. return null;
  121. }
  122. const iv = hash.slice(0, 24);
  123. const key = hash.slice(24);
  124. let content;
  125. try {
  126. content = await decryptFile(iv, key, cipher);
  127. } catch (e) {
  128. log.error(`Could not decrypt file ${uri.toString()}`);
  129. log.error(e);
  130. return null;
  131. }
  132. const [filename, extension] = uri.filename()?.split('.');
  133. const mimetype = MIMETYPES_MAP[extension];
  134. try {
  135. const file = new File([content], filename, { 'type': mimetype });
  136. return URL.createObjectURL(file);
  137. } catch (e) {
  138. log.error(`Could not decrypt file ${uri.toString()}`);
  139. log.error(e);
  140. return null;
  141. }
  142. }
  143. function getTemplateForObjectURL (uri, obj_url, richtext) {
  144. const file_url = uri.toString();
  145. if (obj_url === null) {
  146. return file_url;
  147. }
  148. if (isImageURL(file_url)) {
  149. return tpl_image({
  150. 'url': obj_url,
  151. 'onClick': richtext.onImgClick,
  152. 'onLoad': richtext.onImgLoad
  153. });
  154. } else if (isAudioURL(file_url)) {
  155. return tpl_audio(obj_url);
  156. } else if (isVideoURL(file_url)) {
  157. return tpl_video(obj_url);
  158. } else {
  159. return tpl_file(obj_url, uri.filename());
  160. }
  161. }
  162. function addEncryptedFiles(text, offset, richtext) {
  163. const objs = [];
  164. try {
  165. const parse_options = { 'start': /\b(aesgcm:\/\/)/gi };
  166. URI.withinString(
  167. text,
  168. (url, start, end) => {
  169. objs.push({ url, start, end });
  170. return url;
  171. },
  172. parse_options
  173. );
  174. } catch (error) {
  175. log.debug(error);
  176. return;
  177. }
  178. objs.forEach(o => {
  179. const uri = getURI(text.slice(o.start, o.end));
  180. const promise = getAndDecryptFile(uri)
  181. .then(obj_url => getTemplateForObjectURL(uri, obj_url, richtext));
  182. const template = html`${until(promise, '')}`;
  183. richtext.addTemplateResult(o.start + offset, o.end + offset, template);
  184. });
  185. }
  186. export function handleEncryptedFiles (richtext) {
  187. if (!_converse.config.get('trusted')) {
  188. return;
  189. }
  190. richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
  191. }
  192. /**
  193. * Hook handler for { @link parseMessage } and { @link parseMUCMessage }, which
  194. * parses the passed in `message` stanza for OMEMO attributes and then sets
  195. * them on the attrs object.
  196. * @param { XMLElement } stanza - The message stanza
  197. * @param { (MUCMessageAttributes|MessageAttributes) } attrs
  198. * @returns (MUCMessageAttributes|MessageAttributes)
  199. */
  200. export async function parseEncryptedMessage (stanza, attrs) {
  201. if (api.settings.get('clear_cache_on_logout') ||
  202. !attrs.is_encrypted ||
  203. attrs.encryption_namespace !== Strophe.NS.OMEMO) {
  204. return attrs;
  205. }
  206. const encrypted_el = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop();
  207. const header = encrypted_el.querySelector('header');
  208. attrs.encrypted = { 'device_id': header.getAttribute('sid') };
  209. const device_id = await api.omemo?.getDeviceID();
  210. const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted_el).pop();
  211. if (key) {
  212. Object.assign(attrs.encrypted, {
  213. 'iv': header.querySelector('iv').textContent,
  214. 'key': key.textContent,
  215. 'payload': encrypted_el.querySelector('payload')?.textContent || null,
  216. 'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
  217. });
  218. } else {
  219. return Object.assign(attrs, {
  220. 'error_condition': 'not-encrypted-for-this-device',
  221. 'error_type': 'Decryption',
  222. 'is_ephemeral': true,
  223. 'is_error': true,
  224. 'type': 'error'
  225. });
  226. }
  227. // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
  228. if (attrs.encrypted.prekey === true) {
  229. return decryptPrekeyWhisperMessage(attrs);
  230. } else {
  231. return decryptWhisperMessage(attrs);
  232. }
  233. }
  234. export function onChatBoxesInitialized () {
  235. _converse.chatboxes.on('add', chatbox => {
  236. checkOMEMOSupported(chatbox);
  237. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  238. chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
  239. chatbox.features.on('change', () => checkOMEMOSupported(chatbox));
  240. }
  241. });
  242. }
  243. export function onChatInitialized (el) {
  244. el.listenTo(el.model.messages, 'add', message => {
  245. if (message.get('is_encrypted') && !message.get('is_error')) {
  246. el.model.save('omemo_supported', true);
  247. }
  248. });
  249. el.listenTo(el.model, 'change:omemo_supported', () => {
  250. if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) {
  251. el.model.set('omemo_active', false);
  252. } else {
  253. // Manually trigger an update, setting omemo_active to
  254. // false above will automatically trigger one.
  255. el.querySelector('converse-chat-toolbar')?.requestUpdate();
  256. }
  257. });
  258. el.listenTo(el.model, 'change:omemo_active', () => {
  259. el.querySelector('converse-chat-toolbar').requestUpdate();
  260. });
  261. }
  262. export function getSessionCipher (jid, id) {
  263. const address = new libsignal.SignalProtocolAddress(jid, id);
  264. return new window.libsignal.SessionCipher(_converse.omemo_store, address);
  265. }
  266. function getJIDForDecryption (attrs) {
  267. const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from;
  268. if (!from_jid) {
  269. Object.assign(attrs, {
  270. 'error_text': __("Sorry, could not decrypt a received OMEMO "+
  271. "message because we don't have the XMPP address for that user."),
  272. 'error_type': 'Decryption',
  273. 'is_ephemeral': true,
  274. 'is_error': true,
  275. 'type': 'error'
  276. });
  277. throw new Error("Could not find JID to decrypt OMEMO message for");
  278. }
  279. return from_jid;
  280. }
  281. async function handleDecryptedWhisperMessage (attrs, key_and_tag) {
  282. const from_jid = getJIDForDecryption(attrs);
  283. const devicelist = await api.omemo.devicelists.get(from_jid, true);
  284. const encrypted = attrs.encrypted;
  285. let device = devicelist.devices.get(encrypted.device_id);
  286. if (!device) {
  287. device = await devicelist.devices.create({ 'id': encrypted.device_id, 'jid': from_jid }, { 'promise': true });
  288. }
  289. if (encrypted.payload) {
  290. const key = key_and_tag.slice(0, 16);
  291. const tag = key_and_tag.slice(16);
  292. const result = await omemo.decryptMessage(Object.assign(encrypted, { 'key': key, 'tag': tag }));
  293. device.save('active', true);
  294. return result;
  295. }
  296. }
  297. function getDecryptionErrorAttributes (e) {
  298. return {
  299. 'error_text':
  300. __('Sorry, could not decrypt a received OMEMO message due to an error.') + ` ${e.name} ${e.message}`,
  301. 'error_condition': e.name,
  302. 'error_message': e.message,
  303. 'error_type': 'Decryption',
  304. 'is_ephemeral': true,
  305. 'is_error': true,
  306. 'type': 'error'
  307. };
  308. }
  309. async function decryptPrekeyWhisperMessage (attrs) {
  310. const from_jid = getJIDForDecryption(attrs);
  311. const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
  312. const key = base64ToArrayBuffer(attrs.encrypted.key);
  313. let key_and_tag;
  314. try {
  315. key_and_tag = await session_cipher.decryptPreKeyWhisperMessage(key, 'binary');
  316. } catch (e) {
  317. // TODO from the XEP:
  318. // There are various reasons why decryption of an
  319. // OMEMOKeyExchange or an OMEMOAuthenticatedMessage
  320. // could fail. One reason is if the message was
  321. // received twice and already decrypted once, in this
  322. // case the client MUST ignore the decryption failure
  323. // and not show any warnings/errors. In all other cases
  324. // of decryption failure, clients SHOULD respond by
  325. // forcibly doing a new key exchange and sending a new
  326. // OMEMOKeyExchange with a potentially empty SCE
  327. // payload. By building a new session with the original
  328. // sender this way, the invalid session of the original
  329. // sender will get overwritten with this newly created,
  330. // valid session.
  331. log.error(`${e.name} ${e.message}`);
  332. return Object.assign(attrs, getDecryptionErrorAttributes(e));
  333. }
  334. // TODO from the XEP:
  335. // When a client receives the first message for a given
  336. // ratchet key with a counter of 53 or higher, it MUST send
  337. // a heartbeat message. Heartbeat messages are normal OMEMO
  338. // encrypted messages where the SCE payload does not include
  339. // any elements. These heartbeat messages cause the ratchet
  340. // to forward, thus consequent messages will have the
  341. // counter restarted from 0.
  342. try {
  343. const plaintext = await handleDecryptedWhisperMessage(attrs, key_and_tag);
  344. await _converse.omemo_store.generateMissingPreKeys();
  345. await _converse.omemo_store.publishBundle();
  346. if (plaintext) {
  347. return Object.assign(attrs, { 'plaintext': plaintext });
  348. } else {
  349. return Object.assign(attrs, { 'is_only_key': true });
  350. }
  351. } catch (e) {
  352. log.error(`${e.name} ${e.message}`);
  353. return Object.assign(attrs, getDecryptionErrorAttributes(e));
  354. }
  355. }
  356. async function decryptWhisperMessage (attrs) {
  357. const from_jid = getJIDForDecryption(attrs);
  358. const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
  359. const key = base64ToArrayBuffer(attrs.encrypted.key);
  360. try {
  361. const key_and_tag = await session_cipher.decryptWhisperMessage(key, 'binary');
  362. const plaintext = await handleDecryptedWhisperMessage(attrs, key_and_tag);
  363. return Object.assign(attrs, { 'plaintext': plaintext });
  364. } catch (e) {
  365. log.error(`${e.name} ${e.message}`);
  366. return Object.assign(attrs, getDecryptionErrorAttributes(e));
  367. }
  368. }
  369. export function addKeysToMessageStanza (stanza, dicts, iv) {
  370. for (const i in dicts) {
  371. if (Object.prototype.hasOwnProperty.call(dicts, i)) {
  372. const payload = dicts[i].payload;
  373. const device = dicts[i].device;
  374. const prekey = 3 == parseInt(payload.type, 10);
  375. stanza.c('key', { 'rid': device.get('id') }).t(btoa(payload.body));
  376. if (prekey) {
  377. stanza.attrs({ 'prekey': prekey });
  378. }
  379. stanza.up();
  380. if (i == dicts.length - 1) {
  381. stanza.c('iv').t(iv).up().up();
  382. }
  383. }
  384. }
  385. return Promise.resolve(stanza);
  386. }
  387. /**
  388. * Given an XML element representing a user's OMEMO bundle, parse it
  389. * and return a map.
  390. */
  391. export function parseBundle (bundle_el) {
  392. const signed_prekey_public_el = bundle_el.querySelector('signedPreKeyPublic');
  393. const signed_prekey_signature_el = bundle_el.querySelector('signedPreKeySignature');
  394. const prekeys = sizzle(`prekeys > preKeyPublic`, bundle_el).map(el => ({
  395. 'id': parseInt(el.getAttribute('preKeyId'), 10),
  396. 'key': el.textContent
  397. }));
  398. return {
  399. 'identity_key': bundle_el.querySelector('identityKey').textContent.trim(),
  400. 'signed_prekey': {
  401. 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
  402. 'public_key': signed_prekey_public_el.textContent,
  403. 'signature': signed_prekey_signature_el.textContent
  404. },
  405. 'prekeys': prekeys
  406. };
  407. }
  408. export async function generateFingerprint (device) {
  409. if (device.get('bundle')?.fingerprint) {
  410. return;
  411. }
  412. const bundle = await device.getBundle();
  413. bundle['fingerprint'] = arrayBufferToHex(base64ToArrayBuffer(bundle['identity_key']));
  414. device.save('bundle', bundle);
  415. device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
  416. }
  417. export async function getDevicesForContact (jid) {
  418. await api.waitUntil('OMEMOInitialized');
  419. const devicelist = await api.omemo.devicelists.get(jid, true);
  420. await devicelist.fetchDevices();
  421. return devicelist.devices;
  422. }
  423. export async function generateDeviceID () {
  424. /* Generates a device ID, making sure that it's unique */
  425. const devicelist = await api.omemo.devicelists.get(_converse.bare_jid, true);
  426. const existing_ids = devicelist.devices.pluck('id');
  427. let device_id = libsignal.KeyHelper.generateRegistrationId();
  428. // Before publishing a freshly generated device id for the first time,
  429. // a device MUST check whether that device id already exists, and if so, generate a new one.
  430. let i = 0;
  431. while (existing_ids.includes(device_id)) {
  432. device_id = libsignal.KeyHelper.generateRegistrationId();
  433. i++;
  434. if (i === 10) {
  435. throw new Error('Unable to generate a unique device ID');
  436. }
  437. }
  438. return device_id.toString();
  439. }
  440. async function buildSession (device) {
  441. const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
  442. const sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address);
  443. const prekey = device.getRandomPreKey();
  444. const bundle = await device.getBundle();
  445. return sessionBuilder.processPreKey({
  446. 'registrationId': parseInt(device.get('id'), 10),
  447. 'identityKey': base64ToArrayBuffer(bundle.identity_key),
  448. 'signedPreKey': {
  449. 'keyId': bundle.signed_prekey.id, // <Number>
  450. 'publicKey': base64ToArrayBuffer(bundle.signed_prekey.public_key),
  451. 'signature': base64ToArrayBuffer(bundle.signed_prekey.signature)
  452. },
  453. 'preKey': {
  454. 'keyId': prekey.id, // <Number>
  455. 'publicKey': base64ToArrayBuffer(prekey.key)
  456. }
  457. });
  458. }
  459. export async function getSession (device) {
  460. if (!device.get('bundle')) {
  461. log.error(`Could not build an OMEMO session for device ${device.get('id')} because we don't have its bundle`);
  462. return null;
  463. }
  464. const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
  465. const session = await _converse.omemo_store.loadSession(address.toString());
  466. if (session) {
  467. return session;
  468. } else {
  469. try {
  470. const session = await buildSession(device);
  471. return session;
  472. } catch (e) {
  473. log.error(`Could not build an OMEMO session for device ${device.get('id')}`);
  474. log.error(e);
  475. return null;
  476. }
  477. }
  478. }
  479. async function updateBundleFromStanza (stanza) {
  480. const items_el = sizzle(`items`, stanza).pop();
  481. if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) {
  482. return;
  483. }
  484. const device_id = items_el.getAttribute('node').split(':')[1];
  485. const jid = stanza.getAttribute('from');
  486. const bundle_el = sizzle(`item > bundle`, items_el).pop();
  487. const devicelist = await api.omemo.devicelists.get(jid, true);
  488. const device = devicelist.devices.get(device_id) || devicelist.devices.create({ 'id': device_id, jid });
  489. device.save({ 'bundle': parseBundle(bundle_el) });
  490. }
  491. async function updateDevicesFromStanza (stanza) {
  492. const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"]`, stanza).pop();
  493. if (!items_el) {
  494. return;
  495. }
  496. const device_selector = `item list[xmlns="${Strophe.NS.OMEMO}"] device`;
  497. const device_ids = sizzle(device_selector, items_el).map(d => d.getAttribute('id'));
  498. const jid = stanza.getAttribute('from');
  499. const devicelist = await api.omemo.devicelists.get(jid, true);
  500. const devices = devicelist.devices;
  501. const removed_ids = difference(devices.pluck('id'), device_ids);
  502. removed_ids.forEach(id => {
  503. if (jid === _converse.bare_jid && id === _converse.omemo_store.get('device_id')) {
  504. return; // We don't set the current device as inactive
  505. }
  506. devices.get(id).save('active', false);
  507. });
  508. device_ids.forEach(device_id => {
  509. const device = devices.get(device_id);
  510. if (device) {
  511. device.save('active', true);
  512. } else {
  513. devices.create({ 'id': device_id, 'jid': jid });
  514. }
  515. });
  516. if (u.isSameBareJID(jid, _converse.bare_jid)) {
  517. // Make sure our own device is on the list
  518. // (i.e. if it was removed, add it again).
  519. devicelist.publishCurrentDevice(device_ids);
  520. }
  521. }
  522. export function registerPEPPushHandler () {
  523. // Add a handler for devices pushed from other connected clients
  524. _converse.connection.addHandler(
  525. async message => {
  526. try {
  527. if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"]`, message).length) {
  528. await api.waitUntil('OMEMOInitialized');
  529. await updateDevicesFromStanza(message);
  530. await updateBundleFromStanza(message);
  531. }
  532. } catch (e) {
  533. log.error(e.message);
  534. }
  535. return true;
  536. },
  537. null,
  538. 'message',
  539. 'headline'
  540. );
  541. }
  542. export async function restoreOMEMOSession () {
  543. if (_converse.omemo_store === undefined) {
  544. const id = `converse.omemosession-${_converse.bare_jid}`;
  545. _converse.omemo_store = new _converse.OMEMOStore({ id });
  546. initStorage(_converse.omemo_store, id);
  547. }
  548. await _converse.omemo_store.fetchSession();
  549. }
  550. async function fetchDeviceLists () {
  551. const id = `converse.devicelists-${_converse.bare_jid}`;
  552. _converse.devicelists = new _converse.DeviceLists({ id });
  553. initStorage(_converse.devicelists, id);
  554. await new Promise(resolve => {
  555. _converse.devicelists.fetch({
  556. 'success': resolve,
  557. 'error': (m, e) => { log.error(e); resolve(); }
  558. })
  559. });
  560. // Call API method to wait for our own device list to be fetched from the
  561. // server or to be created. If we have no pre-existing OMEMO session, this
  562. // will cause a new device and bundle to be generated and published.
  563. await api.omemo.devicelists.get(_converse.bare_jid, true);
  564. }
  565. export async function initOMEMO (reconnecting) {
  566. if (reconnecting) {
  567. return;
  568. }
  569. if (!_converse.config.get('trusted') || api.settings.get('clear_cache_on_logout')) {
  570. log.warn('Not initializing OMEMO, since this browser is not trusted or clear_cache_on_logout is set to true');
  571. return;
  572. }
  573. try {
  574. await fetchDeviceLists();
  575. await restoreOMEMOSession();
  576. await _converse.omemo_store.publishBundle();
  577. } catch (e) {
  578. log.error('Could not initialize OMEMO support');
  579. log.error(e);
  580. return;
  581. }
  582. /**
  583. * Triggered once OMEMO support has been initialized
  584. * @event _converse#OMEMOInitialized
  585. * @example _converse.api.listen.on('OMEMOInitialized', () => { ... });
  586. */
  587. api.trigger('OMEMOInitialized');
  588. }
  589. async function onOccupantAdded (chatroom, occupant) {
  590. if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) {
  591. return;
  592. }
  593. if (chatroom.get('omemo_active')) {
  594. const supported = await _converse.contactHasOMEMOSupport(occupant.get('jid'));
  595. if (!supported) {
  596. chatroom.createMessage({
  597. 'message': __(
  598. "%1$s doesn't appear to have a client that supports OMEMO. " +
  599. 'Encrypted chat will no longer be possible in this grouchat.',
  600. occupant.get('nick')
  601. ),
  602. 'type': 'error'
  603. });
  604. chatroom.save({ 'omemo_active': false, 'omemo_supported': false });
  605. }
  606. }
  607. }
  608. async function checkOMEMOSupported (chatbox) {
  609. let supported;
  610. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  611. await api.waitUntil('OMEMOInitialized');
  612. supported = chatbox.features.get('nonanonymous') && chatbox.features.get('membersonly');
  613. } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
  614. supported = await _converse.contactHasOMEMOSupport(chatbox.get('jid'));
  615. }
  616. chatbox.set('omemo_supported', supported);
  617. if (supported && api.settings.get('omemo_default')) {
  618. chatbox.set('omemo_active', true);
  619. }
  620. }
  621. function toggleOMEMO (ev) {
  622. ev.stopPropagation();
  623. ev.preventDefault();
  624. const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar');
  625. if (!toolbar_el.model.get('omemo_supported')) {
  626. let messages;
  627. if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) {
  628. messages = [
  629. __(
  630. 'Cannot use end-to-end encryption in this groupchat, ' +
  631. 'either the groupchat has some anonymity or not all participants support OMEMO.'
  632. )
  633. ];
  634. } else {
  635. messages = [
  636. __(
  637. "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.",
  638. toolbar_el.model.contact.getDisplayName()
  639. )
  640. ];
  641. }
  642. return api.alert('error', __('Error'), messages);
  643. }
  644. toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') });
  645. }
  646. export function getOMEMOToolbarButton (toolbar_el, buttons) {
  647. const model = toolbar_el.model;
  648. const is_muc = model.get('type') === _converse.CHATROOMS_TYPE;
  649. let title;
  650. if (model.get('omemo_supported')) {
  651. const i18n_plaintext = __('Messages are being sent in plaintext');
  652. const i18n_encrypted = __('Messages are sent encrypted');
  653. title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext;
  654. } else if (is_muc) {
  655. title = __(
  656. 'This groupchat needs to be members-only and non-anonymous in ' +
  657. 'order to support OMEMO encrypted messages'
  658. );
  659. } else {
  660. title = __('OMEMO encryption is not supported');
  661. }
  662. let color;
  663. if (model.get('omemo_supported')) {
  664. if (model.get('omemo_active')) {
  665. color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`;
  666. } else {
  667. color = `var(--error-color)`;
  668. }
  669. } else {
  670. color = `var(--muc-toolbar-btn-disabled-color)`;
  671. }
  672. buttons.push(html`
  673. <button class="toggle-omemo" title="${title}" data-disabled=${!model.get('omemo_supported')} @click=${toggleOMEMO}>
  674. <converse-icon
  675. class="fa ${model.get('omemo_active') ? `fa-lock` : `fa-unlock`}"
  676. path-prefix="${api.settings.get('assets_path')}"
  677. size="1em"
  678. color="${color}"
  679. ></converse-icon>
  680. </button>
  681. `);
  682. return buttons;
  683. }
  684. export async function getBundlesAndBuildSessions (chatbox) {
  685. const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.');
  686. let devices;
  687. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  688. const collections = await Promise.all(chatbox.occupants.map(o => getDevicesForContact(o.get('jid'))));
  689. devices = collections.reduce((a, b) => concat(a, b.models), []);
  690. } else if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
  691. const their_devices = await getDevicesForContact(chatbox.get('jid'));
  692. if (their_devices.length === 0) {
  693. const err = new Error(no_devices_err);
  694. err.user_facing = true;
  695. throw err;
  696. }
  697. const own_list = await api.omemo.devicelists.get(_converse.bare_jid)
  698. const own_devices = own_list.devices;
  699. devices = [...own_devices.models, ...their_devices.models];
  700. }
  701. // Filter out our own device
  702. const id = _converse.omemo_store.get('device_id');
  703. devices = devices.filter(d => d.get('id') !== id);
  704. // Fetch bundles if necessary
  705. await Promise.all(devices.map(d => d.getBundle()));
  706. const sessions = devices.filter(d => d).map(d => getSession(d));
  707. await Promise.all(sessions);
  708. if (sessions.includes(null)) {
  709. // We couldn't build a session for certain devices.
  710. devices = devices.filter(d => sessions[devices.indexOf(d)]);
  711. if (devices.length === 0) {
  712. const err = new Error(no_devices_err);
  713. err.user_facing = true;
  714. throw err;
  715. }
  716. }
  717. return devices;
  718. }
  719. export function createOMEMOMessageStanza (chatbox, message, devices) {
  720. const body = __(
  721. 'This is an OMEMO encrypted message which your client doesn’t seem to support. ' +
  722. 'Find more information on https://conversations.im/omemo'
  723. );
  724. if (!message.get('message')) {
  725. throw new Error('No message body to encrypt!');
  726. }
  727. const stanza = $msg({
  728. 'from': _converse.connection.jid,
  729. 'to': chatbox.get('jid'),
  730. 'type': chatbox.get('message_type'),
  731. 'id': message.get('msgid')
  732. }).c('body').t(body).up();
  733. if (message.get('type') === 'chat') {
  734. stanza.c('request', { 'xmlns': Strophe.NS.RECEIPTS }).up();
  735. }
  736. // An encrypted header is added to the message for
  737. // each device that is supposed to receive it.
  738. // These headers simply contain the key that the
  739. // payload message is encrypted with,
  740. // and they are separately encrypted using the
  741. // session corresponding to the counterpart device.
  742. stanza
  743. .c('encrypted', { 'xmlns': Strophe.NS.OMEMO })
  744. .c('header', { 'sid': _converse.omemo_store.get('device_id') });
  745. return omemo.encryptMessage(message.get('message')).then(obj => {
  746. // The 16 bytes key and the GCM authentication tag (The tag
  747. // SHOULD have at least 128 bit) are concatenated and for each
  748. // intended recipient device, i.e. both own devices as well as
  749. // devices associated with the contact, the result of this
  750. // concatenation is encrypted using the corresponding
  751. // long-standing SignalProtocol session.
  752. const promises = devices
  753. .filter(device => device.get('trusted') != UNTRUSTED && device.get('active'))
  754. .map(device => chatbox.encryptKey(obj.key_and_tag, device));
  755. return Promise.all(promises)
  756. .then(dicts => addKeysToMessageStanza(stanza, dicts, obj.iv))
  757. .then(stanza => {
  758. stanza.c('payload').t(obj.payload).up().up();
  759. stanza.c('store', { 'xmlns': Strophe.NS.HINTS }).up();
  760. stanza.c('encryption', { 'xmlns': Strophe.NS.EME, namespace: Strophe.NS.OMEMO });
  761. return stanza;
  762. });
  763. });
  764. }
  765. export const omemo = {
  766. decryptMessage,
  767. encryptMessage,
  768. formatFingerprint
  769. }