Do not prompt for a password when doing a „reset all“ after login (#10208)
parent
26652138bd
commit
eb6278df1d
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]),
|
||||
|
|
Loading…
Reference in New Issue