Merge pull request #6811 from matrix-org/fayed/fix-verification-dialog

Make cross-signing dialog clearer and more context-aware
This commit is contained in:
Faye Duxovni 2021-10-05 09:06:33 -04:00 committed by GitHub
commit 418043488b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 278 additions and 88 deletions

View file

@ -39,6 +39,7 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
@import "./structures/auth/_SetupEncryptionBody.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";
@import "./views/audio_messages/_PlaybackContainer.scss";

View file

@ -33,6 +33,19 @@ limitations under the License.
margin: 0 auto;
}
.mx_CompleteSecurity_skip {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 18px;
height: 18px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
right: 24px;
}
.mx_CompleteSecurity_body {
font-size: $font-15px;
}

View file

@ -0,0 +1,24 @@
/*
Copyright 2021 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.
*/
.mx_SetupEncryptionBody_reset {
color: $light-fg-color;
margin-top: $font-14px;
a.mx_SetupEncryptionBody_reset_link:is(:link, :hover, :visited) {
color: $warning-color;
}
}

View file

@ -20,6 +20,7 @@ import * as sdk from '../../../index';
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from '../../views/elements/AccessibleButton';
interface IProps {
onFinished: () => void;
@ -27,6 +28,7 @@ interface IProps {
interface IState {
phase: Phase;
lostKeys: boolean;
}
@replaceableComponent("structures.auth.CompleteSecurity")
@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this.onStoreUpdate);
store.start();
this.state = { phase: store.phase };
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
}
private onStoreUpdate = (): void => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({ phase: store.phase });
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
};
private onSkipClick = (): void => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
};
public componentWillUnmount(): void {
@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
public render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const { phase } = this.state;
const { phase, lostKeys } = this.state;
let icon;
let title;
if (phase === Phase.Loading) {
return null;
} else if (phase === Phase.Intro) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
if (lostKeys) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Unable to verify this login");
} else {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
}
} else if (phase === Phase.Done) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified");
@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
} else if (phase === Phase.Busy) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login");
} else if (phase === Phase.ConfirmReset) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Really reset verification keys?");
} else if (phase === Phase.Finished) {
// SetupEncryptionBody will take care of calling onFinished, we don't need to do anything
} else {
throw new Error(`Unknown phase ${phase}`);
}
let skipButton;
if (phase === Phase.Intro || phase === Phase.ConfirmReset) {
skipButton = (
<AccessibleButton onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("Skip verification for now")} />
);
}
return (
<AuthPage>
<CompleteSecurityBody>
<h2 className="mx_CompleteSecurity_header">
{ icon }
{ title }
{ skipButton }
</h2>
<div className="mx_CompleteSecurity_body">
<SetupEncryptionBody onFinished={this.props.onFinished} />

View file

@ -46,6 +46,7 @@ interface IState {
phase: Phase;
verificationRequest: VerificationRequest;
backupInfo: IKeyBackupInfo;
lostKeys: boolean;
}
@replaceableComponent("structures.auth.SetupEncryptionBody")
@ -62,6 +63,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
lostKeys: store.lostKeys(),
};
}
@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
lostKeys: store.lostKeys(),
});
};
@ -105,11 +108,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
});
};
private onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
};
private onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
store.returnAfterSkip();
};
private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
ev.preventDefault();
const store = SetupEncryptionStore.sharedInstance();
store.reset();
};
private onResetConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.resetConfirm();
};
private onResetBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterReset();
};
private onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
public render() {
const {
phase,
lostKeys,
} = this.state;
if (this.state.verificationRequest) {
@ -143,43 +158,67 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
isRoomEncrypted={false}
/>;
} else if (phase === Phase.Intro) {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Use Security Key or Phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Use Security Key");
}
if (lostKeys) {
return (
<div>
<p>{ _t(
"It looks like you don't have a Security Key or any other devices you can " +
"verify against. This device will not be able to access old encrypted messages. " +
"In order to verify your identity on this device, you'll need to reset " +
"your verification keys.",
) }</p>
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
{ recoveryKeyPrompt }
</AccessibleButton>;
}
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Use another login") }
</AccessibleButton>;
}
return (
<div>
<p>{ _t(
"Verify your identity to access encrypted messages and prove your identity to others.",
) }</p>
<div className="mx_CompleteSecurity_actionRow">
{ verifyButton }
{ useRecoveryKeyButton }
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
{ _t("Skip") }
</AccessibleButton>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
{ _t("Proceed with reset") }
</AccessibleButton>
</div>
</div>
</div>
);
);
} else {
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Verify with Security Key");
}
let useRecoveryKeyButton;
if (recoveryKeyPrompt) {
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
{ recoveryKeyPrompt }
</AccessibleButton>;
}
let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{ _t("Verify with another login") }
</AccessibleButton>;
}
return (
<div>
<p>{ _t(
"Verify your identity to access encrypted messages and prove your identity to others.",
) }</p>
<div className="mx_CompleteSecurity_actionRow">
{ verifyButton }
{ useRecoveryKeyButton }
</div>
<div className="mx_SetupEncryptionBody_reset">
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
a: (sub) => <a
href=""
onClick={this.onResetClick}
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>,
}) }
</div>
</div>
);
}
} else if (phase === Phase.Done) {
let message;
if (this.state.backupInfo) {
@ -215,14 +254,13 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
) }</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
className="warning"
kind="secondary"
kind="danger_outline"
onClick={this.onSkipConfirmClick}
>
{ _t("Skip") }
{ _t("I'll verify later") }
</AccessibleButton>
<AccessibleButton
kind="danger"
kind="primary"
onClick={this.onSkipBackClick}
>
{ _t("Go Back") }
@ -230,6 +268,30 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
</div>
</div>
);
} else if (phase === Phase.ConfirmReset) {
return (
<div>
<p>{ _t(
"Resetting your verification keys cannot be undone. After resetting, " +
"you won't have access to old encrypted messages, and any friends who " +
"have previously verified you will see security warnings until you " +
"re-verify with them.",
) }</p>
<p>{ _t(
"Please only proceed if you're sure you've lost all of your other " +
"devices and your security key.",
) }</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
{ _t("Proceed with reset") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
{ _t("Go Back") }
</AccessibleButton>
</div>
</div>
);
} else if (phase === Phase.Busy || phase === Phase.Loading) {
return <Spinner />;
} else {

View file

@ -49,16 +49,18 @@ const EncryptionInfo: React.FC<IProps> = ({
isSelfVerification,
}: IProps) => {
let content: JSX.Element;
if (waitingForOtherParty || waitingForNetwork) {
if (waitingForOtherParty && isSelfVerification) {
content = (
<div>
{ _t("To proceed, please accept the verification request on your other login.") }
</div>
);
} else if (waitingForOtherParty || waitingForNetwork) {
let text: string;
if (waitingForOtherParty) {
if (isSelfVerification) {
text = _t("Accept on your other login…");
} else {
text = _t("Waiting for %(displayName)s to accept…", {
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
});
}
text = _t("Waiting for %(displayName)s to accept…", {
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
});
} else {
text = _t("Accepting…");
}

View file

@ -121,24 +121,24 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
}
let confirm;
if (this.state.pending || this.state.cancelling) {
if (this.state.pending && this.props.isSelf) {
let text;
// device shouldn't be null in this situation but it can be, eg. if the device is
// logged out during verification
if (this.props.device) {
text = _t("Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…", {
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
deviceId: this.props.device ? this.props.device.deviceId : '',
});
} else {
text = _t("Waiting for you to verify on your other session…");
}
confirm = <p>{ text }</p>;
} else if (this.state.pending || this.state.cancelling) {
let text;
if (this.state.pending) {
if (this.props.isSelf) {
// device shouldn't be null in this situation but it can be, eg. if the device is
// logged out during verification
if (this.props.device) {
text = _t("Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…", {
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
deviceId: this.props.device ? this.props.device.deviceId : '',
});
} else {
text = _t("Waiting for your other session to verify…");
}
} else {
const { displayName } = this.props;
text = _t("Waiting for %(displayName)s to verify…", { displayName });
}
const { displayName } = this.props;
text = _t("Waiting for %(displayName)s to verify…", { displayName });
} else {
text = _t("Cancelling…");
}

View file

@ -947,8 +947,8 @@
"Verify this session by confirming the following number appears on its screen.": "Verify this session by confirming the following number appears on its screen.",
"Verify this user by confirming the following number appears on their screen.": "Verify this user by confirming the following number appears on their screen.",
"Unable to find a supported verification method.": "Unable to find a supported verification method.",
"Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…",
"Waiting for your other session to verify…": "Waiting for your other session to verify…",
"Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…": "Waiting for you to verify on your other session, %(deviceName)s (%(deviceId)s)…",
"Waiting for you to verify on your other session…": "Waiting for you to verify on your other session…",
"Waiting for %(displayName)s to verify…": "Waiting for %(displayName)s to verify…",
"Cancelling…": "Cancelling…",
"They match": "They match",
@ -1803,7 +1803,7 @@
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
"Back": "Back",
"Accept on your other login…": "Accept on your other login…",
"To proceed, please accept the verification request on your other login.": "To proceed, please accept the verification request on your other login.",
"Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…",
"Accepting…": "Accepting…",
"Start Verification": "Start Verification",
@ -2982,8 +2982,11 @@
"Could not load user profile": "Could not load user profile",
"Decrypted event source": "Decrypted event source",
"Original event source": "Original event source",
"Unable to verify this login": "Unable to verify this login",
"Verify this login": "Verify this login",
"Session verified": "Session verified",
"Really reset verification keys?": "Really reset verification keys?",
"Skip verification for now": "Skip verification for now",
"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.",
@ -3037,13 +3040,18 @@
"Create account": "Create account",
"Host account on": "Host account on",
"Decide where your account is hosted": "Decide where your account is hosted",
"Use Security Key or Phrase": "Use Security Key or Phrase",
"Use Security Key": "Use Security Key",
"Use another login": "Use another login",
"It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.": "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
"Proceed with reset": "Proceed with reset",
"Verify with Security Key or Phrase": "Verify with Security Key or Phrase",
"Verify with Security Key": "Verify with Security Key",
"Verify with another login": "Verify with another login",
"Verify your identity to access encrypted messages and prove your identity to others.": "Verify your identity to access encrypted messages and prove your identity to others.",
"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 verifying, you wont have access to all your messages and may appear as untrusted to others.": "Without verifying, you wont have access to all your messages and may appear as untrusted to others.",
"I'll verify later": "I'll verify later",
"Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.",
"Please only proceed if you're sure you've lost all of your other devices and your security key.": "Please only proceed if you're sure you've lost all of your other devices and your security key.",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Incorrect password": "Incorrect password",
"Failed to re-authenticate": "Failed to re-authenticate",

View file

@ -22,6 +22,9 @@ import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verific
import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
import Modal from '../Modal';
import InteractiveAuthDialog from '../components/views/dialogs/InteractiveAuthDialog';
import { _t } from '../languageHandler';
import { logger } from "matrix-js-sdk/src/logger";
@ -32,6 +35,7 @@ export enum Phase {
Done = 3, // final done stage, but still showing UX
ConfirmSkip = 4,
Finished = 5, // UX can be closed
ConfirmReset = 6,
}
export class SetupEncryptionStore extends EventEmitter {
@ -103,20 +107,23 @@ export class SetupEncryptionStore extends EventEmitter {
this.keyInfo = keys[this.keyId];
}
// do we have any other devices which are E2EE which we can verify against?
// do we have any other verified devices which are E2EE which we can verify against?
const dehydratedDevice = await cli.getDehydratedDevice();
this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
const ownUserId = cli.getUserId();
const crossSigningInfo = cli.getStoredCrossSigningForUser(ownUserId);
this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(ownUserId).some(
device =>
device.getIdentityKey() &&
(!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)),
(!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)) &&
crossSigningInfo.checkDeviceTrust(
crossSigningInfo,
device,
false,
true,
).isCrossSigningVerified(),
);
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
// skip before we can even render anything.
this.phase = Phase.Finished;
} else {
this.phase = Phase.Intro;
}
this.phase = Phase.Intro;
this.emit("update");
}
@ -208,6 +215,50 @@ export class SetupEncryptionStore extends EventEmitter {
this.emit("update");
}
public reset(): void {
this.phase = Phase.ConfirmReset;
this.emit("update");
}
public async resetConfirm(): Promise<void> {
try {
// If we've gotten here, the user presumably lost their
// secret storage key if they had one. Start by resetting
// secret storage and setting up a new recovery key, then
// create new cross-signing keys once that succeeds.
await accessSecretStorage(async () => {
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: cli,
makeRequest,
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
},
setupNewCrossSigning: true,
});
this.phase = Phase.Finished;
}, true);
} catch (e) {
console.error("Error resetting cross-signing", e);
this.phase = Phase.Intro;
}
this.emit("update");
}
public returnAfterReset(): void {
this.phase = Phase.Intro;
this.emit("update");
}
public done(): void {
this.phase = Phase.Finished;
this.emit("update");
@ -226,4 +277,8 @@ export class SetupEncryptionStore extends EventEmitter {
request.on("change", this.onVerificationRequestChange);
this.emit("update");
}
public lostKeys(): boolean {
return !this.hasDevicesToVerifyAgainst && !this.keyInfo;
}
}