utils.js 34 KB

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