選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

OlmAdapter.js 36KB

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