Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

OlmAdapter.js 36KB

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