From f7bf0d013c54111c903468010b878994f118a13e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 23 Mar 2020 10:17:45 -0600 Subject: [PATCH 01/22] Support sending and receiving v1 Jitsi widgets Fixes https://github.com/vector-im/riot-web/issues/12816 Fixes https://github.com/vector-im/riot-web/issues/12814 --- src/CallHandler.js | 8 +++++++- src/utils/WidgetUtils.js | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index b21f07ad4f..362db939a3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -433,7 +433,13 @@ async function _startCallApp(roomId, type) { const confId = `JitsiConference_${generateHumanReadableId()}`; const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; - const widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); + let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); + + // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets + const parsedUrl = new URL(widgetUrl); + parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead + parsedUrl.searchParams.set('confId', confId); + widgetUrl = parsedUrl.toString(); const widgetData = { conferenceId: confId, diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 45ca7602bd..74e5f82c35 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -422,6 +422,22 @@ export default class WidgetUtils { app.eventId = eventId; app.name = app.name || app.type; + if (app.type === 'jitsi') { + console.log("Replacing Jitsi widget URL with local wrapper"); + if (!app.data || !app.data.conferenceId) { + // Assumed to be a v1 widget: add a data object for visibility on the wrapper + // TODO: Remove this once mobile supports v2 widgets + console.log("Replacing v1 Jitsi widget with v2 equivalent"); + const parsed = new URL(app.url); + app.data = { + conferenceId: parsed.searchParams.get("confId"), + domain: "jitsi.riot.im", // v1 widgets have this hardcoded + }; + } + + app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); + } + if (app.data) { Object.keys(app.data).forEach((key) => { params['$' + key] = app.data[key]; @@ -430,11 +446,6 @@ export default class WidgetUtils { app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true); } - if (app.type === 'jitsi') { - console.log("Replacing Jitsi widget URL with local wrapper"); - app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); - } - app.url = encodeUri(app.url, params); return app; From d7953dfa6dfc18ce0161ab8cf1a46359bab83068 Mon Sep 17 00:00:00 2001 From: thobyv-kismat Date: Tue, 24 Mar 2020 13:09:06 +0100 Subject: [PATCH 02/22] fix formatbar not hidden on highlighted message sent --- src/components/views/rooms/BasicMessageComposer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 147f3c0af8..899b3360d8 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -152,6 +152,7 @@ export default class BasicMessageEditor extends React.Component { if (this.props.placeholder) { const {isEmpty} = this.props.model; if (isEmpty) { + this._formatBarRef.hide(); this._showPlaceholder(); } else { this._hidePlaceholder(); From 4ff847c8a32fcefef52a0e872704e6f4f9caeba3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 15:57:04 +0100 Subject: [PATCH 03/22] put CompleteSecurity state management in store and split off a child component (SetupCrossSigningBody) that can be reused from the "Verify this session" toast. --- .../structures/auth/CompleteSecurity.js | 235 ++---------------- .../structures/auth/SetupEncryptionBody.js | 196 +++++++++++++++ src/stores/SetupEncryptionStore.js | 147 +++++++++++ 3 files changed, 366 insertions(+), 212 deletions(-) create mode 100644 src/components/structures/auth/SetupEncryptionBody.js create mode 100644 src/stores/SetupEncryptionStore.js diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 3154564cd3..06cece0af2 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -18,13 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager'; - -const PHASE_INTRO = 0; -const PHASE_BUSY = 1; -const PHASE_DONE = 2; -const PHASE_CONFIRM_SKIP = 3; +import { + SetupEncryptionStore, + PHASE_INTRO, + PHASE_BUSY, + PHASE_DONE, + PHASE_CONFIRM_SKIP, +} from '../../../stores/SetupEncryptionStore'; +import SetupEncryptionBody from "./SetupEncryptionBody"; export default class CompleteSecurity extends React.Component { static propTypes = { @@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component { constructor() { super(); - - this.state = { - phase: PHASE_INTRO, - // this serves dual purpose as the object for the request logic and - // the presence of it insidicating that we're in 'verify mode'. - // Because of the latter, it lives in the state. - verificationRequest: null, - backupInfo: null, - }; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this._onStoreUpdate); + store.start(); + this.state = {phase: store.phase}; } + _onStoreUpdate = () => { + const store = SetupEncryptionStore.sharedInstance(); + this.setState({phase: store.phase}); + }; + componentWillUnmount() { - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - } - } - - _onUsePassphraseClick = async () => { - this.setState({ - phase: PHASE_BUSY, - }); - const cli = MatrixClientPeg.get(); - try { - const backupInfo = await cli.getKeyBackupVersion(); - this.setState({backupInfo}); - - // The control flow is fairly twisted here... - // For the purposes of completing security, we only wait on getting - // as far as the trust check and then show a green shield. - // We also begin the key backup restore as well, which we're - // awaiting inside `accessSecretStorage` only so that it keeps your - // passphase cached for that work. This dialog itself will only wait - // on the first trust check, and the key backup restore will happen - // in the background. - await new Promise((resolve, reject) => { - try { - accessSecretStorage(async () => { - await cli.checkOwnCrossSigningTrust(); - resolve(); - if (backupInfo) { - // A complete restore can take many minutes for large - // accounts / slow servers, so we allow the dialog - // to advance before this. - await cli.restoreKeyBackupWithSecretStorage(backupInfo); - } - }); - } catch (e) { - console.error(e); - reject(e); - } - }); - - if (cli.getCrossSigningId()) { - this.setState({ - phase: PHASE_DONE, - }); - } - } catch (e) { - if (!(e instanceof AccessCancelledError)) { - console.log(e); - } - // this will throw if the user hits cancel, so ignore - this.setState({ - phase: PHASE_INTRO, - }); - } - } - - onVerificationRequest = async (request) => { - if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; - - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - await request.accept(); - request.on("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: request, - }); - } - - onVerificationRequestChange = () => { - if (this.state.verificationRequest.cancelled) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: null, - }); - } - } - - onSkipClick = () => { - this.setState({ - phase: PHASE_CONFIRM_SKIP, - }); - } - - onSkipConfirmClick = () => { - this.props.onFinished(); - } - - onSkipBackClick = () => { - this.setState({ - phase: PHASE_INTRO, - }); - } - - onDoneClick = () => { - this.props.onFinished(); + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); } render() { const AuthPage = sdk.getComponent("auth.AuthPage"); const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - - const { - phase, - } = this.state; - + const {phase} = this.state; let icon; let title; - let body; - - if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); - body = ; - } else if (phase === PHASE_INTRO) { - const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + if (phase === PHASE_INTRO) { icon = ; title = _t("Complete security"); - body = ( -
-

{_t( - "Open an existing session & use it to verify this one, " + - "granting it access to encrypted messages.", - )}

-

{_t("Waiting…")}

-

{_t( - "If you can’t access one, ", - {}, { - button: sub => - {sub} - , - })}

-
- - {_t("Skip")} - -
-
- ); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); - let message; - if (this.state.backupInfo) { - message =

{_t( - "Your new session is now verified. It has access to your " + - "encrypted messages, and other users will see it as trusted.", - )}

; - } else { - message =

{_t( - "Your new session is now verified. Other users will see it as trusted.", - )}

; - } - body = ( -
-
- {message} -
- - {_t("Done")} - -
-
- ); } else if (phase === PHASE_CONFIRM_SKIP) { icon = ; title = _t("Are you sure?"); - body = ( -
-

{_t( - "Without completing security on this session, it won’t have " + - "access to encrypted messages.", - )}

-
- - {_t("Skip")} - - - {_t("Go Back")} - -
-
- ); } else if (phase === PHASE_BUSY) { - const Spinner = sdk.getComponent('views.elements.Spinner'); icon = ; title = _t("Complete security"); - body = ; } else { throw new Error(`Unknown phase ${phase}`); } @@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component { {title}
- {body} +
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js new file mode 100644 index 0000000000..a59fa08b32 --- /dev/null +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -0,0 +1,196 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import * as sdk from '../../../index'; +import { + SetupEncryptionStore, + PHASE_INTRO, + PHASE_BUSY, + PHASE_DONE, + PHASE_CONFIRM_SKIP, + PHASE_FINISHED, +} from '../../../stores/SetupEncryptionStore'; + +export default class SetupEncryptionBody extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this._onStoreUpdate); + store.start(); + this.state = { + phase: store.phase, + // this serves dual purpose as the object for the request logic and + // the presence of it insidicating that we're in 'verify mode'. + // Because of the latter, it lives in the state. + verificationRequest: store.verificationRequest, + backupInfo: store.backupInfo, + }; + } + + _onStoreUpdate = () => { + const store = SetupEncryptionStore.sharedInstance(); + if (store.phase === PHASE_FINISHED) { + this.props.onFinished(); + return; + } + this.setState({ + phase: store.phase, + verificationRequest: store.verificationRequest, + backupInfo: store.backupInfo, + }); + }; + + componentWillUnmount() { + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); + } + + _onUsePassphraseClick = async () => { + const store = SetupEncryptionStore.sharedInstance(); + store.usePassPhrase(); + } + + onSkipClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skip(); + } + + onSkipConfirmClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skipConfirm(); + } + + onSkipBackClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.returnAfterSkip(); + } + + onDoneClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.done(); + } + + render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const { + phase, + } = this.state; + + if (this.state.verificationRequest) { + const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); + return ; + } else if (phase === PHASE_INTRO) { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + return ( +
+

{_t( + "Open an existing session & use it to verify this one, " + + "granting it access to encrypted messages.", + )}

+

{_t("Waiting…")}

+

{_t( + "If you can’t access one, ", + {}, { + button: sub => + {sub} + , + })}

+
+ + {_t("Skip")} + +
+
+ ); + } else if (phase === PHASE_DONE) { + let message; + if (this.state.backupInfo) { + message =

{_t( + "Your new session is now verified. It has access to your " + + "encrypted messages, and other users will see it as trusted.", + )}

; + } else { + message =

{_t( + "Your new session is now verified. Other users will see it as trusted.", + )}

; + } + return ( +
+
+ {message} +
+ + {_t("Done")} + +
+
+ ); + } else if (phase === PHASE_CONFIRM_SKIP) { + return ( +
+

{_t( + "Without completing security on this session, it won’t have " + + "access to encrypted messages.", + )}

+
+ + {_t("Skip")} + + + {_t("Go Back")} + +
+
+ ); + } else if (phase === PHASE_BUSY) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return ; + } else { + throw new Error(`Unknown phase ${phase}`); + } + } +} diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js new file mode 100644 index 0000000000..93c1770b1f --- /dev/null +++ b/src/stores/SetupEncryptionStore.js @@ -0,0 +1,147 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventEmitter from 'events'; +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { accessSecretStorage, AccessCancelledError } from '../CrossSigningManager'; + +export const PHASE_INTRO = 0; +export const PHASE_BUSY = 1; +export const PHASE_DONE = 2; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 3; +export const PHASE_FINISHED = 4; //UX can be closed + +/** + * Holds the active "Complete Security" session + */ +export class SetupEncryptionStore extends EventEmitter { + static sharedInstance() { + if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); + return global.mx_SetupEncryptionStore; + } + + start() { + if (this._started) { + return; + } + this._started = true; + this.phase = PHASE_INTRO; + this.verificationRequest = null; + this.backupInfo = null; + MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); + } + + stop() { + if (!this._started) { + return; + } + this._started = false; + if (this.verificationRequest) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + } + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); + } + } + + async usePassPhrase() { + this.phase = PHASE_BUSY; + this.emit("update"); + const cli = MatrixClientPeg.get(); + try { + const backupInfo = await cli.getKeyBackupVersion(); + this.backupInfo = backupInfo; + this.emit("update"); + // The control flow is fairly twisted here... + // For the purposes of completing security, we only wait on getting + // as far as the trust check and then show a green shield. + // We also begin the key backup restore as well, which we're + // awaiting inside `accessSecretStorage` only so that it keeps your + // passphase cached for that work. This dialog itself will only wait + // on the first trust check, and the key backup restore will happen + // in the background. + await new Promise((resolve, reject) => { + try { + accessSecretStorage(async () => { + await cli.checkOwnCrossSigningTrust(); + resolve(); + if (backupInfo) { + // A complete restore can take many minutes for large + // accounts / slow servers, so we allow the dialog + // to advance before this. + await cli.restoreKeyBackupWithSecretStorage(backupInfo); + } + }).catch(reject); + } catch (e) { + console.error(e); + reject(e); + } + }); + + if (cli.getCrossSigningId()) { + this.phase = PHASE_DONE; + this.emit("update"); + } + } catch (e) { + if (!(e instanceof AccessCancelledError)) { + console.log(e); + } + // this will throw if the user hits cancel, so ignore + this.phase = PHASE_INTRO; + this.emit("update"); + } + } + + onVerificationRequest = async (request) => { + if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; + + if (this.verificationRequest) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + } + this.verificationRequest = request; + await request.accept(); + request.on("change", this.onVerificationRequestChange); + this.emit("update"); + } + + onVerificationRequestChange = () => { + if (this.verificationRequest.cancelled) { + this.verificationRequest.off("change", this.onVerificationRequestChange); + this.verificationRequest = null; + this.emit("update"); + } + } + + skip() { + this.phase = PHASE_CONFIRM_SKIP; + this.emit("update"); + } + + skipConfirm() { + this.phase = PHASE_FINISHED; + this.emit("update"); + } + + returnAfterSkip() { + this.phase = PHASE_INTRO; + this.emit("update"); + } + + done() { + this.phase = PHASE_FINISHED; + this.emit("update"); + } +} From 3e59127d12a83e55def0f7003c39c7e82cc33a2f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 16:10:43 +0100 Subject: [PATCH 04/22] use SetupEncryptionBody to show a dialog from "Verify this session" --- .../views/dialogs/SetupEncryptionDialog.js | 29 +++++++++++++++++++ .../views/toasts/SetupEncryptionToast.js | 9 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/components/views/dialogs/SetupEncryptionDialog.js diff --git a/src/components/views/dialogs/SetupEncryptionDialog.js b/src/components/views/dialogs/SetupEncryptionDialog.js new file mode 100644 index 0000000000..f32a289a29 --- /dev/null +++ b/src/components/views/dialogs/SetupEncryptionDialog.js @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; + +export default function SetupEncryptionDialog({onFinished}) { + return + + ; +} diff --git a/src/components/views/toasts/SetupEncryptionToast.js b/src/components/views/toasts/SetupEncryptionToast.js index 9016e4c6d7..ad6488a9bb 100644 --- a/src/components/views/toasts/SetupEncryptionToast.js +++ b/src/components/views/toasts/SetupEncryptionToast.js @@ -18,7 +18,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; import DeviceListener from '../../../DeviceListener'; +import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog"; import { accessSecretStorage } from '../../../CrossSigningManager'; export default class SetupEncryptionToast extends React.PureComponent { @@ -32,7 +34,12 @@ export default class SetupEncryptionToast extends React.PureComponent { }; _onSetupClick = async () => { - accessSecretStorage(); + if (this.props.kind === "verify_this_session") { + Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog, + {}, null, /* priority = */ false, /* static = */ true); + } else { + accessSecretStorage(); + } }; getDescription() { From c53b07a35a73d8a83c0e1bfaebdb0284e03b398d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 24 Mar 2020 15:49:51 +0000 Subject: [PATCH 05/22] Add logging when secrets are missing from cache --- src/CrossSigningManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5c254bbd00..097464ee43 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -149,9 +149,15 @@ const onSecretRequested = async function({ if (!callbacks.getCrossSigningKeyCache) return; if (name === "m.cross_signing.self_signing") { const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log(`self_signing requested by ${deviceId}, but not found in cache`); + } return key && encodeBase64(key); } else if (name === "m.cross_signing.user_signing") { const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log(`user_signing requested by ${deviceId}, but not found in cache`); + } return key && encodeBase64(key); } console.warn("onSecretRequested didn't recognise the secret named ", name); From 1c802cc6afd6e27effc791238e07da13e8dcbf86 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 24 Mar 2020 15:50:08 +0000 Subject: [PATCH 06/22] Show private key cache state in debug panel --- .../views/settings/CrossSigningPanel.js | 17 +++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index cf47c797fc..b960434ca1 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -32,6 +32,8 @@ export default class CrossSigningPanel extends React.PureComponent { error: null, crossSigningPublicKeysOnDevice: false, crossSigningPrivateKeysInStorage: false, + selfSigningPrivateKeyCached: false, + userSigningPrivateKeyCached: false, secretStorageKeyInAccount: false, secretStorageKeyNeedsUpgrade: null, }; @@ -71,10 +73,13 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); + const pkCache = cli.getCrossSigningCacheCallbacks(); const crossSigning = cli._crypto._crossSigningInfo; const secretStorage = cli._crypto._secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); + const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing")); + const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing")); const secretStorageKeyInAccount = await secretStorage.hasKey(); const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); @@ -84,6 +89,8 @@ export default class CrossSigningPanel extends React.PureComponent { this.setState({ crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, + selfSigningPrivateKeyCached, + userSigningPrivateKeyCached, secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, @@ -130,6 +137,8 @@ export default class CrossSigningPanel extends React.PureComponent { error, crossSigningPublicKeysOnDevice, crossSigningPrivateKeysInStorage, + selfSigningPrivateKeyCached, + userSigningPrivateKeyCached, secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, @@ -209,6 +218,14 @@ export default class CrossSigningPanel extends React.PureComponent { {_t("Cross-signing private keys:")} {crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")} + + {_t("Self signing private key:")} + {selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")} + + + {_t("User signing private key:")} + {userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")} + {_t("Secret storage public key:")} {secretStorageKeyInAccount ? _t("in account data") : _t("not found")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 57b39309b0..12bd462937 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -582,6 +582,10 @@ "not found": "not found", "Cross-signing private keys:": "Cross-signing private keys:", "in secret storage": "in secret storage", + "Self signing private key:": "Self signing private key:", + "cached locally": "cached locally", + "not found locally": "not found locally", + "User signing private key:": "User signing private key:", "Secret storage public key:": "Secret storage public key:", "in account data": "in account data", "Homeserver feature support:": "Homeserver feature support:", From bdcb65de77107a45875c0854441594720439cb84 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 Mar 2020 09:55:54 -0600 Subject: [PATCH 07/22] Support and send the config over to capable widgets For https://github.com/vector-im/riot-web/pull/12845 --- src/FromWidgetPostMessageApi.js | 11 +++++++- src/WidgetMessaging.js | 11 ++++++++ src/components/views/elements/AppTile.js | 6 +++++ src/utils/WidgetUtils.js | 8 ++++-- src/widgets/WidgetApi.ts | 34 +++++++++++++++++++++--- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 64caba0fdf..ea76c85643 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; +import SdkConfig from "./SdkConfig"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi { const data = event.data.data; const val = data.value; - if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } } else if (action === 'get_openid') { // Handled by caller + } else if (action === KnownWidgetActions.GetRiotWebConfig) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) { + this.sendResponse(event, { + api: INBOUND_API_NAME, + config: SdkConfig.get(), + }); + } } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index d40a8ab637..b0cfe963f3 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetUtils from "./utils/WidgetUtils"; +import {KnownWidgetActions} from "./widgets/WidgetApi"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -75,6 +76,16 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget that the client is ready to handle further widget requests. + */ + flagReadyToContinue() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClientReady, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index a26478c461..0a8bf7443b 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -419,6 +419,12 @@ export default class AppTile extends React.Component { if (this.props.onCapabilityRequest) { this.props.onCapabilityRequest(requestedCapabilities); } + + // We only tell Jitsi widgets that we're ready because they're realistically the only ones + // using this custom extension to the widget API. + if (this.props.type === 'jitsi') { + widgetMessaging.flagReadyToContinue(); + } }).catch((err) => { console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err); }); diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 74e5f82c35..eea995cfea 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -28,6 +28,7 @@ const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; +import {Capability} from "../widgets/WidgetApi"; /** * Encodes a URI according to a set of template variables. Variables will be @@ -454,12 +455,15 @@ export default class WidgetUtils { static getCapWhitelistForAppTypeInRoomId(appType, roomId) { const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); - const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : []; + const capWhitelist = enableScreenshots ? [Capability.Screenshot] : []; // Obviously anyone that can add a widget can claim it's a jitsi widget, // so this doesn't really offer much over the set of domains we load // widgets from at all, but it probably makes sense for sanity. - if (appType == 'jitsi') capWhitelist.push("m.always_on_screen"); + if (appType === 'jitsi') { + capWhitelist.push(Capability.AlwaysOnScreen); + capWhitelist.push(Capability.GetRiotWebConfig); + } return capWhitelist; } diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index c19e34ae43..d6d1c79a99 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -23,6 +23,7 @@ export enum Capability { Screenshot = "m.capability.screenshot", Sticker = "m.sticker", AlwaysOnScreen = "m.always_on_screen", + GetRiotWebConfig = "im.vector.web.riot_config", } export enum KnownWidgetActions { @@ -33,7 +34,10 @@ export enum KnownWidgetActions { UpdateVisibility = "visibility", ReceiveOpenIDCredentials = "openid_credentials", SetAlwaysOnScreen = "set_always_on_screen", + GetRiotWebConfig = "im.vector.web.riot_config", + ClientReady = "im.vector.ready", } + export type WidgetAction = KnownWidgetActions | string; export enum WidgetApiType { @@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest { */ export class WidgetApi { private origin: string; - private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {}; + private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; private readyPromise: Promise; private readyPromiseResolve: () => void; + /** + * Set this to true if your widget is expecting a ready message from the client. False otherwise (default). + */ + public expectingExplicitReady = false; + constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { this.origin = new URL(currentUrl).origin; @@ -83,7 +92,14 @@ export class WidgetApi { if (payload.action === KnownWidgetActions.GetCapabilities) { this.onCapabilitiesRequest(payload); + if (!this.expectingExplicitReady) { + this.readyPromiseResolve(); + } + } else if (payload.action === KnownWidgetActions.ClientReady) { this.readyPromiseResolve(); + + // Automatically acknowledge so we can move on + this.replyToRequest(payload, {}); } else { console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); } @@ -126,7 +142,10 @@ export class WidgetApi { data: payload, response: {}, // Not used at this layer - it's used when the client responds }; - this.inFlightRequests[request.requestId] = callback; + + if (callback) { + this.inFlightRequests[request.requestId] = callback; + } console.log(`[WidgetAPI] Sending request: `, request); window.parent.postMessage(request, "*"); @@ -134,7 +153,16 @@ export class WidgetApi { public setAlwaysOnScreen(onScreen: boolean): Promise { return new Promise(resolve => { - this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve); + this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null); + resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change. + }); + } + + public getRiotConfig(): Promise { + return new Promise(resolve => { + this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => { + resolve(response.response.config); + }); }); } } From 7cded53cdb118c3fee1bc21e60148d60a88f0c74 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 17:02:36 +0100 Subject: [PATCH 08/22] fix i18n --- src/i18n/strings/en_EN.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 57b39309b0..9dcea47cc8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2006,14 +2006,7 @@ "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Could not load user profile": "Could not load user profile", "Complete security": "Complete security", - "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.", - "Waiting…": "Waiting…", - "If you can’t access one, ": "If you can’t access one, ", "Session verified": "Session verified", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", - "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", - "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", - "Go Back": "Go Back", "Failed to send email": "Failed to send email", "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", "A new password must be entered.": "A new password must be entered.", @@ -2063,6 +2056,13 @@ "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", "Create your account": "Create your account", + "Open an existing session & use it to verify this one, granting it access to encrypted messages.": "Open an existing session & use it to verify this one, granting it access to encrypted messages.", + "Waiting…": "Waiting…", + "If you can’t access one, ": "If you can’t access one, ", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", + "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", + "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", + "Go Back": "Go Back", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Failed to re-authenticate": "Failed to re-authenticate", "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.", From 7d4e4982575d9bc97f5ba8a2eb47a927f36ecf4f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Mar 2020 17:03:40 +0100 Subject: [PATCH 09/22] fix lint --- src/stores/SetupEncryptionStore.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 93c1770b1f..7b42e1552d 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -24,9 +24,6 @@ export const PHASE_DONE = 2; //final done stage, but still showing UX export const PHASE_CONFIRM_SKIP = 3; export const PHASE_FINISHED = 4; //UX can be closed -/** - * Holds the active "Complete Security" session - */ export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); From 7ea61e41055fef1ba199e78af6420dbd7405dce4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 24 Mar 2020 10:05:57 -0600 Subject: [PATCH 10/22] Appease the linter --- src/WidgetMessaging.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index b0cfe963f3..30c2389b1e 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -78,6 +78,7 @@ export default class WidgetMessaging { /** * Tells the widget that the client is ready to handle further widget requests. + * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message. */ flagReadyToContinue() { return this.messageToWidget({ From 91b9a04ede6ebb18bd6a53e756a1f2f6a24399d4 Mon Sep 17 00:00:00 2001 From: thobyv-kismat Date: Tue, 24 Mar 2020 19:00:43 +0100 Subject: [PATCH 11/22] refactor:consider checking if no placeholder --- src/components/views/rooms/BasicMessageComposer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 899b3360d8..707dc12a97 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -149,15 +149,17 @@ export default class BasicMessageEditor extends React.Component { const position = selection.end || selection; this._setLastCaretFromPosition(position); } + const {isEmpty} = this.props.model; if (this.props.placeholder) { - const {isEmpty} = this.props.model; if (isEmpty) { - this._formatBarRef.hide(); this._showPlaceholder(); } else { this._hidePlaceholder(); } } + if(isEmpty) { + this._formatBarRef.hide(); + } this.setState({autoComplete: this.props.model.autoComplete}); this.historyManager.tryPush(this.props.model, selection, inputType, diff); TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty); From b817c06c6e6259b0f23c21c1329a061490ae3c10 Mon Sep 17 00:00:00 2001 From: thobyv-kismat Date: Tue, 24 Mar 2020 19:09:50 +0100 Subject: [PATCH 12/22] fix failing linter tests --- src/components/views/rooms/BasicMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 707dc12a97..75455518a1 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -157,7 +157,7 @@ export default class BasicMessageEditor extends React.Component { this._hidePlaceholder(); } } - if(isEmpty) { + if (isEmpty) { this._formatBarRef.hide(); } this.setState({autoComplete: this.props.model.autoComplete}); From f6e9c32c48081be68a6b9bbf5ff3b86a0d56704a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 12:04:09 +0100 Subject: [PATCH 13/22] fall back to non-standard persisted api for Safari --- src/rageshake/submit-rageshake.js | 6 ++++++ src/utils/StorageManager.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 53e9f24788..5a7039a913 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -119,6 +119,12 @@ export default async function sendBugReport(bugReportEndpoint, opts) { body.append("storageManager_persisted", await navigator.storage.persisted()); } catch (e) {} } + // Safari + if (document.hasStorageAccess) { + try { + body.append("storageManager_persisted", await document.hasStorageAccess()); + } catch (e) {} + } if (navigator.storage && navigator.storage.estimate) { try { const estimate = await navigator.storage.estimate(); diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 4ed118da8a..175772903d 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -48,6 +48,11 @@ export function tryPersistStorage() { navigator.storage.persist().then(persistent => { console.log("StorageManager: Persistent?", persistent); }); + } else if (document.requestStorageAccess) { //Safari + document.requestStorageAccess().then( + () => console.log("StorageManager: Persistent?", true), + () => console.log("StorageManager: Persistent?", false), + ); } else { console.log("StorageManager: Persistence unsupported"); } From 9a5f4d9b22b2dadaddcdca3e0e17acbd969bc9dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 12:07:11 +0100 Subject: [PATCH 14/22] fall back, don't do both on FF --- src/rageshake/submit-rageshake.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 5a7039a913..00ef87f89c 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -118,9 +118,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) { try { body.append("storageManager_persisted", await navigator.storage.persisted()); } catch (e) {} - } - // Safari - if (document.hasStorageAccess) { + } else if (document.hasStorageAccess) { // Safari try { body.append("storageManager_persisted", await document.hasStorageAccess()); } catch (e) {} From 8b2ae3e20ff2eb67ff508afd97e40d24c6a2cb9d Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 24 Mar 2020 12:24:12 +0000 Subject: [PATCH 15/22] If cached keys are present in the key backup dialog, use them --- .../keybackup/RestoreKeyBackupDialog.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 8e4a4e1e60..5f6f08453d 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -200,6 +200,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } } + async _restoreWithCachedKey(backupInfo) { + if (!backupInfo) return false; + try { + const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( + undefined, /* targetRoomId */ + undefined, /* targetSessionId */ + backupInfo + ); + this.setState({ + recoverInfo + }); + return true; + } catch (e) { + console.log("restoreWithCachedKey failed:", e); + return false; + } + } + async _loadBackupStatus() { this.setState({ loading: true, @@ -213,6 +231,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { backupKeyStored, }); + const gotCache = await this._restoreWithCachedKey(backupInfo); + if (gotCache) { + console.log("RestoreKeyBackupDialog: found cached backup key"); + this.setState({ + loading: false, + }); + return; + } + // If the backup key is stored, we can proceed directly to restore. if (backupKeyStored) { return this._restoreWithSecretStorage(); From 936a4a00225871442e82a3bb99ff9002f2196316 Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 25 Mar 2020 11:47:07 +0000 Subject: [PATCH 16/22] lint --- .../views/dialogs/keybackup/RestoreKeyBackupDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 5f6f08453d..aecfa9bfd1 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -206,10 +206,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( undefined, /* targetRoomId */ undefined, /* targetSessionId */ - backupInfo + backupInfo, ); this.setState({ - recoverInfo + recoverInfo, }); return true; } catch (e) { From 792a7b395394f5bffe7d0009b4d9089509a7d03e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 13:07:07 +0100 Subject: [PATCH 17/22] don't throw here --- src/components/structures/auth/SetupEncryptionBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index a59fa08b32..e8c15bd1af 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -190,7 +190,7 @@ export default class SetupEncryptionBody extends React.Component { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { - throw new Error(`Unknown phase ${phase}`); + console.log(`SetupEncryptionBody: Unknown phase ${phase}`); } } } From 78b167a7ea4f6a055c248c1d3e24c01f71345108 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 13:40:09 +0100 Subject: [PATCH 18/22] fix typo --- src/components/structures/auth/SetupEncryptionBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index e8c15bd1af..c7c73cd616 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -41,7 +41,7 @@ export default class SetupEncryptionBody extends React.Component { this.state = { phase: store.phase, // this serves dual purpose as the object for the request logic and - // the presence of it insidicating that we're in 'verify mode'. + // the presence of it indicating that we're in 'verify mode'. // Because of the latter, it lives in the state. verificationRequest: store.verificationRequest, backupInfo: store.backupInfo, From 4d63c11f260f78e93d01f025d55ae028a2897cd8 Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 25 Mar 2020 14:06:47 +0000 Subject: [PATCH 19/22] Respond to backup key sharing requests --- src/CrossSigningManager.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 097464ee43..5def8d9fd5 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -145,18 +145,33 @@ const onSecretRequested = async function({ console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); return; } - const callbacks = client.getCrossSigningCacheCallbacks(); - if (!callbacks.getCrossSigningKeyCache) return; - if (name === "m.cross_signing.self_signing") { - const key = await callbacks.getCrossSigningKeyCache("self_signing"); - if (!key) { - console.log(`self_signing requested by ${deviceId}, but not found in cache`); + if (name.startsWith("m.cross_signing")) { + const callbacks = client.getCrossSigningCacheCallbacks(); + if (!callbacks.getCrossSigningKeyCache) return; + /* Explicit enumeration here is deliberate – never share the master key! */ + if (name === "m.cross_signing.self_signing") { + const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log( + `self_signing requested by ${deviceId}, but not found in cache` + ); + } + return key && encodeBase64(key); + } else if (name === "m.cross_signing.user_signing") { + const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log( + `user_signing requested by ${deviceId}, but not found in cache` + ); + } + return key && encodeBase64(key); } - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); + } else if (name === "m.megolm_backup.v1") { + const key = await client._crypto.getSessionBackupPrivateKey(); if (!key) { - console.log(`user_signing requested by ${deviceId}, but not found in cache`); + console.log( + `session backup key requested by ${deviceId}, but not found in cache` + ); } return key && encodeBase64(key); } From f891f3e9fa70ffc83fb679d5364d5dfc909ad42b Mon Sep 17 00:00:00 2001 From: Zoe Date: Wed, 25 Mar 2020 16:08:26 +0000 Subject: [PATCH 20/22] lint --- src/CrossSigningManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5def8d9fd5..29eb3cb8be 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -153,7 +153,7 @@ const onSecretRequested = async function({ const key = await callbacks.getCrossSigningKeyCache("self_signing"); if (!key) { console.log( - `self_signing requested by ${deviceId}, but not found in cache` + `self_signing requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); @@ -161,7 +161,7 @@ const onSecretRequested = async function({ const key = await callbacks.getCrossSigningKeyCache("user_signing"); if (!key) { console.log( - `user_signing requested by ${deviceId}, but not found in cache` + `user_signing requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); @@ -170,7 +170,7 @@ const onSecretRequested = async function({ const key = await client._crypto.getSessionBackupPrivateKey(); if (!key) { console.log( - `session backup key requested by ${deviceId}, but not found in cache` + `session backup key requested by ${deviceId}, but not found in cache`, ); } return key && encodeBase64(key); From 0097134ade4c3953d57fa64e33c70d3b45d92c5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Mar 2020 17:56:35 +0000 Subject: [PATCH 21/22] Update src/utils/StorageManager.js Co-Authored-By: J. Ryan Stinnett --- src/utils/StorageManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.js index 175772903d..e29b6d9b0e 100644 --- a/src/utils/StorageManager.js +++ b/src/utils/StorageManager.js @@ -48,7 +48,7 @@ export function tryPersistStorage() { navigator.storage.persist().then(persistent => { console.log("StorageManager: Persistent?", persistent); }); - } else if (document.requestStorageAccess) { //Safari + } else if (document.requestStorageAccess) { // Safari document.requestStorageAccess().then( () => console.log("StorageManager: Persistent?", true), () => console.log("StorageManager: Persistent?", false), From 9c20bf22ce47de52796bd1d44db80554f56a856e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Mar 2020 10:45:26 +0000 Subject: [PATCH 22/22] Fix soft-crash on bad permalinks Fixes https://github.com/vector-im/riot-web/issues/12880 --- src/linkify-matrix.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index cff7a93d08..ee9f703136 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -200,13 +200,17 @@ matrixLinkify.options = { switch (type) { case "url": { // intercept local permalinks to users and show them like userids (in userinfo of current room) - const permalink = parsePermalink(href); - if (permalink && permalink.userId) { - return { - click: function(e) { - matrixLinkify.onUserClick(e, permalink.userId); - }, - }; + try { + const permalink = parsePermalink(href); + if (permalink && permalink.userId) { + return { + click: function(e) { + matrixLinkify.onUserClick(e, permalink.userId); + }, + }; + } + } catch (e) { + // OK fine, it's not actually a permalink } break; }