Do not prompt for a password when doing a „reset all“ after login (#10208)

pull/28217/head
Michael Weimann 2023-02-23 08:46:49 +01:00 committed by GitHub
parent 26652138bd
commit eb6278df1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 202 additions and 14 deletions

View File

@ -226,8 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private screenAfterLogin?: IScreen;
private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: number;
private focusComposer: boolean;
private subTitleStatus: string;
private prevWindowWidth: number;
@ -296,9 +294,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.loadSession();
}
this.accountPassword = null;
this.accountPasswordTimer = null;
this.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher();
@ -439,7 +434,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
window.removeEventListener("resize", this.onWindowResized);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
this.stores.accountPasswordStore.clearPassword();
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
}
@ -1987,13 +1982,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* this, as they instead jump straight into the app after `attemptTokenLogin`.
*/
private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => {
this.accountPassword = password;
// self-destruct the password after 5mins
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
this.accountPasswordTimer = window.setTimeout(() => {
this.accountPassword = null;
this.accountPasswordTimer = null;
}, 60 * 5 * 1000);
this.stores.accountPasswordStore.setPassword(password);
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
@ -2037,7 +2026,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view = (
<E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.accountPassword}
accountPassword={this.stores.accountPasswordStore.getPassword()}
tokenLogin={!!this.tokenLogin}
/>
);

View File

@ -21,6 +21,7 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import LegacyCallHandler from "../LegacyCallHandler";
import { PosthogAnalytics } from "../PosthogAnalytics";
import { SlidingSyncManager } from "../SlidingSyncManager";
import { AccountPasswordStore } from "../stores/AccountPasswordStore";
import { MemberListStore } from "../stores/MemberListStore";
import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore";
import RightPanelStore from "../stores/right-panel/RightPanelStore";
@ -73,6 +74,7 @@ export class SdkContextClass {
protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore;
protected _AccountPasswordStore?: AccountPasswordStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
@ -176,4 +178,11 @@ export class SdkContextClass {
}
return this._VoiceBroadcastPlaybacksStore;
}
public get accountPasswordStore(): AccountPasswordStore {
if (!this._AccountPasswordStore) {
this._AccountPasswordStore = new AccountPasswordStore();
}
return this._AccountPasswordStore;
}
}

View File

@ -0,0 +1,43 @@
/*
Copyright 2023 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.
*/
const PASSWORD_TIMEOUT = 5 * 60 * 1000; // five minutes
/**
* Store for the account password.
* This password can be used for a short time after login
* to avoid requestin the password all the time for instance during e2ee setup.
*/
export class AccountPasswordStore {
private password?: string;
private passwordTimeoutId?: ReturnType<typeof setTimeout>;
public setPassword(password: string): void {
this.password = password;
clearTimeout(this.passwordTimeoutId);
this.passwordTimeoutId = setTimeout(this.clearPassword, PASSWORD_TIMEOUT);
}
public getPassword(): string | undefined {
return this.password;
}
public clearPassword = (): void => {
clearTimeout(this.passwordTimeoutId);
this.passwordTimeoutId = undefined;
this.password = undefined;
};
}

View File

@ -30,6 +30,7 @@ import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
import Modal from "../Modal";
import InteractiveAuthDialog from "../components/views/dialogs/InteractiveAuthDialog";
import { _t } from "../languageHandler";
import { SdkContextClass } from "../contexts/SDKContext";
export enum Phase {
Loading = 0,
@ -224,6 +225,21 @@ export class SetupEncryptionStore extends EventEmitter {
const cli = MatrixClientPeg.get();
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
const cachedPassword = SdkContextClass.instance.accountPasswordStore.getPassword();
if (cachedPassword) {
await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: cli.getUserId(),
},
user: cli.getUserId(),
password: cachedPassword,
});
return;
}
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
title: _t("Setting up keys"),
matrixClient: cli,

View File

@ -0,0 +1,61 @@
/*
Copyright 2023 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 { AccountPasswordStore } from "../../src/stores/AccountPasswordStore";
jest.useFakeTimers();
describe("AccountPasswordStore", () => {
let accountPasswordStore: AccountPasswordStore;
beforeEach(() => {
accountPasswordStore = new AccountPasswordStore();
});
it("should not have a password by default", () => {
expect(accountPasswordStore.getPassword()).toBeUndefined();
});
describe("when setting a password", () => {
beforeEach(() => {
accountPasswordStore.setPassword("pass1");
});
it("should return the password", () => {
expect(accountPasswordStore.getPassword()).toBe("pass1");
});
describe("and the password timeout exceed", () => {
beforeEach(() => {
jest.advanceTimersToNextTimer();
});
it("should clear the password", () => {
expect(accountPasswordStore.getPassword()).toBeUndefined();
});
});
describe("and setting another password", () => {
beforeEach(() => {
accountPasswordStore.setPassword("pass2");
});
it("should return the other password", () => {
expect(accountPasswordStore.getPassword()).toBe("pass2");
});
});
});
});

View File

@ -0,0 +1,68 @@
/*
Copyright 2023 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 { mocked, Mocked } from "jest-mock";
import { IBootstrapCrossSigningOpts } from "matrix-js-sdk/src/crypto";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { SdkContextClass } from "../../src/contexts/SDKContext";
import { accessSecretStorage } from "../../src/SecurityManager";
import { SetupEncryptionStore } from "../../src/stores/SetupEncryptionStore";
import { stubClient } from "../test-utils";
jest.mock("../../src/SecurityManager", () => ({
accessSecretStorage: jest.fn(),
}));
describe("SetupEncryptionStore", () => {
const cachedPassword = "p4assword";
let client: Mocked<MatrixClient>;
let setupEncryptionStore: SetupEncryptionStore;
beforeEach(() => {
client = mocked(stubClient());
setupEncryptionStore = new SetupEncryptionStore();
SdkContextClass.instance.accountPasswordStore.setPassword(cachedPassword);
});
afterEach(() => {
SdkContextClass.instance.accountPasswordStore.clearPassword();
});
it("resetConfirm should work with a cached account password", async () => {
const makeRequest = jest.fn();
client.hasSecretStorageKey.mockResolvedValue(true);
client.bootstrapCrossSigning.mockImplementation(async (opts: IBootstrapCrossSigningOpts) => {
await opts?.authUploadDeviceSigningKeys(makeRequest);
});
mocked(accessSecretStorage).mockImplementation(async (func: () => Promise<void>) => {
await func();
});
await setupEncryptionStore.resetConfirm();
expect(mocked(accessSecretStorage)).toHaveBeenCalledWith(expect.any(Function), true);
expect(makeRequest).toHaveBeenCalledWith({
identifier: {
type: "m.id.user",
user: "@userId:matrix.org",
},
password: cachedPassword,
type: "m.login.password",
user: "@userId:matrix.org",
});
});
});

View File

@ -102,6 +102,8 @@ export function createTestClient(): MatrixClient {
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
credentials: { userId: "@userId:matrix.org" },
bootstrapCrossSigning: jest.fn(),
hasSecretStorageKey: jest.fn(),
store: {
getPendingEvents: jest.fn().mockResolvedValue([]),