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

Commit

Permalink
Do not prompt for a password when doing a „reset all“ after login (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 authored Feb 23, 2023
1 parent 2665213 commit eb6278d
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 14 deletions.
17 changes: 3 additions & 14 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
/>
);
Expand Down
9 changes: 9 additions & 0 deletions src/contexts/SDKContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -176,4 +178,11 @@ export class SdkContextClass {
}
return this._VoiceBroadcastPlaybacksStore;
}

public get accountPasswordStore(): AccountPasswordStore {
if (!this._AccountPasswordStore) {
this._AccountPasswordStore = new AccountPasswordStore();
}
return this._AccountPasswordStore;
}
}
43 changes: 43 additions & 0 deletions src/stores/AccountPasswordStore.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
16 changes: 16 additions & 0 deletions src/stores/SetupEncryptionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 61 additions & 0 deletions test/stores/AccountPasswordStore-test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
});
68 changes: 68 additions & 0 deletions test/stores/SetupEncryptionStore-test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
2 changes: 2 additions & 0 deletions test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]),
Expand Down

0 comments on commit eb6278d

Please sign in to comment.