Merge remote-tracking branch 'origin/develop' into dbkr/wait_for_upgrade_to_complete
This commit is contained in:
commit
4586971a82
19 changed files with 601 additions and 253 deletions
|
@ -433,7 +433,13 @@ async function _startCallApp(roomId, type) {
|
||||||
const confId = `JitsiConference_${generateHumanReadableId()}`;
|
const confId = `JitsiConference_${generateHumanReadableId()}`;
|
||||||
const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain'];
|
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 = {
|
const widgetData = {
|
||||||
conferenceId: confId,
|
conferenceId: confId,
|
||||||
|
|
|
@ -145,13 +145,34 @@ const onSecretRequested = async function({
|
||||||
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
|
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name.startsWith("m.cross_signing")) {
|
||||||
const callbacks = client.getCrossSigningCacheCallbacks();
|
const callbacks = client.getCrossSigningCacheCallbacks();
|
||||||
if (!callbacks.getCrossSigningKeyCache) return;
|
if (!callbacks.getCrossSigningKeyCache) return;
|
||||||
|
/* Explicit enumeration here is deliberate – never share the master key! */
|
||||||
if (name === "m.cross_signing.self_signing") {
|
if (name === "m.cross_signing.self_signing") {
|
||||||
const key = await callbacks.getCrossSigningKeyCache("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);
|
return key && encodeBase64(key);
|
||||||
} else if (name === "m.cross_signing.user_signing") {
|
} else if (name === "m.cross_signing.user_signing") {
|
||||||
const key = await callbacks.getCrossSigningKeyCache("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);
|
||||||
|
}
|
||||||
|
} else if (name === "m.megolm_backup.v1") {
|
||||||
|
const key = await client._crypto.getSessionBackupPrivateKey();
|
||||||
|
if (!key) {
|
||||||
|
console.log(
|
||||||
|
`session backup key requested by ${deviceId}, but not found in cache`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return key && encodeBase64(key);
|
return key && encodeBase64(key);
|
||||||
}
|
}
|
||||||
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
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 WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi {
|
||||||
const data = event.data.data;
|
const data = event.data.data;
|
||||||
const val = data.value;
|
const val = data.value;
|
||||||
|
|
||||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
|
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
||||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
||||||
}
|
}
|
||||||
} else if (action === 'get_openid') {
|
} else if (action === 'get_openid') {
|
||||||
// Handled by caller
|
// 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 {
|
} else {
|
||||||
console.warn('Widget postMessage event unhandled');
|
console.warn('Widget postMessage event unhandled');
|
||||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
||||||
import WidgetUtils from "./utils/WidgetUtils";
|
import WidgetUtils from "./utils/WidgetUtils";
|
||||||
|
import {KnownWidgetActions} from "./widgets/WidgetApi";
|
||||||
|
|
||||||
if (!global.mxFromWidgetMessaging) {
|
if (!global.mxFromWidgetMessaging) {
|
||||||
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||||
|
@ -75,6 +76,17 @@ 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({
|
||||||
|
api: OUTBOUND_API_NAME,
|
||||||
|
action: KnownWidgetActions.ClientReady,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a screenshot from a widget
|
* Request a screenshot from a widget
|
||||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||||
|
|
|
@ -18,13 +18,14 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import {
|
||||||
import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
|
SetupEncryptionStore,
|
||||||
|
PHASE_INTRO,
|
||||||
const PHASE_INTRO = 0;
|
PHASE_BUSY,
|
||||||
const PHASE_BUSY = 1;
|
PHASE_DONE,
|
||||||
const PHASE_DONE = 2;
|
PHASE_CONFIRM_SKIP,
|
||||||
const PHASE_CONFIRM_SKIP = 3;
|
} from '../../../stores/SetupEncryptionStore';
|
||||||
|
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||||
|
|
||||||
export default class CompleteSecurity extends React.Component {
|
export default class CompleteSecurity extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
this.state = {
|
store.on("update", this._onStoreUpdate);
|
||||||
phase: PHASE_INTRO,
|
store.start();
|
||||||
// this serves dual purpose as the object for the request logic and
|
this.state = {phase: store.phase};
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onStoreUpdate = () => {
|
||||||
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
|
this.setState({phase: store.phase});
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.state.verificationRequest) {
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
store.off("update", this._onStoreUpdate);
|
||||||
}
|
store.stop();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
const {phase} = this.state;
|
||||||
|
|
||||||
const {
|
|
||||||
phase,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
let title;
|
let title;
|
||||||
let body;
|
|
||||||
|
|
||||||
if (this.state.verificationRequest) {
|
|
||||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
|
||||||
body = <EncryptionPanel
|
|
||||||
layout="dialog"
|
|
||||||
verificationRequest={this.state.verificationRequest}
|
|
||||||
onClose={this.props.onFinished}
|
|
||||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
|
||||||
/>;
|
|
||||||
} else if (phase === PHASE_INTRO) {
|
|
||||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
|
||||||
|
|
||||||
|
if (phase === PHASE_INTRO) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||||
title = _t("Complete security");
|
title = _t("Complete security");
|
||||||
body = (
|
|
||||||
<div>
|
|
||||||
<p>{_t(
|
|
||||||
"Open an existing session & use it to verify this one, " +
|
|
||||||
"granting it access to encrypted messages.",
|
|
||||||
)}</p>
|
|
||||||
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
|
|
||||||
<p>{_t(
|
|
||||||
"If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
|
||||||
{}, {
|
|
||||||
button: sub => <AccessibleButton element="span"
|
|
||||||
className="mx_linkButton"
|
|
||||||
onClick={this._onUsePassphraseClick}
|
|
||||||
>
|
|
||||||
{sub}
|
|
||||||
</AccessibleButton>,
|
|
||||||
})}</p>
|
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
|
||||||
<AccessibleButton
|
|
||||||
kind="danger"
|
|
||||||
onClick={this.onSkipClick}
|
|
||||||
>
|
|
||||||
{_t("Skip")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (phase === PHASE_DONE) {
|
} else if (phase === PHASE_DONE) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
|
||||||
title = _t("Session verified");
|
title = _t("Session verified");
|
||||||
let message;
|
|
||||||
if (this.state.backupInfo) {
|
|
||||||
message = <p>{_t(
|
|
||||||
"Your new session is now verified. It has access to your " +
|
|
||||||
"encrypted messages, and other users will see it as trusted.",
|
|
||||||
)}</p>;
|
|
||||||
} else {
|
|
||||||
message = <p>{_t(
|
|
||||||
"Your new session is now verified. Other users will see it as trusted.",
|
|
||||||
)}</p>;
|
|
||||||
}
|
|
||||||
body = (
|
|
||||||
<div>
|
|
||||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
|
|
||||||
{message}
|
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
onClick={this.onDoneClick}
|
|
||||||
>
|
|
||||||
{_t("Done")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||||
title = _t("Are you sure?");
|
title = _t("Are you sure?");
|
||||||
body = (
|
|
||||||
<div>
|
|
||||||
<p>{_t(
|
|
||||||
"Without completing security on this session, it won’t have " +
|
|
||||||
"access to encrypted messages.",
|
|
||||||
)}</p>
|
|
||||||
<div className="mx_CompleteSecurity_actionRow">
|
|
||||||
<AccessibleButton
|
|
||||||
className="warning"
|
|
||||||
kind="secondary"
|
|
||||||
onClick={this.onSkipConfirmClick}
|
|
||||||
>
|
|
||||||
{_t("Skip")}
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton
|
|
||||||
kind="danger"
|
|
||||||
onClick={this.onSkipBackClick}
|
|
||||||
>
|
|
||||||
{_t("Go Back")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (phase === PHASE_BUSY) {
|
} else if (phase === PHASE_BUSY) {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||||
title = _t("Complete security");
|
title = _t("Complete security");
|
||||||
body = <Spinner />;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown phase ${phase}`);
|
throw new Error(`Unknown phase ${phase}`);
|
||||||
}
|
}
|
||||||
|
@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component {
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mx_CompleteSecurity_body">
|
<div className="mx_CompleteSecurity_body">
|
||||||
{body}
|
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||||
</div>
|
</div>
|
||||||
</CompleteSecurityBody>
|
</CompleteSecurityBody>
|
||||||
</AuthPage>
|
</AuthPage>
|
||||||
|
|
196
src/components/structures/auth/SetupEncryptionBody.js
Normal file
196
src/components/structures/auth/SetupEncryptionBody.js
Normal file
|
@ -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 indicating 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 <EncryptionPanel
|
||||||
|
layout="dialog"
|
||||||
|
verificationRequest={this.state.verificationRequest}
|
||||||
|
onClose={this.props.onFinished}
|
||||||
|
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||||
|
/>;
|
||||||
|
} else if (phase === PHASE_INTRO) {
|
||||||
|
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{_t(
|
||||||
|
"Open an existing session & use it to verify this one, " +
|
||||||
|
"granting it access to encrypted messages.",
|
||||||
|
)}</p>
|
||||||
|
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
||||||
|
{}, {
|
||||||
|
button: sub => <AccessibleButton element="span"
|
||||||
|
className="mx_linkButton"
|
||||||
|
onClick={this._onUsePassphraseClick}
|
||||||
|
>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>,
|
||||||
|
})}</p>
|
||||||
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
|
<AccessibleButton
|
||||||
|
kind="danger"
|
||||||
|
onClick={this.onSkipClick}
|
||||||
|
>
|
||||||
|
{_t("Skip")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (phase === PHASE_DONE) {
|
||||||
|
let message;
|
||||||
|
if (this.state.backupInfo) {
|
||||||
|
message = <p>{_t(
|
||||||
|
"Your new session is now verified. It has access to your " +
|
||||||
|
"encrypted messages, and other users will see it as trusted.",
|
||||||
|
)}</p>;
|
||||||
|
} else {
|
||||||
|
message = <p>{_t(
|
||||||
|
"Your new session is now verified. Other users will see it as trusted.",
|
||||||
|
)}</p>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
|
||||||
|
{message}
|
||||||
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary"
|
||||||
|
onClick={this.onDoneClick}
|
||||||
|
>
|
||||||
|
{_t("Done")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{_t(
|
||||||
|
"Without completing security on this session, it won’t have " +
|
||||||
|
"access to encrypted messages.",
|
||||||
|
)}</p>
|
||||||
|
<div className="mx_CompleteSecurity_actionRow">
|
||||||
|
<AccessibleButton
|
||||||
|
className="warning"
|
||||||
|
kind="secondary"
|
||||||
|
onClick={this.onSkipConfirmClick}
|
||||||
|
>
|
||||||
|
{_t("Skip")}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
kind="danger"
|
||||||
|
onClick={this.onSkipBackClick}
|
||||||
|
>
|
||||||
|
{_t("Go Back")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (phase === PHASE_BUSY) {
|
||||||
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
|
return <Spinner />;
|
||||||
|
} else {
|
||||||
|
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/components/views/dialogs/SetupEncryptionDialog.js
Normal file
29
src/components/views/dialogs/SetupEncryptionDialog.js
Normal file
|
@ -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 <BaseDialog
|
||||||
|
headerImage={require("../../../../res/img/e2e/warning.svg")}
|
||||||
|
onFinished={onFinished}
|
||||||
|
title={_t("Verify this session")}
|
||||||
|
>
|
||||||
|
<SetupEncryptionBody onFinished={onFinished} />
|
||||||
|
</BaseDialog>;
|
||||||
|
}
|
|
@ -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() {
|
async _loadBackupStatus() {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -213,6 +231,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
backupKeyStored,
|
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 the backup key is stored, we can proceed directly to restore.
|
||||||
if (backupKeyStored) {
|
if (backupKeyStored) {
|
||||||
return this._restoreWithSecretStorage();
|
return this._restoreWithSecretStorage();
|
||||||
|
|
|
@ -419,6 +419,12 @@ export default class AppTile extends React.Component {
|
||||||
if (this.props.onCapabilityRequest) {
|
if (this.props.onCapabilityRequest) {
|
||||||
this.props.onCapabilityRequest(requestedCapabilities);
|
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) => {
|
}).catch((err) => {
|
||||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||||
});
|
});
|
||||||
|
|
|
@ -149,14 +149,17 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
const position = selection.end || selection;
|
const position = selection.end || selection;
|
||||||
this._setLastCaretFromPosition(position);
|
this._setLastCaretFromPosition(position);
|
||||||
}
|
}
|
||||||
if (this.props.placeholder) {
|
|
||||||
const {isEmpty} = this.props.model;
|
const {isEmpty} = this.props.model;
|
||||||
|
if (this.props.placeholder) {
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
this._showPlaceholder();
|
this._showPlaceholder();
|
||||||
} else {
|
} else {
|
||||||
this._hidePlaceholder();
|
this._hidePlaceholder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isEmpty) {
|
||||||
|
this._formatBarRef.hide();
|
||||||
|
}
|
||||||
this.setState({autoComplete: this.props.model.autoComplete});
|
this.setState({autoComplete: this.props.model.autoComplete});
|
||||||
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
|
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
|
||||||
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
|
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
|
||||||
|
|
|
@ -32,6 +32,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
error: null,
|
error: null,
|
||||||
crossSigningPublicKeysOnDevice: false,
|
crossSigningPublicKeysOnDevice: false,
|
||||||
crossSigningPrivateKeysInStorage: false,
|
crossSigningPrivateKeysInStorage: false,
|
||||||
|
selfSigningPrivateKeyCached: false,
|
||||||
|
userSigningPrivateKeyCached: false,
|
||||||
secretStorageKeyInAccount: false,
|
secretStorageKeyInAccount: false,
|
||||||
secretStorageKeyNeedsUpgrade: null,
|
secretStorageKeyNeedsUpgrade: null,
|
||||||
};
|
};
|
||||||
|
@ -71,10 +73,13 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
|
|
||||||
async _getUpdatedStatus() {
|
async _getUpdatedStatus() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||||
const crossSigning = cli._crypto._crossSigningInfo;
|
const crossSigning = cli._crypto._crossSigningInfo;
|
||||||
const secretStorage = cli._crypto._secretStorage;
|
const secretStorage = cli._crypto._secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
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 secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||||
const homeserverSupportsCrossSigning =
|
const homeserverSupportsCrossSigning =
|
||||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||||
|
@ -84,6 +89,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
this.setState({
|
this.setState({
|
||||||
crossSigningPublicKeysOnDevice,
|
crossSigningPublicKeysOnDevice,
|
||||||
crossSigningPrivateKeysInStorage,
|
crossSigningPrivateKeysInStorage,
|
||||||
|
selfSigningPrivateKeyCached,
|
||||||
|
userSigningPrivateKeyCached,
|
||||||
secretStorageKeyInAccount,
|
secretStorageKeyInAccount,
|
||||||
homeserverSupportsCrossSigning,
|
homeserverSupportsCrossSigning,
|
||||||
crossSigningReady,
|
crossSigningReady,
|
||||||
|
@ -130,6 +137,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
error,
|
error,
|
||||||
crossSigningPublicKeysOnDevice,
|
crossSigningPublicKeysOnDevice,
|
||||||
crossSigningPrivateKeysInStorage,
|
crossSigningPrivateKeysInStorage,
|
||||||
|
selfSigningPrivateKeyCached,
|
||||||
|
userSigningPrivateKeyCached,
|
||||||
secretStorageKeyInAccount,
|
secretStorageKeyInAccount,
|
||||||
homeserverSupportsCrossSigning,
|
homeserverSupportsCrossSigning,
|
||||||
crossSigningReady,
|
crossSigningReady,
|
||||||
|
@ -209,6 +218,14 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
<td>{_t("Cross-signing private keys:")}</td>
|
<td>{_t("Cross-signing private keys:")}</td>
|
||||||
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
|
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{_t("Self signing private key:")}</td>
|
||||||
|
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{_t("User signing private key:")}</td>
|
||||||
|
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{_t("Secret storage public key:")}</td>
|
<td>{_t("Secret storage public key:")}</td>
|
||||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||||
|
|
|
@ -20,7 +20,9 @@ import Modal from '../../../Modal';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
import DeviceListener from '../../../DeviceListener';
|
import DeviceListener from '../../../DeviceListener';
|
||||||
|
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
|
||||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||||
|
|
||||||
export default class SetupEncryptionToast extends React.PureComponent {
|
export default class SetupEncryptionToast extends React.PureComponent {
|
||||||
|
@ -57,6 +59,10 @@ export default class SetupEncryptionToast extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSetupClick = async () => {
|
_onSetupClick = async () => {
|
||||||
|
if (this.props.kind === "verify_this_session") {
|
||||||
|
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
|
||||||
|
{}, null, /* priority = */ false, /* static = */ true);
|
||||||
|
} else {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||||
try {
|
try {
|
||||||
|
@ -65,6 +71,7 @@ export default class SetupEncryptionToast extends React.PureComponent {
|
||||||
} finally {
|
} finally {
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getDescription() {
|
getDescription() {
|
||||||
|
|
|
@ -582,6 +582,10 @@
|
||||||
"not found": "not found",
|
"not found": "not found",
|
||||||
"Cross-signing private keys:": "Cross-signing private keys:",
|
"Cross-signing private keys:": "Cross-signing private keys:",
|
||||||
"in secret storage": "in secret storage",
|
"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:",
|
"Secret storage public key:": "Secret storage public key:",
|
||||||
"in account data": "in account data",
|
"in account data": "in account data",
|
||||||
"Homeserver feature support:": "Homeserver feature support:",
|
"Homeserver feature support:": "Homeserver feature support:",
|
||||||
|
@ -2006,14 +2010,7 @@
|
||||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
"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",
|
"Could not load user profile": "Could not load user profile",
|
||||||
"Complete security": "Complete security",
|
"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, <button>use your recovery key or passphrase.</button>": "If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
|
||||||
"Session verified": "Session verified",
|
"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",
|
"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.",
|
"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.",
|
"A new password must be entered.": "A new password must be entered.",
|
||||||
|
@ -2063,6 +2060,13 @@
|
||||||
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
|
||||||
"Registration Successful": "Registration Successful",
|
"Registration Successful": "Registration Successful",
|
||||||
"Create your account": "Create your account",
|
"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, <button>use your recovery key or passphrase.</button>": "If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
||||||
|
"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 due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
|
||||||
"Failed to re-authenticate": "Failed to re-authenticate",
|
"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.",
|
"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.",
|
||||||
|
|
|
@ -200,6 +200,7 @@ matrixLinkify.options = {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "url": {
|
case "url": {
|
||||||
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
||||||
|
try {
|
||||||
const permalink = parsePermalink(href);
|
const permalink = parsePermalink(href);
|
||||||
if (permalink && permalink.userId) {
|
if (permalink && permalink.userId) {
|
||||||
return {
|
return {
|
||||||
|
@ -208,6 +209,9 @@ matrixLinkify.options = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// OK fine, it's not actually a permalink
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "userid":
|
case "userid":
|
||||||
|
|
|
@ -118,6 +118,10 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
|
||||||
try {
|
try {
|
||||||
body.append("storageManager_persisted", await navigator.storage.persisted());
|
body.append("storageManager_persisted", await navigator.storage.persisted());
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
} else if (document.hasStorageAccess) { // Safari
|
||||||
|
try {
|
||||||
|
body.append("storageManager_persisted", await document.hasStorageAccess());
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
if (navigator.storage && navigator.storage.estimate) {
|
if (navigator.storage && navigator.storage.estimate) {
|
||||||
try {
|
try {
|
||||||
|
|
144
src/stores/SetupEncryptionStore.js
Normal file
144
src/stores/SetupEncryptionStore.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,11 @@ export function tryPersistStorage() {
|
||||||
navigator.storage.persist().then(persistent => {
|
navigator.storage.persist().then(persistent => {
|
||||||
console.log("StorageManager: Persistent?", 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 {
|
} else {
|
||||||
console.log("StorageManager: Persistence unsupported");
|
console.log("StorageManager: Persistence unsupported");
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ const WIDGET_WAIT_TIME = 20000;
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||||
|
import {Capability} from "../widgets/WidgetApi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes a URI according to a set of template variables. Variables will be
|
* Encodes a URI according to a set of template variables. Variables will be
|
||||||
|
@ -422,6 +423,22 @@ export default class WidgetUtils {
|
||||||
app.eventId = eventId;
|
app.eventId = eventId;
|
||||||
app.name = app.name || app.type;
|
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) {
|
if (app.data) {
|
||||||
Object.keys(app.data).forEach((key) => {
|
Object.keys(app.data).forEach((key) => {
|
||||||
params['$' + key] = app.data[key];
|
params['$' + key] = app.data[key];
|
||||||
|
@ -430,11 +447,6 @@ export default class WidgetUtils {
|
||||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
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);
|
app.url = encodeUri(app.url, params);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
@ -443,12 +455,15 @@ export default class WidgetUtils {
|
||||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", 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,
|
// 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
|
// 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.
|
// 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;
|
return capWhitelist;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ export enum Capability {
|
||||||
Screenshot = "m.capability.screenshot",
|
Screenshot = "m.capability.screenshot",
|
||||||
Sticker = "m.sticker",
|
Sticker = "m.sticker",
|
||||||
AlwaysOnScreen = "m.always_on_screen",
|
AlwaysOnScreen = "m.always_on_screen",
|
||||||
|
GetRiotWebConfig = "im.vector.web.riot_config",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
export enum KnownWidgetActions {
|
||||||
|
@ -33,7 +34,10 @@ export enum KnownWidgetActions {
|
||||||
UpdateVisibility = "visibility",
|
UpdateVisibility = "visibility",
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
ReceiveOpenIDCredentials = "openid_credentials",
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
|
GetRiotWebConfig = "im.vector.web.riot_config",
|
||||||
|
ClientReady = "im.vector.ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetAction = KnownWidgetActions | string;
|
export type WidgetAction = KnownWidgetActions | string;
|
||||||
|
|
||||||
export enum WidgetApiType {
|
export enum WidgetApiType {
|
||||||
|
@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest {
|
||||||
*/
|
*/
|
||||||
export class WidgetApi {
|
export class WidgetApi {
|
||||||
private origin: string;
|
private origin: string;
|
||||||
private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {};
|
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
||||||
private readyPromise: Promise<any>;
|
private readyPromise: Promise<any>;
|
||||||
private readyPromiseResolve: () => void;
|
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[]) {
|
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
||||||
this.origin = new URL(currentUrl).origin;
|
this.origin = new URL(currentUrl).origin;
|
||||||
|
|
||||||
|
@ -83,7 +92,14 @@ export class WidgetApi {
|
||||||
|
|
||||||
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
||||||
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
||||||
|
if (!this.expectingExplicitReady) {
|
||||||
this.readyPromiseResolve();
|
this.readyPromiseResolve();
|
||||||
|
}
|
||||||
|
} else if (payload.action === KnownWidgetActions.ClientReady) {
|
||||||
|
this.readyPromiseResolve();
|
||||||
|
|
||||||
|
// Automatically acknowledge so we can move on
|
||||||
|
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +142,10 @@ export class WidgetApi {
|
||||||
data: payload,
|
data: payload,
|
||||||
response: {}, // Not used at this layer - it's used when the client responds
|
response: {}, // Not used at this layer - it's used when the client responds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
this.inFlightRequests[request.requestId] = callback;
|
this.inFlightRequests[request.requestId] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[WidgetAPI] Sending request: `, request);
|
console.log(`[WidgetAPI] Sending request: `, request);
|
||||||
window.parent.postMessage(request, "*");
|
window.parent.postMessage(request, "*");
|
||||||
|
@ -134,7 +153,16 @@ export class WidgetApi {
|
||||||
|
|
||||||
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
||||||
return new Promise<any>(resolve => {
|
return new Promise<any>(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<any> {
|
||||||
|
return new Promise<any>(resolve => {
|
||||||
|
this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => {
|
||||||
|
resolve(response.response.config);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue