Merge branch 'develop' into foldleft/reset-refactor

This commit is contained in:
Zoe 2020-03-27 10:50:05 +00:00
commit a02731f632
49 changed files with 1116 additions and 489 deletions

View 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>;
}

View file

@ -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);
});

View file

@ -28,7 +28,7 @@ export const PendingActionSpinner = ({text}) => {
</div>;
};
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification}) => {
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification, isRoomEncrypted}) => {
let content;
if (waitingForOtherParty || waitingForNetwork) {
let text;
@ -49,13 +49,27 @@ const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStar
);
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
let description;
if (isRoomEncrypted) {
description = (
<div>
<p>{_t("Messages in this room are end-to-end encrypted.")}</p>
<p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
</div>
);
} else {
description = (
<div>
<p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
<p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
</div>
);
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
{ description }
</div>
<div className="mx_UserInfo_container">
<h3>{_t("Verify User")}</h3>

View file

@ -30,7 +30,8 @@ import {_t} from "../../../languageHandler";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = ({verificationRequest, verificationRequestPromise, member, onClose, layout}) => {
const EncryptionPanel = (props) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
// before we have a request
@ -83,6 +84,22 @@ const EncryptionPanel = ({verificationRequest, verificationRequestPromise, membe
}, [onClose, request]);
useEventEmitter(request, "change", changeHandler);
const onCancel = useCallback(function() {
if (request) {
request.cancel();
}
}, [request]);
let cancelButton;
if (layout !== "dialog" && request && request.pending) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
cancelButton = (<AccessibleButton
className="mx_EncryptionPanel_cancel"
onClick={onCancel}
title={_t('Cancel')}
></AccessibleButton>);
}
const onStartVerification = useCallback(async () => {
setRequesting(true);
const cli = MatrixClientPeg.get();
@ -97,21 +114,27 @@ const EncryptionPanel = ({verificationRequest, verificationRequestPromise, membe
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
if (!request || requested) {
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
return <EncryptionInfo
onStartVerification={onStartVerification}
member={member}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />;
return (<React.Fragment>
{cancelButton}
<EncryptionInfo
isRoomEncrypted={isRoomEncrypted}
onStartVerification={onStartVerification}
member={member}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />
</React.Fragment>);
} else {
return (
return (<React.Fragment>
{cancelButton}
<VerificationPanel
isRoomEncrypted={isRoomEncrypted}
layout={layout}
onClose={onClose}
member={member}
request={request}
key={request.channel.transactionId}
phase={phase} />
);
</React.Fragment>);
}
};
EncryptionPanel.propTypes = {

View file

@ -1297,8 +1297,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const userVerified = userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
homeserverSupportsCrossSigning &&
isRoomEncrypted && !userVerified && !isMe;
homeserverSupportsCrossSigning && !userVerified && !isMe;
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
@ -1320,20 +1319,15 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
);
}
let devicesSection;
if (isRoomEncrypted) {
devicesSection = <DevicesSection
loading={devices === undefined}
devices={devices}
userId={member.userId} />;
}
const securitySection = (
<div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3>
<p>{ text }</p>
{ verifyButton }
{ devicesSection }
<DevicesSection
loading={devices === undefined}
devices={devices}
userId={member.userId} />
</div>
);
@ -1388,6 +1382,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
<div>
<div>
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member}
width={2 * 0.3 * window.innerHeight} // 2x@30vh
height={2 * 0.3 * window.innerHeight} // 2x@30vh
@ -1496,7 +1491,7 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
case RIGHT_PANEL_PHASES.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
<EncryptionPanel {...props} member={member} onClose={onClose} />
<EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} />
);
break;
}

View file

@ -48,6 +48,7 @@ export default class VerificationPanel extends React.PureComponent {
PHASE_DONE,
]).isRequired,
onClose: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
constructor(props) {
@ -174,15 +175,22 @@ export default class VerificationPanel extends React.PureComponent {
renderVerifiedPhase() {
const {member} = this.props;
let text;
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
} else {
text = _t("In encrypted rooms, verify all users to ensure its secure.");
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
<h3>Verified</h3>
<h3>{_t("Verified")}</h3>
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<p>Verify all users in a room to ensure it's secure.</p>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{_t("Got it")}
@ -209,7 +217,7 @@ export default class VerificationPanel extends React.PureComponent {
return (
<div className="mx_UserInfo_container">
<h3>Verification cancelled</h3>
<h3>{_t("Verification cancelled")}</h3>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
@ -231,7 +239,7 @@ export default class VerificationPanel extends React.PureComponent {
if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <div className="mx_UserInfo_container">
<h3>Compare emoji</h3>
<h3>{_t("Compare emoji")}</h3>
<VerificationShowSas
displayName={displayName}
sas={this.state.sasEvent.sas}

View file

@ -447,6 +447,8 @@ export default class BasicMessageEditor extends React.Component {
} else if (event.key === Key.TAB) {
this._tabCompleteName();
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this._formatBarRef.hide();
}
}
if (handled) {

View file

@ -65,6 +65,7 @@ export default createReactClass({
});
if (isRoomEncrypted) {
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.updateE2EStatus();
} else {
// Listen for room to become encrypted
@ -88,6 +89,7 @@ export default createReactClass({
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
@ -110,6 +112,11 @@ export default createReactClass({
this.updateE2EStatus();
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (userId !== this.props.member.userId) return;
this.updateE2EStatus();
},
updateE2EStatus: async function() {
const cli = MatrixClientPeg.get();
const { userId } = this.props.member;

View file

@ -32,6 +32,9 @@ export default class CrossSigningPanel extends React.PureComponent {
error: null,
crossSigningPublicKeysOnDevice: false,
crossSigningPrivateKeysInStorage: false,
selfSigningPrivateKeyCached: false,
userSigningPrivateKeyCached: false,
sessionBackupKeyCached: false,
secretStorageKeyInAccount: false,
secretStorageKeyNeedsUpgrade: null,
};
@ -71,10 +74,14 @@ 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 sessionBackupKeyCached = !!(await cli._crypto.getSessionBackupPrivateKey());
const secretStorageKeyInAccount = await secretStorage.hasKey();
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
@ -84,6 +91,9 @@ export default class CrossSigningPanel extends React.PureComponent {
this.setState({
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -130,6 +140,9 @@ export default class CrossSigningPanel extends React.PureComponent {
error,
crossSigningPublicKeysOnDevice,
crossSigningPrivateKeysInStorage,
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -209,6 +222,18 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Cross-signing private keys:")}</td>
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
</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>
<td>{_t("Session backup key:")}</td>
<td>{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>

View file

@ -0,0 +1,39 @@
/*
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 * as sdk from '../../../index';
import {_t} from "../../../languageHandler";
import {SettingLevel} from "../../../settings/SettingsStore";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel = props => {
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
return <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
level={SettingLevel.DEVICE}
/>
<div className="mx_E2eAdvancedPanel_settingLongDescription">{_t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}</div>
</div>;
};
export default E2eAdvancedPanel;

View file

@ -281,6 +281,8 @@ export default class SecurityUserSettingsTab extends React.Component {
);
}
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
@ -311,6 +313,7 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
{this._renderIgnoredUsers()}
{this._renderManageInvites()}
<E2eAdvancedPanel />
</div>
);
}

View file

@ -16,23 +16,61 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
import SetupEncryptionDialog from "../dialogs/SetupEncryptionDialog";
import { accessSecretStorage } from '../../../CrossSigningManager';
export default class SetupEncryptionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
kind: PropTypes.oneOf(['set_up_encryption', 'verify_this_session', 'upgrade_encryption']).isRequired,
kind: PropTypes.oneOf([
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
'upgrade_ssss',
]).isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
async _waitForCompletion() {
if (this.props.kind === 'upgrade_ssss') {
return new Promise(resolve => {
const recheck = async () => {
const needsUpgrade = await MatrixClientPeg.get().secretStorageKeyNeedsUpgrade();
if (!needsUpgrade) {
MatrixClientPeg.get().removeListener('accountData', recheck);
resolve();
}
};
MatrixClientPeg.get().on('accountData', recheck);
recheck();
});
} else {
return;
}
}
_onSetupClick = async () => {
accessSecretStorage();
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 modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
try {
await accessSecretStorage();
await this._waitForCompletion();
} finally {
modal.close();
}
}
};
getDescription() {
@ -42,6 +80,8 @@ export default class SetupEncryptionToast extends React.PureComponent {
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
case 'upgrade_ssss':
return _t('Update your secure storage');
}
}
@ -49,6 +89,7 @@ export default class SetupEncryptionToast extends React.PureComponent {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
case 'upgrade_ssss':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');