Compare commits
10 commits
develop
...
dbkr/key_b
Author | SHA1 | Date | |
---|---|---|---|
|
0a43803d46 | ||
|
2a5af99ac2 | ||
|
b5ae775d8f | ||
|
6bff653339 | ||
|
b8f6bd1664 | ||
|
cf52974e09 | ||
|
6a912e28be | ||
|
ce98a0f988 | ||
|
7f10cd6a9e | ||
|
2c7247713a |
10 changed files with 89 additions and 37 deletions
|
@ -23,11 +23,11 @@ import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDia
|
||||||
async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> {
|
async function canUploadKeysWithPasswordOnly(cli: MatrixClient): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
|
await cli.uploadDeviceSigningKeys(undefined, {} as CrossSigningKeys);
|
||||||
// We should never get here: the server should always require
|
// If we get here, it's because the server is allowing us to upload keys without
|
||||||
// UI auth to upload device signing keys. If we do, we upload
|
// auth the first time due to MSC3967. Therefore, yes, we can upload keys
|
||||||
// no keys which would be a no-op.
|
// (with or without password, technically, but that's fine).
|
||||||
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
logger.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
|
||||||
return false;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
if (!(error instanceof MatrixError) || !error.data || !error.data.flows) {
|
||||||
logger.log("uploadDeviceSigningKeys advertised no flows!");
|
logger.log("uploadDeviceSigningKeys advertised no flows!");
|
||||||
|
|
|
@ -295,13 +295,20 @@ export default class DeviceListener {
|
||||||
await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
|
await crypto.getUserDeviceInfo([cli.getSafeUserId()]);
|
||||||
|
|
||||||
// cross signing isn't enabled - nag to enable it
|
// cross signing isn't enabled - nag to enable it
|
||||||
// There are 2 different toasts for:
|
// There are 3 different toasts for:
|
||||||
if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) {
|
if (!(await crypto.getCrossSigningKeyId()) && (await crypto.userHasCrossSigningKeys())) {
|
||||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
// Toast 1. Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||||
this.checkKeyBackupStatus();
|
this.checkKeyBackupStatus();
|
||||||
} else {
|
} else {
|
||||||
// No cross-signing or key backup on account (set up encryption)
|
const backupInfo = await this.getKeyBackupInfo();
|
||||||
|
if (backupInfo) {
|
||||||
|
// Toast 2: Key backup is enabled but recovery (4S) is not set up: prompt user to set up recovery.
|
||||||
|
// Since we now enable key backup at registration time, this will be the common case for
|
||||||
|
// new users.
|
||||||
|
showSetupEncryptionToast(SetupKind.SET_UP_RECOVERY);
|
||||||
|
} else {
|
||||||
|
// Toast 3: No cross-signing or key backup on account (set up encryption)
|
||||||
await cli.waitForClientWellKnown();
|
await cli.waitForClientWellKnown();
|
||||||
if (isSecureBackupRequired(cli) && isLoggedIn()) {
|
if (isSecureBackupRequired(cli) && isLoggedIn()) {
|
||||||
// If we're meant to set up, and Secure Backup is required,
|
// If we're meant to set up, and Secure Backup is required,
|
||||||
|
@ -313,6 +320,7 @@ export default class DeviceListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This needs to be done after awaiting on getUserDeviceInfo() above, so
|
// This needs to be done after awaiting on getUserDeviceInfo() above, so
|
||||||
// we make sure we get the devices after the fetch is done.
|
// we make sure we get the devices after the fetch is done.
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
|
import CompleteSecurityBody from "../../views/auth/CompleteSecurityBody";
|
||||||
import CreateCrossSigningDialog from "../../views/dialogs/security/CreateCrossSigningDialog";
|
import InitialCryptoSetupDialog from "../../views/dialogs/security/InitialCryptoSetupDialog";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -25,7 +25,7 @@ export default class E2eSetup extends React.Component<IProps> {
|
||||||
return (
|
return (
|
||||||
<AuthPage>
|
<AuthPage>
|
||||||
<CompleteSecurityBody>
|
<CompleteSecurityBody>
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={this.props.matrixClient}
|
matrixClient={this.props.matrixClient}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
accountPassword={this.props.accountPassword}
|
accountPassword={this.props.accountPassword}
|
||||||
|
|
|
@ -25,14 +25,14 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating a cross-signing keys. In most
|
* Walks the user through the process of creating a cross-signing keys and setting
|
||||||
* cases, only a spinner is shown, but for more complex auth like SSO, the user
|
* up message key backup. In most cases, only a spinner is shown, but for more
|
||||||
* may need to complete some steps to proceed.
|
* complex auth like SSO, the user may need to complete some steps to proceed.
|
||||||
*/
|
*/
|
||||||
const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => {
|
const InitialCryptoSetupDialog: React.FC<Props> = ({ matrixClient, accountPassword, tokenLogin, onFinished }) => {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const bootstrapCrossSigning = useCallback(async () => {
|
const doSetup = useCallback(async () => {
|
||||||
const cryptoApi = matrixClient.getCrypto();
|
const cryptoApi = matrixClient.getCrypto();
|
||||||
if (!cryptoApi) return;
|
if (!cryptoApi) return;
|
||||||
|
|
||||||
|
@ -40,6 +40,12 @@ const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPasswo
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createCrossSigning(matrixClient, tokenLogin, accountPassword);
|
await createCrossSigning(matrixClient, tokenLogin, accountPassword);
|
||||||
|
|
||||||
|
const backupInfo = await matrixClient.getKeyBackupVersion();
|
||||||
|
if (backupInfo === null) {
|
||||||
|
await cryptoApi.resetKeyBackup();
|
||||||
|
}
|
||||||
|
|
||||||
onFinished(true);
|
onFinished(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (tokenLogin) {
|
if (tokenLogin) {
|
||||||
|
@ -58,8 +64,8 @@ const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPasswo
|
||||||
}, [onFinished]);
|
}, [onFinished]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bootstrapCrossSigning();
|
doSetup();
|
||||||
}, [bootstrapCrossSigning]);
|
}, [doSetup]);
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -69,7 +75,7 @@ const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPasswo
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("action|retry")}
|
primaryButton={_t("action|retry")}
|
||||||
onPrimaryButtonClick={bootstrapCrossSigning}
|
onPrimaryButtonClick={doSetup}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,4 +102,4 @@ const CreateCrossSigningDialog: React.FC<Props> = ({ matrixClient, accountPasswo
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateCrossSigningDialog;
|
export default InitialCryptoSetupDialog;
|
|
@ -914,6 +914,9 @@
|
||||||
"warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
"warning": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings."
|
||||||
},
|
},
|
||||||
"reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>",
|
"reset_all_button": "Forgotten or lost all recovery methods? <a>Reset all</a>",
|
||||||
|
"set_up_recovery": "Set up recovery",
|
||||||
|
"set_up_recovery_later": "Not now",
|
||||||
|
"set_up_recovery_toast_title": "Set up recovery to protect your account",
|
||||||
"set_up_toast_description": "Safeguard against losing access to encrypted messages & data",
|
"set_up_toast_description": "Safeguard against losing access to encrypted messages & data",
|
||||||
"set_up_toast_title": "Set up Secure Backup",
|
"set_up_toast_title": "Set up Secure Backup",
|
||||||
"setup_secure_backup": {
|
"setup_secure_backup": {
|
||||||
|
|
|
@ -6,6 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import KeyboardIcon from "@vector-im/compound-design-tokens/assets/web/icons/key";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import { _t } from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import DeviceListener from "../DeviceListener";
|
import DeviceListener from "../DeviceListener";
|
||||||
|
@ -23,33 +26,61 @@ const getTitle = (kind: Kind): string => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case Kind.SET_UP_ENCRYPTION:
|
case Kind.SET_UP_ENCRYPTION:
|
||||||
return _t("encryption|set_up_toast_title");
|
return _t("encryption|set_up_toast_title");
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return _t("encryption|set_up_recovery_toast_title");
|
||||||
case Kind.VERIFY_THIS_SESSION:
|
case Kind.VERIFY_THIS_SESSION:
|
||||||
return _t("encryption|verify_toast_title");
|
return _t("encryption|verify_toast_title");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIcon = (kind: Kind): string => {
|
const getIcon = (kind: Kind): string | undefined => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case Kind.SET_UP_ENCRYPTION:
|
case Kind.SET_UP_ENCRYPTION:
|
||||||
return "secure_backup";
|
return "secure_backup";
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return undefined;
|
||||||
case Kind.VERIFY_THIS_SESSION:
|
case Kind.VERIFY_THIS_SESSION:
|
||||||
return "verification_warning";
|
return "verification_warning";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Gets the icon displayed on the prinary button
|
||||||
|
const getPrimaryIcon = (kind: Kind): ComponentType<React.SVGAttributes<SVGElement>> | undefined => {
|
||||||
|
switch (kind) {
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return KeyboardIcon;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getSetupCaption = (kind: Kind): string => {
|
const getSetupCaption = (kind: Kind): string => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case Kind.SET_UP_ENCRYPTION:
|
case Kind.SET_UP_ENCRYPTION:
|
||||||
return _t("action|continue");
|
return _t("action|continue");
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return _t("encryption|set_up_recovery");
|
||||||
case Kind.VERIFY_THIS_SESSION:
|
case Kind.VERIFY_THIS_SESSION:
|
||||||
return _t("action|verify");
|
return _t("action|verify");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecondaryButtonLabel = (kind: Kind): string => {
|
||||||
|
switch (kind) {
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return _t("encryption|set_up_recovery_later");
|
||||||
|
case Kind.SET_UP_ENCRYPTION:
|
||||||
|
case Kind.VERIFY_THIS_SESSION:
|
||||||
|
return _t("encryption|verification|unverified_sessions_toast_reject");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getDescription = (kind: Kind): string => {
|
const getDescription = (kind: Kind): string => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case Kind.SET_UP_ENCRYPTION:
|
case Kind.SET_UP_ENCRYPTION:
|
||||||
return _t("encryption|set_up_toast_description");
|
return _t("encryption|set_up_toast_description");
|
||||||
|
case Kind.SET_UP_RECOVERY:
|
||||||
|
return _t("encryption|set_up_recovery_toast_title");
|
||||||
case Kind.VERIFY_THIS_SESSION:
|
case Kind.VERIFY_THIS_SESSION:
|
||||||
return _t("encryption|verify_toast_description");
|
return _t("encryption|verify_toast_description");
|
||||||
}
|
}
|
||||||
|
@ -57,6 +88,7 @@ const getDescription = (kind: Kind): string => {
|
||||||
|
|
||||||
export enum Kind {
|
export enum Kind {
|
||||||
SET_UP_ENCRYPTION = "set_up_encryption",
|
SET_UP_ENCRYPTION = "set_up_encryption",
|
||||||
|
SET_UP_RECOVERY = "set_up_recovery",
|
||||||
VERIFY_THIS_SESSION = "verify_this_session",
|
VERIFY_THIS_SESSION = "verify_this_session",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,9 +133,9 @@ export const showToast = (kind: Kind): void => {
|
||||||
description: getDescription(kind),
|
description: getDescription(kind),
|
||||||
primaryLabel: getSetupCaption(kind),
|
primaryLabel: getSetupCaption(kind),
|
||||||
onPrimaryClick: onAccept,
|
onPrimaryClick: onAccept,
|
||||||
secondaryLabel: _t("encryption|verification|unverified_sessions_toast_reject"),
|
secondaryLabel: getSecondaryButtonLabel(kind),
|
||||||
onSecondaryClick: onReject,
|
onSecondaryClick: onReject,
|
||||||
destructive: "secondary",
|
PrimaryIcon: getPrimaryIcon(kind),
|
||||||
},
|
},
|
||||||
component: GenericToast,
|
component: GenericToast,
|
||||||
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
|
priority: kind === Kind.VERIFY_THIS_SESSION ? 95 : 40,
|
||||||
|
|
|
@ -12,14 +12,14 @@ import { mocked } from "jest-mock";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { createCrossSigning } from "../../../../../src/CreateCrossSigning";
|
import { createCrossSigning } from "../../../../../src/CreateCrossSigning";
|
||||||
import CreateCrossSigningDialog from "../../../../../src/components/views/dialogs/security/CreateCrossSigningDialog";
|
import InitialCryptoSetupDialog from "../../../../../src/components/views/dialogs/security/InitialCryptoSetupDialog";
|
||||||
import { createTestClient } from "../../../../test-utils";
|
import { createTestClient } from "../../../../test-utils";
|
||||||
|
|
||||||
jest.mock("../../../../../src/CreateCrossSigning", () => ({
|
jest.mock("../../../../../src/CreateCrossSigning", () => ({
|
||||||
createCrossSigning: jest.fn(),
|
createCrossSigning: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("CreateCrossSigningDialog", () => {
|
describe("InitialCryptoSetupDialog", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let createCrossSigningResolve: () => void;
|
let createCrossSigningResolve: () => void;
|
||||||
let createCrossSigningReject: (e: Error) => void;
|
let createCrossSigningReject: (e: Error) => void;
|
||||||
|
@ -43,7 +43,7 @@ describe("CreateCrossSigningDialog", () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={client}
|
matrixClient={client}
|
||||||
accountPassword="hunter2"
|
accountPassword="hunter2"
|
||||||
tokenLogin={false}
|
tokenLogin={false}
|
||||||
|
@ -61,7 +61,7 @@ describe("CreateCrossSigningDialog", () => {
|
||||||
|
|
||||||
it("should display an error if createCrossSigning fails", async () => {
|
it("should display an error if createCrossSigning fails", async () => {
|
||||||
render(
|
render(
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={client}
|
matrixClient={client}
|
||||||
accountPassword="hunter2"
|
accountPassword="hunter2"
|
||||||
tokenLogin={false}
|
tokenLogin={false}
|
||||||
|
@ -78,7 +78,7 @@ describe("CreateCrossSigningDialog", () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={client}
|
matrixClient={client}
|
||||||
accountPassword="hunter2"
|
accountPassword="hunter2"
|
||||||
tokenLogin={true}
|
tokenLogin={true}
|
||||||
|
@ -95,7 +95,7 @@ describe("CreateCrossSigningDialog", () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={client}
|
matrixClient={client}
|
||||||
accountPassword="hunter2"
|
accountPassword="hunter2"
|
||||||
tokenLogin={false}
|
tokenLogin={false}
|
||||||
|
@ -113,7 +113,7 @@ describe("CreateCrossSigningDialog", () => {
|
||||||
|
|
||||||
it("should retry when the retry button is clicked", async () => {
|
it("should retry when the retry button is clicked", async () => {
|
||||||
render(
|
render(
|
||||||
<CreateCrossSigningDialog
|
<InitialCryptoSetupDialog
|
||||||
matrixClient={client}
|
matrixClient={client}
|
||||||
accountPassword="hunter2"
|
accountPassword="hunter2"
|
||||||
tokenLogin={false}
|
tokenLogin={false}
|
|
@ -281,6 +281,7 @@ export function createTestClient(): MatrixClient {
|
||||||
getLocalAliases: jest.fn().mockReturnValue([]),
|
getLocalAliases: jest.fn().mockReturnValue([]),
|
||||||
uploadDeviceSigningKeys: jest.fn(),
|
uploadDeviceSigningKeys: jest.fn(),
|
||||||
isKeyBackupKeyStored: jest.fn().mockResolvedValue(null),
|
isKeyBackupKeyStored: jest.fn().mockResolvedValue(null),
|
||||||
|
getKeyBackupVersion: jest.fn(),
|
||||||
} as unknown as MatrixClient;
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
client.reEmitter = new ReEmitter(client);
|
client.reEmitter = new ReEmitter(client);
|
||||||
|
|
|
@ -352,13 +352,13 @@ describe("DeviceListener", () => {
|
||||||
mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc");
|
mockCrypto!.getCrossSigningKeyId.mockResolvedValue("abc");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows set up encryption toast when user has a key backup available", async () => {
|
it("shows set up recovery toast when user has a key backup available", async () => {
|
||||||
// non falsy response
|
// non falsy response
|
||||||
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
mockCrypto.getKeyBackupInfo.mockResolvedValue({} as unknown as KeyBackupInfo);
|
||||||
await createAndStart();
|
await createAndStart();
|
||||||
|
|
||||||
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
|
||||||
SetupEncryptionToast.Kind.SET_UP_ENCRYPTION,
|
SetupEncryptionToast.Kind.SET_UP_RECOVERY,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -148,6 +148,7 @@ describe("<MatrixChat />", () => {
|
||||||
whoami: jest.fn(),
|
whoami: jest.fn(),
|
||||||
logout: jest.fn(),
|
logout: jest.fn(),
|
||||||
getDeviceId: jest.fn(),
|
getDeviceId: jest.fn(),
|
||||||
|
getKeyBackupVersion: jest.fn(),
|
||||||
});
|
});
|
||||||
let mockClient: Mocked<MatrixClient>;
|
let mockClient: Mocked<MatrixClient>;
|
||||||
const serverConfig = {
|
const serverConfig = {
|
||||||
|
@ -1003,6 +1004,7 @@ describe("<MatrixChat />", () => {
|
||||||
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
|
||||||
// This needs to not finish immediately because we need to test the screen appears
|
// This needs to not finish immediately because we need to test the screen appears
|
||||||
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
bootstrapCrossSigning: jest.fn().mockImplementation(() => bootstrapDeferred.promise),
|
||||||
|
resetKeyBackup: jest.fn(),
|
||||||
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
|
||||||
};
|
};
|
||||||
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
|
loginClient.getCrypto.mockReturnValue(mockCrypto as any);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue