Skip to content
This repository has been archived by the owner on Oct 16, 2024. It is now read-only.

Refactor CreateCrossSigningDialog #131

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/CreateCrossSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { logger } from "matrix-js-sdk/src/logger";
import { AuthDict, CrossSigningKeys, MatrixClient, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";

import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import Modal from "./Modal";
import { _t } from "./languageHandler";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";

async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> {
try {
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
return false;
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
logger.log("uploadDeviceSigningKeys advertised no flows!");
return false;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
return f.stages.length === 1 && f.stages[0] === "m.login.password";
});
return canUploadKeysWithPasswordOnly;
}
}

export async function createCrossSigning(
cli: MatrixClient,
isTokenLogin: boolean,
accountPassword?: string,
): Promise<void> {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) {
throw new Error("No crypto API found!");
}

const doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => {
if (accountPassword && (await canUploadKeysWithPasswordOnly(cli))) {
await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: cli.getUserId(),
},
password: accountPassword,
});
} else if (isTokenLogin) {
// We are hoping the grace period is active
await makeRequest({});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
body: _t("auth|uia|sso_preauth_body"),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("encryption|confirm_encryption_setup_title"),
body: _t("encryption|confirm_encryption_setup_body"),
continueText: _t("action|confirm"),
continueKind: "primary",
},
};

const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"),
matrixClient: cli,
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
};

await cryptoApi.bootstrapCrossSigning({
authUploadDeviceSigningKeys: doBootstrapUIAuth,
});
}
1 change: 1 addition & 0 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (this.state.view === Views.E2E_SETUP) {
view = (
<E2eSetup
matrixClient={MatrixClientPeg.safeGet()}
onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.stores.accountPasswordStore.getPassword()}
tokenLogin={!!this.tokenLogin}
Expand Down
5 changes: 4 additions & 1 deletion src/components/structures/auth/E2eSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";

import AuthPage from "../../views/auth/AuthPage";
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog";

interface IProps {
matrixClient: MatrixClient;
onFinished: () => void;
accountPassword?: string;
tokenLogin?: boolean;
tokenLogin: boolean;
}

export default class E2eSetup extends React.Component<IProps> {
Expand All @@ -24,6 +26,7 @@ export default class E2eSetup extends React.Component<IProps> {
<AuthPage>
<CompleteSecurityBody>
<CreateCrossSigningDialog
matrixClient={this.props.matrixClient}
onFinished={this.props.onFinished}
accountPassword={this.props.accountPassword}
tokenLogin={this.props.tokenLogin}
Expand Down
216 changes: 60 additions & 156 deletions src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,189 +7,93 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
import React, { useCallback, useEffect, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient } from "matrix-js-sdk/src/matrix";

import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
import DialogButtons from "../../elements/DialogButtons";
import BaseDialog from "../BaseDialog";
import Spinner from "../../elements/Spinner";
import InteractiveAuthDialog from "../InteractiveAuthDialog";
import { createCrossSigning } from "../../../../CreateCrossSigning";

interface IProps {
interface Props {
matrixClient: MatrixClient;
accountPassword?: string;
tokenLogin?: boolean;
tokenLogin: boolean;
onFinished: (success?: boolean) => void;
}

interface IState {
error: boolean;
canUploadKeysWithPasswordOnly: boolean | null;
accountPassword: string;
}

/*
* Walks the user through the process of creating a cross-signing keys. In most
* cases, only a spinner is shown, but for more complex auth like SSO, the user
* may need to complete some steps to proceed.
*/
export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
public constructor(props: IProps) {
super(props);
const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => {
const [error, setError] = useState(false);

this.state = {
error: false,
// Does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
accountPassword: props.accountPassword || "",
};
const bootstrapCrossSigning = useCallback(async () => {
const cryptoApi = matrixClient.getCrypto();
if (!cryptoApi) return;

if (!this.state.accountPassword) {
this.queryKeyUploadAuth();
}
}
setError(false);

public componentDidMount(): void {
this.bootstrapCrossSigning();
}

private async queryKeyUploadAuth(): Promise<void> {
try {
await MatrixClientPeg.safeGet().uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) {
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
logger.log("uploadDeviceSigningKeys advertised no flows!");
return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => {
return f.stages.length === 1 && f.stages[0] === "m.login.password";
});
this.setState({
canUploadKeysWithPasswordOnly,
});
}
}

private doBootstrapUIAuth = async (
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
): Promise<void> => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: MatrixClientPeg.safeGet().getUserId(),
},
password: this.state.accountPassword,
});
} else if (this.props.tokenLogin) {
// We are hoping the grace period is active
await makeRequest({});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("auth|uia|sso_title"),
body: _t("auth|uia|sso_preauth_body"),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("encryption|confirm_encryption_setup_title"),
body: _t("encryption|confirm_encryption_setup_body"),
continueText: _t("action|confirm"),
continueKind: "primary",
},
};

const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("encryption|bootstrap_title"),
matrixClient: MatrixClientPeg.safeGet(),
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
});
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
};

private bootstrapCrossSigning = async (): Promise<void> => {
this.setState({
error: false,
});

try {
const cli = MatrixClientPeg.safeGet();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
});
this.props.onFinished(true);
await createCrossSigning(matrixClient, tokenLogin, accountPassword);
onFinished(true);
} catch (e) {
if (this.props.tokenLogin) {
if (tokenLogin) {
// ignore any failures, we are relying on grace period here
this.props.onFinished(false);
onFinished(false);
return;
}

this.setState({ error: true });
setError(true);
logger.error("Error bootstrapping cross-signing", e);
}
};

private onCancel = (): void => {
this.props.onFinished(false);
};

public render(): React.ReactNode {
let content;
if (this.state.error) {
content = (
<div>
<p>{_t("encryption|unable_to_setup_keys_error")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={this.bootstrapCrossSigning}
onCancel={this.onCancel}
/>
</div>
}, [matrixClient, tokenLogin, accountPassword, onFinished]);

const onCancel = useCallback(() => {
onFinished(false);
}, [onFinished]);

useEffect(() => {
bootstrapCrossSigning();
}, [bootstrapCrossSigning]);

let content;
if (error) {
content = (
<div>
<p>{_t("encryption|unable_to_setup_keys_error")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={bootstrapCrossSigning}
onCancel={onCancel}
/>
</div>
);
} else {
content = (
<div>
<Spinner />
</div>
);
}

return (
<BaseDialog
className="mx_CreateCrossSigningDialog"
onFinished={this.props.onFinished}
title={_t("encryption|bootstrap_title")}
hasCancel={false}
fixedWidth={false}
>
<div>{content}</div>
</BaseDialog>
</div>
);
} else {
content = (
<div>
<Spinner />
</div>
);
}
}

return (
<BaseDialog
className="mx_CreateCrossSigningDialog"
onFinished={onFinished}
title={_t("encryption|bootstrap_title")}
hasCancel={false}
fixedWidth={false}
>
<div>{content}</div>
</BaseDialog>
);
};

export default CreateCrossSigningDialog;
Loading
Loading