You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OlmAdapter.js 36KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. /* global Olm */
  2. import { getLogger } from '@jitsi/logger';
  3. import base64js from 'base64-js';
  4. import isEqual from 'lodash.isequal';
  5. import { v4 as uuidv4 } from 'uuid';
  6. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  7. import Deferred from '../util/Deferred';
  8. import Listenable from '../util/Listenable';
  9. import { FEATURE_E2EE, JITSI_MEET_MUC_TYPE } from '../xmpp/xmpp';
  10. import { E2EEErrors } from './E2EEErrors';
  11. import { generateSas } from './SAS';
  12. const logger = getLogger(__filename);
  13. const REQ_TIMEOUT = 5 * 1000;
  14. const OLM_MESSAGE_TYPE = 'olm';
  15. const OLM_MESSAGE_TYPES = {
  16. ERROR: 'error',
  17. KEY_INFO: 'key-info',
  18. KEY_INFO_ACK: 'key-info-ack',
  19. SESSION_ACK: 'session-ack',
  20. SESSION_INIT: 'session-init',
  21. SAS_START: 'sas-start',
  22. SAS_ACCEPT: 'sas-accept',
  23. SAS_KEY: 'sas-key',
  24. SAS_MAC: 'sas-mac'
  25. };
  26. const OLM_SAS_NUM_BYTES = 6;
  27. const OLM_KEY_VERIFICATION_MAC_INFO = 'Jitsi-KEY_VERIFICATION_MAC';
  28. const OLM_KEY_VERIFICATION_MAC_KEY_IDS = 'Jitsi-KEY_IDS';
  29. const kOlmData = Symbol('OlmData');
  30. const OlmAdapterEvents = {
  31. PARTICIPANT_E2EE_CHANNEL_READY: 'olm.participant_e2ee_channel_ready',
  32. PARTICIPANT_SAS_AVAILABLE: 'olm.participant_sas_available',
  33. PARTICIPANT_SAS_READY: 'olm.participant_sas_ready',
  34. PARTICIPANT_KEY_UPDATED: 'olm.partitipant_key_updated',
  35. PARTICIPANT_VERIFICATION_COMPLETED: 'olm.participant_verification_completed'
  36. };
  37. /**
  38. * This class implements an End-to-End Encrypted communication channel between every two peers
  39. * in the conference. This channel uses libolm to achieve E2EE.
  40. *
  41. * The created channel is then used to exchange the secret key that each participant will use
  42. * to encrypt the actual media (see {@link E2EEContext}).
  43. *
  44. * A simple JSON message based protocol is implemented, which follows a request - response model:
  45. * - session-init: Initiates an olm session establishment procedure. This message will be sent
  46. * by the participant who just joined, to everyone else.
  47. * - session-ack: Completes the olm session etablishment. This messsage may contain ancilliary
  48. * encrypted data, more specifically the sender's current key.
  49. * - key-info: Includes the sender's most up to date key information.
  50. * - key-info-ack: Acknowledges the reception of a key-info request. In addition, it may contain
  51. * the sender's key information, if available.
  52. * - error: Indicates a request processing error has occurred.
  53. *
  54. * These requessts and responses are transport independent. Currently they are sent using XMPP
  55. * MUC private messages.
  56. */
  57. console.log("ljm_dbg OlmAdapter!....")
  58. export class OlmAdapter extends Listenable {
  59. /**
  60. * Creates an adapter instance for the given conference.
  61. */
  62. constructor(conference) {
  63. clog("ljm_dbg OlmAdapter constructor")
  64. super();
  65. this._conf = conference;
  66. this._init = new Deferred();
  67. this._mediaKey = undefined;
  68. this._mediaKeyIndex = -1;
  69. this._reqs = new Map();
  70. this._sessionInitialization = undefined;
  71. if (OlmAdapter.isSupported()) {
  72. this._bootstrapOlm();
  73. this._conf.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, this._onEndpointMessageReceived.bind(this));
  74. this._conf.on(JitsiConferenceEvents.CONFERENCE_LEFT, this._onConferenceLeft.bind(this));
  75. this._conf.on(JitsiConferenceEvents.USER_LEFT, this._onParticipantLeft.bind(this));
  76. this._conf.on(JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
  77. this._onParticipantPropertyChanged.bind(this));
  78. } else {
  79. this._init.reject(new Error('Olm not supported'));
  80. }
  81. }
  82. /**
  83. * Returns the current participants conference ID.
  84. *
  85. * @returns {string}
  86. */
  87. get myId() {
  88. return this._conf.myUserId();
  89. }
  90. /**
  91. * Starts new olm sessions with every other participant that has the participantId "smaller" the localParticipantId.
  92. */
  93. async initSessions() {
  94. if (this._sessionInitialization) {
  95. throw new Error('OlmAdapter initSessions called multiple times');
  96. } else {
  97. this._sessionInitialization = new Deferred();
  98. await this._init;
  99. const promises = [];
  100. const localParticipantId = this._conf.myUserId();
  101. for (const participant of this._conf.getParticipants()) {
  102. if (participant.hasFeature(FEATURE_E2EE) && localParticipantId < participant.getId()) {
  103. promises.push(this._sendSessionInit(participant));
  104. }
  105. }
  106. await Promise.allSettled(promises);
  107. // TODO: retry failed ones.
  108. this._sessionInitialization.resolve();
  109. this._sessionInitialization = undefined;
  110. }
  111. }
  112. /**
  113. * Indicates if olm is supported on the current platform.
  114. *
  115. * @returns {boolean}
  116. */
  117. static isSupported() {
  118. return typeof window.Olm !== 'undefined';
  119. }
  120. /**
  121. * Updates the current participant key and distributes it to all participants in the conference
  122. * by sending a key-info message.
  123. *
  124. * @param {Uint8Array|boolean} key - The new key.
  125. * @retrns {Promise<Number>}
  126. */
  127. async updateKey(key) {
  128. // Store it locally for new sessions.
  129. this._mediaKey = key;
  130. this._mediaKeyIndex++;
  131. // Broadcast it.
  132. const promises = [];
  133. for (const participant of this._conf.getParticipants()) {
  134. const pId = participant.getId();
  135. const olmData = this._getParticipantOlmData(participant);
  136. // TODO: skip those who don't support E2EE.
  137. if (!olmData.session) {
  138. logger.warn(`Tried to send key to participant ${pId} but we have no session`);
  139. // eslint-disable-next-line no-continue
  140. continue;
  141. }
  142. const uuid = uuidv4();
  143. const data = {
  144. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  145. olm: {
  146. type: OLM_MESSAGE_TYPES.KEY_INFO,
  147. data: {
  148. ciphertext: this._encryptKeyInfo(olmData.session),
  149. uuid
  150. }
  151. }
  152. };
  153. const d = new Deferred();
  154. d.setRejectTimeout(REQ_TIMEOUT);
  155. d.catch(() => {
  156. this._reqs.delete(uuid);
  157. });
  158. this._reqs.set(uuid, d);
  159. promises.push(d);
  160. this._sendMessage(data, pId);
  161. }
  162. await Promise.allSettled(promises);
  163. // TODO: retry failed ones?
  164. return this._mediaKeyIndex;
  165. }
  166. /**
  167. * Updates the current participant key.
  168. * @param {Uint8Array|boolean} key - The new key.
  169. * @returns {number}
  170. */
  171. updateCurrentMediaKey(key) {
  172. this._mediaKey = key;
  173. return this._mediaKeyIndex;
  174. }
  175. /**
  176. * Frees the olmData session for the given participant.
  177. *
  178. */
  179. clearParticipantSession(participant) {
  180. const olmData = this._getParticipantOlmData(participant);
  181. if (olmData.session) {
  182. olmData.session.free();
  183. olmData.session = undefined;
  184. }
  185. }
  186. /**
  187. * Frees the olmData sessions for all participants.
  188. *
  189. */
  190. clearAllParticipantsSessions() {
  191. for (const participant of this._conf.getParticipants()) {
  192. this.clearParticipantSession(participant);
  193. }
  194. }
  195. /**
  196. * Sends sacMac if channel verification waas successful.
  197. *
  198. */
  199. markParticipantVerified(participant, isVerified) {
  200. const olmData = this._getParticipantOlmData(participant);
  201. const pId = participant.getId();
  202. if (!isVerified) {
  203. olmData.sasVerification = undefined;
  204. logger.warn(`Verification failed for participant ${pId}`);
  205. this.eventEmitter.emit(
  206. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  207. pId,
  208. false,
  209. E2EEErrors.E2EE_SAS_CHANNEL_VERIFICATION_FAILED);
  210. return;
  211. }
  212. if (!olmData.sasVerification) {
  213. logger.warn(`Participant ${pId} does not have valid sasVerification`);
  214. this.eventEmitter.emit(
  215. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  216. pId,
  217. false,
  218. E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
  219. return;
  220. }
  221. const { sas, sasMacSent } = olmData.sasVerification;
  222. if (sas && sas.is_their_key_set() && !sasMacSent) {
  223. this._sendSasMac(participant);
  224. // Mark the MAC as sent so we don't send it multiple times.
  225. olmData.sasVerification.sasMacSent = true;
  226. }
  227. }
  228. /**
  229. * Internal helper to bootstrap the olm library.
  230. *
  231. * @returns {Promise<void>}
  232. * @private
  233. */
  234. async _bootstrapOlm() {
  235. logger.debug('Initializing Olm...');
  236. try {
  237. await Olm.init();
  238. this._olmAccount = new Olm.Account();
  239. this._olmAccount.create();
  240. this._idKeys = JSON.parse(this._olmAccount.identity_keys());
  241. logger.debug(`Olm ${Olm.get_library_version().join('.')} initialized`);
  242. this._init.resolve();
  243. this._onIdKeysReady(this._idKeys);
  244. } catch (e) {
  245. logger.error('Failed to initialize Olm', e);
  246. this._init.reject(e);
  247. }
  248. }
  249. /**
  250. * Starts the verification process for the given participant as described here
  251. * https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
  252. *
  253. * | |
  254. | m.key.verification.start |
  255. |-------------------------------->|
  256. | |
  257. | m.key.verification.accept |
  258. |<--------------------------------|
  259. | |
  260. | m.key.verification.key |
  261. |-------------------------------->|
  262. | |
  263. | m.key.verification.key |
  264. |<--------------------------------|
  265. | |
  266. | m.key.verification.mac |
  267. |-------------------------------->|
  268. | |
  269. | m.key.verification.mac |
  270. |<--------------------------------|
  271. | |
  272. *
  273. * @param {JitsiParticipant} participant - The target participant.
  274. * @returns {Promise<void>}
  275. * @private
  276. */
  277. startVerification(participant) {
  278. const pId = participant.getId();
  279. const olmData = this._getParticipantOlmData(participant);
  280. if (!olmData.session) {
  281. logger.warn(`Tried to start verification with participant ${pId} but we have no session`);
  282. return;
  283. }
  284. if (olmData.sasVerification) {
  285. logger.warn(`There is already a verification in progress with participant ${pId}`);
  286. return;
  287. }
  288. olmData.sasVerification = {
  289. sas: new Olm.SAS(),
  290. transactionId: uuidv4()
  291. };
  292. const startContent = {
  293. transactionId: olmData.sasVerification.transactionId
  294. };
  295. olmData.sasVerification.startContent = startContent;
  296. olmData.sasVerification.isInitiator = true;
  297. const startMessage = {
  298. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  299. olm: {
  300. type: OLM_MESSAGE_TYPES.SAS_START,
  301. data: startContent
  302. }
  303. };
  304. this._sendMessage(startMessage, pId);
  305. }
  306. /**
  307. * Publishes our own Olmn id key in presence.
  308. * @private
  309. */
  310. _onIdKeysReady(idKeys) {
  311. logger.debug(`Olm id key ready: ${idKeys}`);
  312. // Publish it in presence.
  313. for (const keyType in idKeys) {
  314. if (idKeys.hasOwnProperty(keyType)) {
  315. const key = idKeys[keyType];
  316. this._conf.setLocalParticipantProperty(`e2ee.idKey.${keyType}`, key);
  317. }
  318. }
  319. }
  320. /**
  321. * Event posted when the E2EE signalling channel has been established with the given participant.
  322. * @private
  323. */
  324. _onParticipantE2EEChannelReady(id) {
  325. logger.debug(`E2EE channel with participant ${id} is ready`);
  326. }
  327. /**
  328. * Internal helper for encrypting the current key information for a given participant.
  329. *
  330. * @param {Olm.Session} session - Participant's session.
  331. * @returns {string} - The encrypted text with the key information.
  332. * @private
  333. */
  334. _encryptKeyInfo(session) {
  335. const keyInfo = {};
  336. if (this._mediaKey !== undefined) {
  337. keyInfo.key = this._mediaKey ? base64js.fromByteArray(this._mediaKey) : false;
  338. keyInfo.keyIndex = this._mediaKeyIndex;
  339. }
  340. return session.encrypt(JSON.stringify(keyInfo));
  341. }
  342. /**
  343. * Internal helper for getting the olm related data associated with a participant.
  344. *
  345. * @param {JitsiParticipant} participant - Participant whose data wants to be extracted.
  346. * @returns {Object}
  347. * @private
  348. */
  349. _getParticipantOlmData(participant) {
  350. participant[kOlmData] = participant[kOlmData] || {};
  351. return participant[kOlmData];
  352. }
  353. /**
  354. * Handles leaving the conference, cleaning up olm sessions.
  355. *
  356. * @private
  357. */
  358. async _onConferenceLeft() {
  359. logger.debug('Conference left');
  360. await this._init;
  361. for (const participant of this._conf.getParticipants()) {
  362. this._onParticipantLeft(participant.getId(), participant);
  363. }
  364. if (this._olmAccount) {
  365. this._olmAccount.free();
  366. this._olmAccount = undefined;
  367. }
  368. }
  369. /**
  370. * Main message handler. Handles 1-to-1 messages received from other participants
  371. * and send the appropriate replies.
  372. *
  373. * @private
  374. */
  375. async _onEndpointMessageReceived(participant, payload) {
  376. if (payload[JITSI_MEET_MUC_TYPE] !== OLM_MESSAGE_TYPE) {
  377. return;
  378. }
  379. if (!payload.olm) {
  380. logger.warn('Incorrectly formatted message');
  381. return;
  382. }
  383. await this._init;
  384. const msg = payload.olm;
  385. const pId = participant.getId();
  386. const olmData = this._getParticipantOlmData(participant);
  387. switch (msg.type) {
  388. case OLM_MESSAGE_TYPES.SESSION_INIT: {
  389. if (olmData.session) {
  390. logger.warn(`Participant ${pId} already has a session`);
  391. this._sendError(participant, 'Session already established');
  392. } else {
  393. // Create a session for communicating with this participant.
  394. const session = new Olm.Session();
  395. session.create_outbound(this._olmAccount, msg.data.idKey, msg.data.otKey);
  396. olmData.session = session;
  397. // Send ACK
  398. const ack = {
  399. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  400. olm: {
  401. type: OLM_MESSAGE_TYPES.SESSION_ACK,
  402. data: {
  403. ciphertext: this._encryptKeyInfo(session),
  404. uuid: msg.data.uuid
  405. }
  406. }
  407. };
  408. this._sendMessage(ack, pId);
  409. this._onParticipantE2EEChannelReady(pId);
  410. }
  411. break;
  412. }
  413. case OLM_MESSAGE_TYPES.SESSION_ACK: {
  414. if (olmData.session) {
  415. logger.warn(`Participant ${pId} already has a session`);
  416. this._sendError(participant, 'No session found');
  417. } else if (msg.data.uuid === olmData.pendingSessionUuid) {
  418. const { ciphertext } = msg.data;
  419. const d = this._reqs.get(msg.data.uuid);
  420. const session = new Olm.Session();
  421. session.create_inbound(this._olmAccount, ciphertext.body);
  422. // Remove OT keys that have been used to setup this session.
  423. this._olmAccount.remove_one_time_keys(session);
  424. // Decrypt first message.
  425. const data = session.decrypt(ciphertext.type, ciphertext.body);
  426. olmData.session = session;
  427. olmData.pendingSessionUuid = undefined;
  428. this._onParticipantE2EEChannelReady(pId);
  429. this._reqs.delete(msg.data.uuid);
  430. d.resolve();
  431. const json = safeJsonParse(data);
  432. if (json.key) {
  433. const key = base64js.toByteArray(json.key);
  434. const keyIndex = json.keyIndex;
  435. olmData.lastKey = key;
  436. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
  437. }
  438. } else {
  439. logger.warn('Received ACK with the wrong UUID');
  440. this._sendError(participant, 'Invalid UUID');
  441. }
  442. break;
  443. }
  444. case OLM_MESSAGE_TYPES.ERROR: {
  445. logger.error(msg.data.error);
  446. break;
  447. }
  448. case OLM_MESSAGE_TYPES.KEY_INFO: {
  449. if (olmData.session) {
  450. const { ciphertext } = msg.data;
  451. const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
  452. const json = safeJsonParse(data);
  453. if (json.key !== undefined && json.keyIndex !== undefined) {
  454. const key = json.key ? base64js.toByteArray(json.key) : false;
  455. const keyIndex = json.keyIndex;
  456. if (!isEqual(olmData.lastKey, key)) {
  457. olmData.lastKey = key;
  458. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
  459. }
  460. // Send ACK.
  461. const ack = {
  462. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  463. olm: {
  464. type: OLM_MESSAGE_TYPES.KEY_INFO_ACK,
  465. data: {
  466. ciphertext: this._encryptKeyInfo(olmData.session),
  467. uuid: msg.data.uuid
  468. }
  469. }
  470. };
  471. this._sendMessage(ack, pId);
  472. }
  473. } else {
  474. logger.debug(`Received key info message from ${pId} but we have no session for them!`);
  475. this._sendError(participant, 'No session found while processing key-info');
  476. }
  477. break;
  478. }
  479. case OLM_MESSAGE_TYPES.KEY_INFO_ACK: {
  480. if (olmData.session) {
  481. const { ciphertext } = msg.data;
  482. const data = olmData.session.decrypt(ciphertext.type, ciphertext.body);
  483. const json = safeJsonParse(data);
  484. if (json.key !== undefined && json.keyIndex !== undefined) {
  485. const key = json.key ? base64js.toByteArray(json.key) : false;
  486. const keyIndex = json.keyIndex;
  487. if (!isEqual(olmData.lastKey, key)) {
  488. olmData.lastKey = key;
  489. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_KEY_UPDATED, pId, key, keyIndex);
  490. }
  491. }
  492. const d = this._reqs.get(msg.data.uuid);
  493. this._reqs.delete(msg.data.uuid);
  494. d.resolve();
  495. } else {
  496. logger.debug(`Received key info ack message from ${pId} but we have no session for them!`);
  497. this._sendError(participant, 'No session found while processing key-info-ack');
  498. }
  499. break;
  500. }
  501. case OLM_MESSAGE_TYPES.SAS_START: {
  502. if (!olmData.session) {
  503. logger.debug(`Received sas init message from ${pId} but we have no session for them!`);
  504. this._sendError(participant, 'No session found while processing sas-init');
  505. return;
  506. }
  507. if (olmData.sasVerification?.sas) {
  508. logger.warn(`SAS already created for participant ${pId}`);
  509. this.eventEmitter.emit(
  510. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  511. pId,
  512. false,
  513. E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
  514. return;
  515. }
  516. const { transactionId } = msg.data;
  517. const sas = new Olm.SAS();
  518. olmData.sasVerification = {
  519. sas,
  520. transactionId,
  521. isInitiator: false
  522. };
  523. const pubKey = olmData.sasVerification.sas.get_pubkey();
  524. const commitment = this._computeCommitment(pubKey, msg.data);
  525. /* The first phase of the verification process, the Key agreement phase
  526. https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
  527. */
  528. const acceptMessage = {
  529. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  530. olm: {
  531. type: OLM_MESSAGE_TYPES.SAS_ACCEPT,
  532. data: {
  533. transactionId,
  534. commitment
  535. }
  536. }
  537. };
  538. this._sendMessage(acceptMessage, pId);
  539. break;
  540. }
  541. case OLM_MESSAGE_TYPES.SAS_ACCEPT: {
  542. if (!olmData.session) {
  543. logger.debug(`Received sas accept message from ${pId} but we have no session for them!`);
  544. this._sendError(participant, 'No session found while processing sas-accept');
  545. return;
  546. }
  547. const { commitment, transactionId } = msg.data;
  548. if (!olmData.sasVerification) {
  549. logger.warn(`SAS_ACCEPT Participant ${pId} does not have valid sasVerification`);
  550. this.eventEmitter.emit(
  551. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  552. pId,
  553. false,
  554. E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
  555. return;
  556. }
  557. if (olmData.sasVerification.sasCommitment) {
  558. logger.debug(`Already received sas commitment message from ${pId}!`);
  559. this._sendError(participant, 'Already received sas commitment message from ${pId}!');
  560. return;
  561. }
  562. olmData.sasVerification.sasCommitment = commitment;
  563. const pubKey = olmData.sasVerification.sas.get_pubkey();
  564. // Send KEY.
  565. const keyMessage = {
  566. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  567. olm: {
  568. type: OLM_MESSAGE_TYPES.SAS_KEY,
  569. data: {
  570. key: pubKey,
  571. transactionId
  572. }
  573. }
  574. };
  575. this._sendMessage(keyMessage, pId);
  576. olmData.sasVerification.keySent = true;
  577. break;
  578. }
  579. case OLM_MESSAGE_TYPES.SAS_KEY: {
  580. if (!olmData.session) {
  581. logger.debug(`Received sas key message from ${pId} but we have no session for them!`);
  582. this._sendError(participant, 'No session found while processing sas-key');
  583. return;
  584. }
  585. if (!olmData.sasVerification) {
  586. logger.warn(`SAS_KEY Participant ${pId} does not have valid sasVerification`);
  587. this.eventEmitter.emit(
  588. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  589. pId,
  590. false,
  591. E2EEErrors.E2EE_SAS_INVALID_SAS_VERIFICATION);
  592. return;
  593. }
  594. const { isInitiator, sas, sasCommitment, startContent, keySent } = olmData.sasVerification;
  595. if (sas.is_their_key_set()) {
  596. logger.warn('SAS already has their key!');
  597. return;
  598. }
  599. const { key: theirKey, transactionId } = msg.data;
  600. if (sasCommitment) {
  601. const commitment = this._computeCommitment(theirKey, startContent);
  602. if (sasCommitment !== commitment) {
  603. this._sendError(participant, 'OlmAdapter commitments mismatched');
  604. this.eventEmitter.emit(
  605. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  606. pId,
  607. false,
  608. E2EEErrors.E2EE_SAS_COMMITMENT_MISMATCHED);
  609. olmData.sasVerification.free();
  610. return;
  611. }
  612. }
  613. sas.set_their_key(theirKey);
  614. const pubKey = sas.get_pubkey();
  615. const myInfo = `${this.myId}|${pubKey}`;
  616. const theirInfo = `${pId}|${theirKey}`;
  617. const info = isInitiator ? `${myInfo}|${theirInfo}` : `${theirInfo}|${myInfo}`;
  618. const sasBytes = sas.generate_bytes(info, OLM_SAS_NUM_BYTES);
  619. const generatedSas = generateSas(sasBytes);
  620. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_SAS_READY, pId, generatedSas);
  621. if (keySent) {
  622. return;
  623. }
  624. const keyMessage = {
  625. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  626. olm: {
  627. type: OLM_MESSAGE_TYPES.SAS_KEY,
  628. data: {
  629. key: pubKey,
  630. transactionId
  631. }
  632. }
  633. };
  634. this._sendMessage(keyMessage, pId);
  635. olmData.sasVerification.keySent = true;
  636. break;
  637. }
  638. case OLM_MESSAGE_TYPES.SAS_MAC: {
  639. if (!olmData.session) {
  640. logger.debug(`Received sas mac message from ${pId} but we have no session for them!`);
  641. this._sendError(participant, 'No session found while processing sas-mac');
  642. return;
  643. }
  644. const { keys, mac, transactionId } = msg.data;
  645. if (!mac || !keys) {
  646. logger.warn('Invalid SAS MAC message');
  647. return;
  648. }
  649. if (!olmData.sasVerification) {
  650. logger.warn(`SAS_MAC Participant ${pId} does not have valid sasVerification`);
  651. return;
  652. }
  653. const sas = olmData.sasVerification.sas;
  654. // Verify the received MACs.
  655. const baseInfo = `${OLM_KEY_VERIFICATION_MAC_INFO}${pId}${this.myId}${transactionId}`;
  656. const keysMac = sas.calculate_mac(
  657. Object.keys(mac).sort().join(','), // eslint-disable-line newline-per-chained-call
  658. baseInfo + OLM_KEY_VERIFICATION_MAC_KEY_IDS
  659. );
  660. if (keysMac !== keys) {
  661. logger.error('SAS verification error: keys MAC mismatch');
  662. this.eventEmitter.emit(
  663. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  664. pId,
  665. false,
  666. E2EEErrors.E2EE_SAS_KEYS_MAC_MISMATCH);
  667. return;
  668. }
  669. if (!olmData.ed25519) {
  670. logger.warn('SAS verification error: Missing ed25519 key');
  671. this.eventEmitter.emit(
  672. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  673. pId,
  674. false,
  675. E2EEErrors.E2EE_SAS_MISSING_KEY);
  676. return;
  677. }
  678. for (const [ keyInfo, computedMac ] of Object.entries(mac)) {
  679. const ourComputedMac = sas.calculate_mac(
  680. olmData.ed25519,
  681. baseInfo + keyInfo
  682. );
  683. if (computedMac !== ourComputedMac) {
  684. logger.error('SAS verification error: MAC mismatch');
  685. this.eventEmitter.emit(
  686. OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED,
  687. pId,
  688. false,
  689. E2EEErrors.E2EE_SAS_MAC_MISMATCH);
  690. return;
  691. }
  692. }
  693. logger.info(`SAS MAC verified for participant ${pId}`);
  694. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_VERIFICATION_COMPLETED, pId, true);
  695. break;
  696. }
  697. }
  698. }
  699. /**
  700. * Handles a participant leaving. When a participant leaves their olm session is destroyed.
  701. *
  702. * @private
  703. */
  704. _onParticipantLeft(id, participant) {
  705. logger.debug(`Participant ${id} left`);
  706. this.clearParticipantSession(participant);
  707. }
  708. /**
  709. * Handles an update in a participant's presence property.
  710. *
  711. * @param {JitsiParticipant} participant - The participant.
  712. * @param {string} name - The name of the property that changed.
  713. * @param {*} oldValue - The property's previous value.
  714. * @param {*} newValue - The property's new value.
  715. * @private
  716. */
  717. async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
  718. const participantId = participant.getId();
  719. const olmData = this._getParticipantOlmData(participant);
  720. switch (name) {
  721. case 'e2ee.enabled':
  722. if (newValue && this._conf.isE2EEEnabled()) {
  723. const localParticipantId = this._conf.myUserId();
  724. const participantFeatures = await participant.getFeatures();
  725. if (participantFeatures.has(FEATURE_E2EE) && localParticipantId < participantId) {
  726. if (this._sessionInitialization) {
  727. await this._sessionInitialization;
  728. }
  729. await this._sendSessionInit(participant);
  730. const uuid = uuidv4();
  731. const d = new Deferred();
  732. d.setRejectTimeout(REQ_TIMEOUT);
  733. d.catch(() => {
  734. this._reqs.delete(uuid);
  735. olmData.pendingSessionUuid = undefined;
  736. });
  737. this._reqs.set(uuid, d);
  738. const data = {
  739. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  740. olm: {
  741. type: OLM_MESSAGE_TYPES.KEY_INFO,
  742. data: {
  743. ciphertext: this._encryptKeyInfo(olmData.session),
  744. uuid
  745. }
  746. }
  747. };
  748. this._sendMessage(data, participantId);
  749. }
  750. }
  751. break;
  752. case 'e2ee.idKey.ed25519':
  753. olmData.ed25519 = newValue;
  754. this.eventEmitter.emit(OlmAdapterEvents.PARTICIPANT_SAS_AVAILABLE, participantId);
  755. break;
  756. }
  757. }
  758. /**
  759. * Builds and sends an error message to the target participant.
  760. *
  761. * @param {JitsiParticipant} participant - The target participant.
  762. * @param {string} error - The error message.
  763. * @returns {void}
  764. */
  765. _sendError(participant, error) {
  766. const pId = participant.getId();
  767. const err = {
  768. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  769. olm: {
  770. type: OLM_MESSAGE_TYPES.ERROR,
  771. data: {
  772. error
  773. }
  774. }
  775. };
  776. this._sendMessage(err, pId);
  777. }
  778. /**
  779. * Internal helper to send the given object to the given participant ID.
  780. * This function merely exists so the transport can be easily swapped.
  781. * Currently messages are transmitted via XMPP MUC private messages.
  782. *
  783. * @param {object} data - The data that will be sent to the target participant.
  784. * @param {string} participantId - ID of the target participant.
  785. */
  786. _sendMessage(data, participantId) {
  787. this._conf.sendMessage(data, participantId);
  788. }
  789. /**
  790. * Builds and sends the session-init request to the target participant.
  791. *
  792. * @param {JitsiParticipant} participant - Participant to whom we'll send the request.
  793. * @returns {Promise} - The promise will be resolved when the session-ack is received.
  794. * @private
  795. */
  796. _sendSessionInit(participant) {
  797. const pId = participant.getId();
  798. const olmData = this._getParticipantOlmData(participant);
  799. if (olmData.session) {
  800. logger.warn(`Tried to send session-init to ${pId} but we already have a session`);
  801. return Promise.reject();
  802. }
  803. if (olmData.pendingSessionUuid !== undefined) {
  804. logger.warn(`Tried to send session-init to ${pId} but we already have a pending session`);
  805. return Promise.reject();
  806. }
  807. // Generate a One Time Key.
  808. this._olmAccount.generate_one_time_keys(1);
  809. const otKeys = JSON.parse(this._olmAccount.one_time_keys());
  810. const otKey = Object.values(otKeys.curve25519)[0];
  811. if (!otKey) {
  812. return Promise.reject(new Error('No one-time-keys generated'));
  813. }
  814. // Mark the OT keys (one really) as published so they are not reused.
  815. this._olmAccount.mark_keys_as_published();
  816. const uuid = uuidv4();
  817. const init = {
  818. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  819. olm: {
  820. type: OLM_MESSAGE_TYPES.SESSION_INIT,
  821. data: {
  822. idKey: this._idKeys.curve25519,
  823. otKey,
  824. uuid
  825. }
  826. }
  827. };
  828. const d = new Deferred();
  829. d.setRejectTimeout(REQ_TIMEOUT);
  830. d.catch(() => {
  831. this._reqs.delete(uuid);
  832. olmData.pendingSessionUuid = undefined;
  833. });
  834. this._reqs.set(uuid, d);
  835. this._sendMessage(init, pId);
  836. // Store the UUID for matching with the ACK.
  837. olmData.pendingSessionUuid = uuid;
  838. return d;
  839. }
  840. /**
  841. * Builds and sends the SAS MAC message to the given participant.
  842. * The second phase of the verification process, the Key verification phase
  843. https://spec.matrix.org/latest/client-server-api/#short-authentication-string-sas-verification
  844. */
  845. _sendSasMac(participant) {
  846. const pId = participant.getId();
  847. const olmData = this._getParticipantOlmData(participant);
  848. const { sas, transactionId } = olmData.sasVerification;
  849. // Calculate and send MAC with the keys to be verified.
  850. const mac = {};
  851. const keyList = [];
  852. const baseInfo = `${OLM_KEY_VERIFICATION_MAC_INFO}${this.myId}${pId}${transactionId}`;
  853. const deviceKeyId = `ed25519:${pId}`;
  854. mac[deviceKeyId] = sas.calculate_mac(
  855. this._idKeys.ed25519,
  856. baseInfo + deviceKeyId);
  857. keyList.push(deviceKeyId);
  858. const keys = sas.calculate_mac(
  859. keyList.sort().join(','),
  860. baseInfo + OLM_KEY_VERIFICATION_MAC_KEY_IDS
  861. );
  862. const macMessage = {
  863. [JITSI_MEET_MUC_TYPE]: OLM_MESSAGE_TYPE,
  864. olm: {
  865. type: OLM_MESSAGE_TYPES.SAS_MAC,
  866. data: {
  867. keys,
  868. mac,
  869. transactionId
  870. }
  871. }
  872. };
  873. this._sendMessage(macMessage, pId);
  874. }
  875. /**
  876. * Computes the commitment.
  877. */
  878. _computeCommitment(pubKey, data) {
  879. const olmUtil = new Olm.Utility();
  880. const commitment = olmUtil.sha256(pubKey + JSON.stringify(data));
  881. olmUtil.free();
  882. return commitment;
  883. }
  884. }
  885. /**
  886. * Helper to ensure JSON parsing always returns an object.
  887. *
  888. * @param {string} data - The data that needs to be parsed.
  889. * @returns {object} - Parsed data or empty object in case of failure.
  890. */
  891. function safeJsonParse(data) {
  892. try {
  893. return JSON.parse(data);
  894. } catch (e) {
  895. return {};
  896. }
  897. }
  898. OlmAdapter.events = OlmAdapterEvents;