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

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