您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

E2EEncryption.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. /* global __filename */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import debounce from 'lodash.debounce';
  4. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  5. import RTCEvents from '../../service/RTC/RTCEvents';
  6. import browser from '../browser';
  7. import E2EEContext from './E2EEContext';
  8. import { OlmAdapter } from './OlmAdapter';
  9. import { importKey, ratchet } from './crypto-utils';
  10. const logger = getLogger(__filename);
  11. // Period which we'll wait before updating / rotating our keys when a participant
  12. // joins or leaves.
  13. const DEBOUNCE_PERIOD = 5000;
  14. // We use ECDSA with Curve P-521 for the long-term signing keys. See
  15. // https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams
  16. const SIGNATURE_OPTIONS = {
  17. name: 'ECDSA',
  18. namedCurve: 'P-521'
  19. };
  20. /**
  21. * This module integrates {@link E2EEContext} with {@link JitsiConference} in order to enable E2E encryption.
  22. */
  23. export class E2EEncryption {
  24. /**
  25. * A constructor.
  26. * @param {JitsiConference} conference - The conference instance for which E2E encryption is to be enabled.
  27. */
  28. constructor(conference) {
  29. this.conference = conference;
  30. this._conferenceJoined = false;
  31. this._enabled = false;
  32. this._initialized = false;
  33. this._key = undefined;
  34. this._signatureKeyPair = undefined;
  35. this._e2eeCtx = new E2EEContext();
  36. this._olmAdapter = new OlmAdapter(conference);
  37. // Debounce key rotation / ratcheting to avoid a storm of messages.
  38. this._ratchetKey = debounce(this._ratchetKeyImpl, DEBOUNCE_PERIOD);
  39. this._rotateKey = debounce(this._rotateKeyImpl, DEBOUNCE_PERIOD);
  40. // Participant join / leave operations. Used for key advancement / rotation.
  41. //
  42. this.conference.on(
  43. JitsiConferenceEvents.CONFERENCE_JOINED,
  44. () => {
  45. this._conferenceJoined = true;
  46. });
  47. this.conference.on(
  48. JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
  49. this._onParticipantPropertyChanged.bind(this));
  50. this.conference.on(
  51. JitsiConferenceEvents.USER_JOINED,
  52. this._onParticipantJoined.bind(this));
  53. this.conference.on(
  54. JitsiConferenceEvents.USER_LEFT,
  55. this._onParticipantLeft.bind(this));
  56. // Conference media events in order to attach the encryptor / decryptor.
  57. // FIXME add events to TraceablePeerConnection which will allow to see when there's new receiver or sender
  58. // added instead of shenanigans around conference track events and track muted.
  59. //
  60. this.conference.on(
  61. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  62. this._onMediaSessionStarted.bind(this));
  63. this.conference.on(
  64. JitsiConferenceEvents.TRACK_ADDED,
  65. track => track.isLocal() && this._onLocalTrackAdded(track));
  66. this.conference.rtc.on(
  67. RTCEvents.REMOTE_TRACK_ADDED,
  68. (track, tpc) => this._setupReceiverE2EEForTrack(tpc, track));
  69. this.conference.on(
  70. JitsiConferenceEvents.TRACK_MUTE_CHANGED,
  71. this._trackMuteChanged.bind(this));
  72. // Olm signalling events.
  73. this._olmAdapter.on(
  74. OlmAdapter.events.OLM_ID_KEY_READY,
  75. this._onOlmIdKeyReady.bind(this));
  76. this._olmAdapter.on(
  77. OlmAdapter.events.PARTICIPANT_E2EE_CHANNEL_READY,
  78. this._onParticipantE2EEChannelReady.bind(this));
  79. this._olmAdapter.on(
  80. OlmAdapter.events.PARTICIPANT_KEY_UPDATED,
  81. this._onParticipantKeyUpdated.bind(this));
  82. }
  83. /**
  84. * Indicates if E2EE is supported in the current platform.
  85. *
  86. * @param {object} config - Global configuration.
  87. * @returns {boolean}
  88. */
  89. static isSupported(config) {
  90. return browser.supportsInsertableStreams()
  91. && OlmAdapter.isSupported()
  92. && !(config.testing && config.testing.disableE2EE);
  93. }
  94. /**
  95. * Indicates whether E2EE is currently enabled or not.
  96. *
  97. * @returns {boolean}
  98. */
  99. isEnabled() {
  100. return this._enabled;
  101. }
  102. /**
  103. * Enables / disables End-To-End encryption.
  104. *
  105. * @param {boolean} enabled - whether E2EE should be enabled or not.
  106. * @returns {void}
  107. */
  108. async setEnabled(enabled) {
  109. if (enabled === this._enabled) {
  110. return;
  111. }
  112. this._enabled = enabled;
  113. if (!this._initialized && enabled) {
  114. // Generate a frame signing key pair. Per session currently.
  115. this._signatureKeyPair = await crypto.subtle.generateKey(SIGNATURE_OPTIONS,
  116. true, [ 'sign', 'verify' ]);
  117. this._e2eeCtx.setSignatureKey(this.conference.myUserId(), this._signatureKeyPair.privateKey);
  118. // Serialize the JWK of the signing key. Using JSON, might be easy to xml-ify.
  119. const serializedSigningKey = await crypto.subtle.exportKey('jwk', this._signatureKeyPair.publicKey);
  120. // TODO: sign this with the OLM account key.
  121. this.conference.setLocalParticipantProperty('e2ee.signatureKey', JSON.stringify(serializedSigningKey));
  122. // Need to re-create the peerconnections in order to apply the insertable streams constraint.
  123. // TODO: this was necessary due to some audio issues when indertable streams are used
  124. // even though encryption is not performed. This should be fixed in the browser eventually.
  125. // https://bugs.chromium.org/p/chromium/issues/detail?id=1103280
  126. this.conference._restartMediaSessions();
  127. this._initialized = true;
  128. }
  129. // Generate a random key in case we are enabling.
  130. this._key = enabled ? this._generateKey() : false;
  131. // Send it to others using the E2EE olm channel.
  132. this._olmAdapter.updateKey(this._key).then(index => {
  133. // Set our key so we begin encrypting.
  134. this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
  135. });
  136. }
  137. /**
  138. * Generates a new 256 bit random key.
  139. *
  140. * @returns {Uint8Array}
  141. * @private
  142. */
  143. _generateKey() {
  144. return window.crypto.getRandomValues(new Uint8Array(32));
  145. }
  146. /**
  147. * Setup E2EE on the new track that has been added to the conference, apply it on all the open peerconnections.
  148. * @param {JitsiLocalTrack} track - the new track that's being added to the conference.
  149. * @private
  150. */
  151. _onLocalTrackAdded(track) {
  152. for (const session of this.conference._getMediaSessions()) {
  153. this._setupSenderE2EEForTrack(session, track);
  154. }
  155. }
  156. /**
  157. * Setups E2E encryption for the new session.
  158. * @param {JingleSessionPC} session - the new media session.
  159. * @private
  160. */
  161. _onMediaSessionStarted(session) {
  162. const localTracks = this.conference.getLocalTracks();
  163. for (const track of localTracks) {
  164. this._setupSenderE2EEForTrack(session, track);
  165. }
  166. }
  167. /**
  168. * Publushes our own Olmn id key in presence.
  169. * @private
  170. */
  171. _onOlmIdKeyReady(idKey) {
  172. logger.debug(`Olm id key ready: ${idKey}`);
  173. // Publish it in presence.
  174. this.conference.setLocalParticipantProperty('e2ee.idKey', idKey);
  175. }
  176. /**
  177. * Advances (using ratcheting) the current key when a new participant joins the conference.
  178. * @private
  179. */
  180. _onParticipantJoined(id) {
  181. logger.debug(`Participant ${id} joined`);
  182. if (this._conferenceJoined && this._enabled) {
  183. this._ratchetKey();
  184. }
  185. }
  186. /**
  187. * Rotates the current key when a participant leaves the conference.
  188. * @private
  189. */
  190. _onParticipantLeft(id) {
  191. logger.debug(`Participant ${id} left`);
  192. this._e2eeCtx.cleanup(id);
  193. if (this._enabled) {
  194. this._rotateKey();
  195. }
  196. }
  197. /**
  198. * Event posted when the E2EE signalling channel has been established with the given participant.
  199. * @private
  200. */
  201. _onParticipantE2EEChannelReady(id) {
  202. logger.debug(`E2EE channel with participant ${id} is ready`);
  203. }
  204. /**
  205. * Handles an update in a participant's key.
  206. *
  207. * @param {string} id - The participant ID.
  208. * @param {Uint8Array | boolean} key - The new key for the participant.
  209. * @param {Number} index - The new key's index.
  210. * @private
  211. */
  212. _onParticipantKeyUpdated(id, key, index) {
  213. logger.debug(`Participant ${id} updated their key`);
  214. this._e2eeCtx.setKey(id, key, index);
  215. }
  216. /**
  217. * Handles an update in a participant's presence property.
  218. *
  219. * @param {JitsiParticipant} participant - The participant.
  220. * @param {string} name - The name of the property that changed.
  221. * @param {*} oldValue - The property's previous value.
  222. * @param {*} newValue - The property's new value.
  223. * @private
  224. */
  225. async _onParticipantPropertyChanged(participant, name, oldValue, newValue) {
  226. switch (name) {
  227. case 'e2ee.idKey':
  228. logger.debug(`Participant ${participant.getId()} updated their id key: ${newValue}`);
  229. break;
  230. case 'e2ee.signatureKey':
  231. logger.debug(`Participant ${participant.getId()} updated their signature key: ${newValue}`);
  232. if (newValue) {
  233. const parsed = JSON.parse(newValue);
  234. const importedKey = await crypto.subtle.importKey('jwk', parsed, { name: 'ECDSA',
  235. namedCurve: parsed.crv }, true, parsed.key_ops);
  236. this._e2eeCtx.setSignatureKey(participant.getId(), importedKey);
  237. } else {
  238. logger.warn(`e2ee signatureKey for ${participant.getId()} could not be updated with empty value.`);
  239. }
  240. break;
  241. }
  242. }
  243. /**
  244. * Advances the current key by using ratcheting.
  245. *
  246. * @private
  247. */
  248. async _ratchetKeyImpl() {
  249. logger.debug('Ratchetting key');
  250. const material = await importKey(this._key);
  251. const newKey = await ratchet(material);
  252. this._key = new Uint8Array(newKey);
  253. const index = await this._olmAdapter.updateCurrentKey(this._key);
  254. this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
  255. }
  256. /**
  257. * Rotates the local key. Rotating the key implies creating a new one, then distributing it
  258. * to all participants and once they all received it, start using it.
  259. *
  260. * @private
  261. */
  262. async _rotateKeyImpl() {
  263. logger.debug('Rotating key');
  264. this._key = this._generateKey();
  265. const index = await this._olmAdapter.updateKey(this._key);
  266. this._e2eeCtx.setKey(this.conference.myUserId(), this._key, index);
  267. }
  268. /**
  269. * Setup E2EE for the receiving side.
  270. *
  271. * @private
  272. */
  273. _setupReceiverE2EEForTrack(tpc, track) {
  274. if (!this._enabled) {
  275. return;
  276. }
  277. const receiver = tpc.findReceiverForTrack(track.track);
  278. if (receiver) {
  279. this._e2eeCtx.handleReceiver(receiver, track.getType(), track.getParticipantId());
  280. } else {
  281. logger.warn(`Could not handle E2EE for ${track}: receiver not found in: ${tpc}`);
  282. }
  283. }
  284. /**
  285. * Setup E2EE for the sending side.
  286. *
  287. * @param {JingleSessionPC} session - the session which sends the media produced by the track.
  288. * @param {JitsiLocalTrack} track - the local track for which e2e encoder will be configured.
  289. * @private
  290. */
  291. _setupSenderE2EEForTrack(session, track) {
  292. if (!this._enabled) {
  293. return;
  294. }
  295. const pc = session.peerconnection;
  296. const sender = pc && pc.findSenderForTrack(track.track);
  297. if (sender) {
  298. this._e2eeCtx.handleSender(sender, track.getType(), track.getParticipantId());
  299. } else {
  300. logger.warn(`Could not handle E2EE for ${track}: sender not found in ${pc}`);
  301. }
  302. }
  303. /**
  304. * Setup E2EE on the sender that is created for the unmuted local video track.
  305. * @param {JitsiLocalTrack} track - the track for which muted status has changed.
  306. * @private
  307. */
  308. _trackMuteChanged(track) {
  309. if (browser.doesVideoMuteByStreamRemove() && track.isLocal() && track.isVideoTrack() && !track.isMuted()) {
  310. for (const session of this.conference._getMediaSessions()) {
  311. this._setupSenderE2EEForTrack(session, track);
  312. }
  313. }
  314. }
  315. }