From 26079972452fb7855745eabee39ed29e9637d111 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 11 Jul 2023 19:27:52 +1200 Subject: [PATCH 01/12] test persistCredentials without a pickle key --- test/Lifecycle-test.ts | 222 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 test/Lifecycle-test.ts diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts new file mode 100644 index 00000000000..b837d7e54d7 --- /dev/null +++ b/test/Lifecycle-test.ts @@ -0,0 +1,222 @@ +/* +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 } from "jest-mock"; +import { logger } from "matrix-js-sdk/src/logger"; +import * as MatrixJs from "matrix-js-sdk/src/matrix"; +import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvictedDialog"; +import { restoreFromLocalStorage } from "../src/Lifecycle"; +import { MatrixClientPeg } from "../src/MatrixClientPeg"; +import Modal from "../src/Modal"; +import PlatformPeg from "../src/PlatformPeg"; +import * as StorageManager from "../src/utils/StorageManager"; + +import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; + +describe("Lifecycle", () => { + const mockPlatform = mockPlatformPeg({ + getPickleKey: jest.fn(), + }); + + const realLocalStorage = global.localStorage; + + const mockClient = getMockClientWithEventEmitter({ + clearStores: jest.fn(), + getAccountData: jest.fn(), + getUserId: jest.fn(), + getDeviceId: jest.fn(), + isVersionSupported: jest.fn().mockResolvedValue(true), + getCrypto: jest.fn(), + getClientWellKnown: jest.fn(), + }); + + beforeEach(() => { + mockPlatform.getPickleKey.mockResolvedValue(null); + + // stub this + jest.spyOn(MatrixClientPeg, "replaceUsingCreds").mockImplementation(() => {}); + jest.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined); + + // reset any mocking + global.localStorage = realLocalStorage; + }); + + const initLocalStorageMock = (mockStore: Record = {}): void => { + jest.spyOn(localStorage.__proto__, "getItem") + .mockClear() + .mockImplementation((key: unknown) => mockStore[key as string] ?? null); + jest.spyOn(localStorage.__proto__, "removeItem") + .mockClear() + .mockImplementation((key: unknown) => mockStore[key as string] ?? null); + jest.spyOn(localStorage.__proto__, "setItem").mockClear(); + }; + + const initSessionStorageMock = (mockStore: Record = {}): void => { + jest.spyOn(sessionStorage.__proto__, "getItem") + .mockClear() + .mockImplementation((key: unknown) => mockStore[key as string] ?? null); + jest.spyOn(sessionStorage.__proto__, "removeItem") + .mockClear() + .mockImplementation((key: unknown) => mockStore[key as string] ?? null); + jest.spyOn(sessionStorage.__proto__, "setItem").mockClear(); + }; + + const initIdbMock = (mockStore: Record> = {}): void => { + jest.spyOn(StorageManager, "idbLoad") + .mockClear() + .mockImplementation( + // @ts-ignore mock type + async (table: string, key: string) => mockStore[table]?.[key] ?? null, + ); + jest.spyOn(StorageManager, "idbSave").mockClear().mockResolvedValue(undefined); + }; + + const homeserverUrl = "https://server.org"; + const identityServerUrl = "https://is.org"; + const userId = "@alice:server.org"; + const deviceId = "abc123"; + const accessToken = "test-access-token"; + const localStorageSession = { + mx_hs_url: homeserverUrl, + mx_is_url: identityServerUrl, + mx_user_id: userId, + mx_device_id: deviceId, + }; + const idbStorageSession = { + account: { + mx_access_token: accessToken, + }, + }; + + describe("restoreFromLocalStorage()", () => { + beforeEach(() => { + initLocalStorageMock(); + initSessionStorageMock(); + initIdbMock(); + + jest.clearAllMocks(); + jest.spyOn(logger, "log").mockClear(); + + jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + + // stub this out + jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: Promise.resolve([true]) }); + }); + + it("should return false when localStorage is not available", async () => { + // @ts-ignore dirty mocking + delete global.localStorage; + // @ts-ignore dirty mocking + global.localStorage = undefined; + + expect(await restoreFromLocalStorage()).toEqual(false); + }); + + it("should return false when no session data is found in local storage", async () => { + expect(await restoreFromLocalStorage()).toEqual(false); + expect(logger.log).toHaveBeenCalledWith("No previous session found."); + }); + + it("should abort login when we expect to find an access token but don't", async () => { + initLocalStorageMock({ mx_has_access_token: "true" }); + + await expect(() => restoreFromLocalStorage()).rejects.toThrow(); + expect(Modal.createDialog).toHaveBeenCalledWith(StorageEvictedDialog); + expect(mockClient.clearStores).toHaveBeenCalled(); + }); + + describe("when session is found in storage", () => { + beforeEach(() => { + initLocalStorageMock(localStorageSession); + initIdbMock(idbStorageSession); + }); + + describe("guest account", () => { + it("should ignore guest accounts when ignoreGuest is true", async () => { + initLocalStorageMock({ ...localStorageSession, mx_is_guest: "true" }); + + expect(await restoreFromLocalStorage({ ignoreGuest: true })).toEqual(false); + expect(logger.log).toHaveBeenCalledWith(`Ignoring stored guest account: ${userId}`); + }); + + it("should restore guest accounts when ignoreGuest is false", async () => { + initLocalStorageMock({ ...localStorageSession, mx_is_guest: "true" }); + + expect(await restoreFromLocalStorage({ ignoreGuest: false })).toEqual(true); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith( + expect.objectContaining({ + userId, + guest: true, + }), + ); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "true"); + }); + }); + + describe("without a pickle key", () => { + it("should persist credentials", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(localStorage.setItem).toHaveBeenCalledWith("mx_user_id", userId); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "false"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); + + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + // dont put accessToken in localstorage when we have idb + expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should persist access token when idb is not available", async () => { + jest.spyOn(StorageManager, "idbSave").mockRejectedValue("oups"); + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + // put accessToken in localstorage as fallback + expect(localStorage.setItem).toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should create new matrix client with credentials", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith({ + userId, + accessToken, + homeserverUrl, + identityServerUrl, + deviceId, + freshLogin: false, + guest: false, + pickleKey: undefined, + }); + }); + + it("should remove fresh login flag from session storage", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(sessionStorage.removeItem).toHaveBeenCalledWith("mx_fresh_login"); + }); + + it("should start matrix client", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(MatrixClientPeg.start).toHaveBeenCalled(); + }); + }); + }); + }); +}); From 609f790c469c3d514f8503377b678ca3363768f2 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 12 Jul 2023 17:58:57 +1200 Subject: [PATCH 02/12] test setLoggedIn with pickle key --- test/Lifecycle-test.ts | 230 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 2 deletions(-) diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts index b837d7e54d7..36ca78d04eb 100644 --- a/test/Lifecycle-test.ts +++ b/test/Lifecycle-test.ts @@ -17,14 +17,24 @@ limitations under the License. import { mocked } from "jest-mock"; import { logger } from "matrix-js-sdk/src/logger"; import * as MatrixJs from "matrix-js-sdk/src/matrix"; +import { setCrypto } from "matrix-js-sdk/src/crypto/crypto"; +import { Crypto } from "@peculiar/webcrypto"; +import * as MatrixCryptoAes from "matrix-js-sdk/src/crypto/aes"; + import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvictedDialog"; -import { restoreFromLocalStorage } from "../src/Lifecycle"; +import { restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; import Modal from "../src/Modal"; import PlatformPeg from "../src/PlatformPeg"; import * as StorageManager from "../src/utils/StorageManager"; +const webCrypto = new Crypto(); + +const windowCrypto = window.crypto; + import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; +import { subtleCrypto } from "matrix-js-sdk/src/crypto/crypto"; +import BasePlatform from "../src/BasePlatform"; describe("Lifecycle", () => { const mockPlatform = mockPlatformPeg({ @@ -34,6 +44,8 @@ describe("Lifecycle", () => { const realLocalStorage = global.localStorage; const mockClient = getMockClientWithEventEmitter({ + stopClient: jest.fn(), + removeAllListeners: jest.fn(), clearStores: jest.fn(), getAccountData: jest.fn(), getUserId: jest.fn(), @@ -41,6 +53,10 @@ describe("Lifecycle", () => { isVersionSupported: jest.fn().mockResolvedValue(true), getCrypto: jest.fn(), getClientWellKnown: jest.fn(), + getThirdpartyProtocols: jest.fn(), + store: { + destroy: jest.fn(), + }, }); beforeEach(() => { @@ -52,6 +68,21 @@ describe("Lifecycle", () => { // reset any mocking global.localStorage = realLocalStorage; + + setCrypto(webCrypto); + // @ts-ignore mocking + delete window.crypto; + window.crypto = webCrypto; + + jest.spyOn(MatrixCryptoAes, "encryptAES").mockRestore(); + }); + + afterAll(() => { + setCrypto(windowCrypto); + + // @ts-ignore unmocking + delete window.crypto; + window.crypto = windowCrypto; }); const initLocalStorageMock = (mockStore: Record = {}): void => { @@ -72,6 +103,7 @@ describe("Lifecycle", () => { .mockClear() .mockImplementation((key: unknown) => mockStore[key as string] ?? null); jest.spyOn(sessionStorage.__proto__, "setItem").mockClear(); + jest.spyOn(sessionStorage.__proto__, "clear").mockClear(); }; const initIdbMock = (mockStore: Record> = {}): void => { @@ -82,6 +114,7 @@ describe("Lifecycle", () => { async (table: string, key: string) => mockStore[table]?.[key] ?? null, ); jest.spyOn(StorageManager, "idbSave").mockClear().mockResolvedValue(undefined); + jest.spyOn(StorageManager, "idbDelete").mockClear().mockResolvedValue(undefined); }; const homeserverUrl = "https://server.org"; @@ -113,7 +146,10 @@ describe("Lifecycle", () => { jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); // stub this out - jest.spyOn(Modal, "createDialog").mockReturnValue({ finished: Promise.resolve([true]) }); + jest.spyOn(Modal, "createDialog").mockReturnValue( + // @ts-ignore allow bad mock + { finished: Promise.resolve([true]) }, + ); }); it("should return false when localStorage is not available", async () => { @@ -219,4 +255,194 @@ describe("Lifecycle", () => { }); }); }); + + describe("setLoggedIn()", () => { + beforeEach(() => { + initLocalStorageMock(); + initSessionStorageMock(); + initIdbMock(); + + jest.clearAllMocks(); + jest.spyOn(logger, "log").mockClear(); + + jest.spyOn(MatrixJs, "createClient").mockReturnValue(mockClient); + // remove any mock implementations + jest.spyOn(mockPlatform, "createPickleKey").mockRestore(); + // but still spy and call through + jest.spyOn(mockPlatform, "createPickleKey"); + }); + + const credentials = { + homeserverUrl, + identityServerUrl, + userId, + deviceId, + accessToken, + }; + + it("should remove fresh login flag from session storage", async () => { + await setLoggedIn(credentials); + + expect(sessionStorage.removeItem).toHaveBeenCalledWith("mx_fresh_login"); + }); + + it("should start matrix client", async () => { + await setLoggedIn(credentials); + + expect(MatrixClientPeg.start).toHaveBeenCalled(); + }); + + describe("without a pickle key", () => { + beforeEach(() => { + jest.spyOn(mockPlatform, "createPickleKey").mockResolvedValue(null); + }); + + it("should persist credentials", async () => { + await setLoggedIn(credentials); + + expect(localStorage.setItem).toHaveBeenCalledWith("mx_user_id", userId); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "false"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); + + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + // dont put accessToken in localstorage when we have idb + expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { + jest.spyOn(StorageManager, "idbSave").mockRejectedValue("oups"); + await setLoggedIn({ + ...credentials, + // @ts-ignore + accessToken: undefined, + }); + + expect(localStorage.removeItem).toHaveBeenCalledWith("mx_has_access_token"); + expect(localStorage.removeItem).toHaveBeenCalledWith("mx_access_token"); + }); + + it("should clear stores", async () => { + await setLoggedIn(credentials); + + expect(StorageManager.idbDelete).toHaveBeenCalledWith("account", "mx_access_token"); + expect(sessionStorage.clear).toHaveBeenCalled(); + expect(mockClient.clearStores).toHaveBeenCalled(); + }); + + it("should create new matrix client with credentials", async () => { + expect(await setLoggedIn(credentials)).toEqual(mockClient); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith({ + userId, + accessToken, + homeserverUrl, + identityServerUrl, + deviceId, + freshLogin: true, + guest: false, + pickleKey: null, + }); + }); + }); + + describe("with a pickle key", () => { + it("should not create a pickle key when credentials do not include deviceId", async () => { + await setLoggedIn({ + ...credentials, + deviceId: undefined, + }); + + // unpickled access token saved + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(mockPlatform.createPickleKey).not.toHaveBeenCalled(); + }); + + it("creates a pickle key with userId and deviceId", async () => { + await setLoggedIn(credentials); + + expect(mockPlatform.createPickleKey).toHaveBeenCalledWith(userId, deviceId); + }); + + it("should persist credentials", async () => { + await setLoggedIn(credentials); + + expect(localStorage.setItem).toHaveBeenCalledWith("mx_user_id", userId); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_is_guest", "false"); + expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); + + expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_pickle_key", "true"); + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", { + ciphertext: expect.any(String), + iv: expect.any(String), + mac: expect.any(String), + }); + expect(StorageManager.idbSave).toHaveBeenCalledWith( + "pickleKey", + [userId, deviceId], + expect.any(Object), + ); + // dont put accessToken in localstorage when we have idb + expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should persist token when encrypting the token fails", async () => { + jest.spyOn(MatrixCryptoAes, "encryptAES").mockRejectedValue("MOCK REJECT ENCRYPTAES"); + await setLoggedIn(credentials); + + // persist the unencrypted token + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + }); + + it("should persist token in localStorage when idb fails to save token", async () => { + // dont fail for pickle key persist + jest.spyOn(StorageManager, "idbSave").mockImplementation( + async (table: string, key: string | string[]) => { + if (table === "account" && key === "mx_access_token") { + throw new Error("oups"); + } + }, + ); + await setLoggedIn(credentials); + + // put plain accessToken in localstorage when we dont have idb + expect(localStorage.setItem).toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { + // dont fail for pickle key persist + jest.spyOn(StorageManager, "idbSave").mockImplementation( + async (table: string, key: string | string[]) => { + if (table === "account" && key === "mx_access_token") { + throw new Error("oups"); + } + }, + ); + await setLoggedIn({ + ...credentials, + // @ts-ignore + accessToken: undefined, + }); + + expect(localStorage.removeItem).toHaveBeenCalledWith("mx_has_access_token"); + expect(localStorage.removeItem).toHaveBeenCalledWith("mx_access_token"); + }); + + it("should create new matrix client with credentials", async () => { + expect(await setLoggedIn(credentials)).toEqual(mockClient); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith({ + userId, + accessToken, + homeserverUrl, + identityServerUrl, + deviceId, + freshLogin: true, + guest: false, + pickleKey: expect.any(String), + }); + }); + }); + }); }); From f3092c7b5cd42a912b88a49b0e84a174df64c425 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Wed, 12 Jul 2023 18:04:28 +1200 Subject: [PATCH 03/12] lint --- test/Lifecycle-test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts index 36ca78d04eb..1e3758ca3d1 100644 --- a/test/Lifecycle-test.ts +++ b/test/Lifecycle-test.ts @@ -14,28 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { mocked } from "jest-mock"; +import { Crypto } from "@peculiar/webcrypto"; import { logger } from "matrix-js-sdk/src/logger"; import * as MatrixJs from "matrix-js-sdk/src/matrix"; import { setCrypto } from "matrix-js-sdk/src/crypto/crypto"; -import { Crypto } from "@peculiar/webcrypto"; import * as MatrixCryptoAes from "matrix-js-sdk/src/crypto/aes"; import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvictedDialog"; import { restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; import Modal from "../src/Modal"; -import PlatformPeg from "../src/PlatformPeg"; import * as StorageManager from "../src/utils/StorageManager"; +import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; const webCrypto = new Crypto(); const windowCrypto = window.crypto; -import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils"; -import { subtleCrypto } from "matrix-js-sdk/src/crypto/crypto"; -import BasePlatform from "../src/BasePlatform"; - describe("Lifecycle", () => { const mockPlatform = mockPlatformPeg({ getPickleKey: jest.fn(), From fad7f33a824e3c2c354f7a33a532db7d69e9fa51 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 13 Jul 2023 08:04:56 +1200 Subject: [PATCH 04/12] type error --- test/Lifecycle-test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts index 1e3758ca3d1..6dbe89527b3 100644 --- a/test/Lifecycle-test.ts +++ b/test/Lifecycle-test.ts @@ -62,6 +62,8 @@ describe("Lifecycle", () => { jest.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined); // reset any mocking + // @ts-ignore mocking + delete global.localStorage; global.localStorage = realLocalStorage; setCrypto(webCrypto); From 32d5fb0fe692f2ac6ac22cb494ae6349ec440bd0 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 13 Jul 2023 10:36:45 +1200 Subject: [PATCH 05/12] extract token persisting code into function, persist refresh token --- src/Lifecycle.ts | 100 ++++++++++++++++++++++++++--------------- src/MatrixClientPeg.ts | 1 + 2 files changed, 66 insertions(+), 35 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 1c5514da776..244fd9ce4a3 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -70,6 +70,14 @@ import { completeOidcLogin } from "./utils/oidc/authorize"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; +const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token"; +const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token"; +/** + * Used during encryption/decryption of token + */ +const ACCESS_TOKEN_NAME = "access_token"; +const REFRESH_TOKEN_NAME = "refresh_token"; + dis.register((payload) => { if (payload.action === Action.TriggerLogout) { // noinspection JSIgnoredPromiseFromCall - we don't care if it fails @@ -452,17 +460,17 @@ export async function getStoredSessionVars(): Promise> { const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) ?? undefined; let accessToken: string | undefined; try { - accessToken = await StorageManager.idbLoad("account", "mx_access_token"); + accessToken = await StorageManager.idbLoad("account", ACCESS_TOKEN_STORAGE_KEY); } catch (e) { logger.error("StorageManager.idbLoad failed for account:mx_access_token", e); } if (!accessToken) { - accessToken = localStorage.getItem("mx_access_token") ?? undefined; + accessToken = localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY) ?? undefined; if (accessToken) { try { // try to migrate access token to IndexedDB if we can - await StorageManager.idbSave("account", "mx_access_token", accessToken); - localStorage.removeItem("mx_access_token"); + await StorageManager.idbSave("account", ACCESS_TOKEN_STORAGE_KEY, accessToken); + localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY); } catch (e) { logger.error("migration of access token to IndexedDB failed", e); } @@ -556,7 +564,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): logger.log("Got pickle key"); if (typeof accessToken !== "string") { const encrKey = await pickleKeyToAesKey(pickleKey); - decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token"); + decryptedAccessToken = await decryptAES(accessToken, encrKey, ACCESS_TOKEN_NAME); encrKey.fill(0); } } else { @@ -761,28 +769,26 @@ async function showStorageEvictedDialog(): Promise { // `instanceof`. Babel 7 supports this natively in their class handling. class AbortLoginAndRebuildStorage extends Error {} -async function persistCredentials(credentials: IMatrixClientCreds): Promise { - localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); - if (credentials.identityServerUrl) { - localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl); - } - localStorage.setItem("mx_user_id", credentials.userId); - localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); - - // store whether we expect to find an access token, to detect the case - // where IndexedDB is blown away - if (credentials.accessToken) { - localStorage.setItem("mx_has_access_token", "true"); - } else { - localStorage.removeItem("mx_has_access_token"); - } - - if (credentials.pickleKey) { - let encryptedAccessToken: IEncryptedPayload | undefined; +/** + * Persist a token in storage + * When pickle key is present, will attempt to encrypt the token + * Stores in idb, falling back to localStorage + * + * @param storageKey key used to store the token + * @param name eg "access_token" used as initialization vector during encryption + * @param token + * @param pickleKey optional pickle key used to encrypt token + */ +async function persistTokenInStorage(storageKey: string, name: string, token: string | undefined, pickleKey: IMatrixClientCreds['pickleKey']): Promise { + if (pickleKey) { + let encryptedToken: IEncryptedPayload | undefined; try { + if (!token) { + throw new Error("No token: not attempting encryption") + } // try to encrypt the access token using the pickle key - const encrKey = await pickleKeyToAesKey(credentials.pickleKey); - encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token"); + const encrKey = await pickleKeyToAesKey(pickleKey); + encryptedToken = await encryptAES(token, encrKey, name); encrKey.fill(0); } catch (e) { logger.warn("Could not encrypt access token", e); @@ -791,28 +797,52 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise { + localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); + if (credentials.identityServerUrl) { + localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl); + } + localStorage.setItem("mx_user_id", credentials.userId); + localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); + + // store whether we expect to find an access token, to detect the case + // where IndexedDB is blown away + if (credentials.accessToken) { + localStorage.setItem("mx_has_access_token", "true"); + } else { + localStorage.removeItem("mx_has_access_token"); + } + + await persistTokenInStorage(ACCESS_TOKEN_STORAGE_KEY, ACCESS_TOKEN_NAME ,credentials.accessToken, credentials.pickleKey); + await persistTokenInStorage(REFRESH_TOKEN_STORAGE_KEY, REFRESH_TOKEN_NAME, credentials.refreshToken, credentials.pickleKey); + + if (credentials.pickleKey) { + localStorage.setItem("mx_has_pickle_key", String(true)); + } else { if (localStorage.getItem("mx_has_pickle_key") === "true") { logger.error("Expected a pickle key, but none provided. Encryption may not work."); } @@ -1003,7 +1033,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise Date: Thu, 13 Jul 2023 11:00:13 +1200 Subject: [PATCH 06/12] store has_refresh_token too --- src/Lifecycle.ts | 22 ++++++++++++---------- test/Lifecycle-test.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 244fd9ce4a3..233945191fc 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -223,7 +223,7 @@ export async function attemptDelegatedAuthLogin( */ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise { try { - const { accessToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams); + const { accessToken, refreshToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams); const { user_id: userId, @@ -233,6 +233,7 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise const credentials = { accessToken, + refreshToken, homeserverUrl, identityServerUrl, deviceId, @@ -478,7 +479,7 @@ export async function getStoredSessionVars(): Promise> { } // if we pre-date storing "mx_has_access_token", but we retrieved an access // token, then we should say we have an access token - const hasAccessToken = localStorage.getItem("mx_has_access_token") === "true" || !!accessToken; + const hasAccessToken = localStorage.getItem(`mx_has_${ACCESS_TOKEN_NAME}`) === "true" || !!accessToken; const userId = localStorage.getItem("mx_user_id") ?? undefined; const deviceId = localStorage.getItem("mx_device_id") ?? undefined; @@ -780,6 +781,15 @@ class AbortLoginAndRebuildStorage extends Error {} * @param pickleKey optional pickle key used to encrypt token */ async function persistTokenInStorage(storageKey: string, name: string, token: string | undefined, pickleKey: IMatrixClientCreds['pickleKey']): Promise { + const hasTokenStorageKey = `mx_has_${name}`; + // store whether we expect to find a token, to detect the case + // where IndexedDB is blown away + if (token) { + localStorage.setItem(hasTokenStorageKey, "true"); + } else { + localStorage.removeItem(hasTokenStorageKey); + } + if (pickleKey) { let encryptedToken: IEncryptedPayload | undefined; try { @@ -829,14 +839,6 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise { jest.spyOn(mockPlatform, "createPickleKey"); }); + const refreshToken = 'test-refresh-token'; + const credentials = { homeserverUrl, identityServerUrl, @@ -307,6 +309,18 @@ describe("Lifecycle", () => { expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); }); + it("should persist a refreshToken when present", async () => { + await setLoggedIn({ + ...credentials, + refreshToken + }); + + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken); + expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_refresh_token", refreshToken); + // dont put accessToken in localstorage when we have idb + expect(localStorage.setItem).not.toHaveBeenCalledWith("mx_access_token", accessToken); + }); + it("should remove any access token from storage when there is none in credentials and idb save fails", async () => { jest.spyOn(StorageManager, "idbSave").mockRejectedValue("oups"); await setLoggedIn({ From 66d57e5f8a48648e6d3238e65fabdc6d0579ca5c Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 13 Jul 2023 11:01:55 +1200 Subject: [PATCH 07/12] pass refreshToken from oidcAuthGrant into credentials --- src/utils/oidc/authorize.ts | 2 ++ test/components/structures/MatrixChat-test.tsx | 1 + test/utils/oidc/authorize-test.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/src/utils/oidc/authorize.ts b/src/utils/oidc/authorize.ts index 705278c63dc..c904d3cf945 100644 --- a/src/utils/oidc/authorize.ts +++ b/src/utils/oidc/authorize.ts @@ -81,6 +81,7 @@ export const completeOidcLogin = async ( homeserverUrl: string; identityServerUrl?: string; accessToken: string; + refreshToken?: string; }> => { const { code, state } = getCodeAndStateFromQueryParams(queryParams); const { homeserverUrl, tokenResponse, identityServerUrl } = await completeAuthorizationCodeGrant(code, state); @@ -91,5 +92,6 @@ export const completeOidcLogin = async ( homeserverUrl: homeserverUrl, identityServerUrl: identityServerUrl, accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token }; }; diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 9b1ea991c7f..8c9435f66e0 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -908,6 +908,7 @@ describe("", () => { expect(localStorageSetSpy).toHaveBeenCalledWith("mx_hs_url", homeserverUrl); expect(localStorageSetSpy).toHaveBeenCalledWith("mx_user_id", userId); expect(localStorageSetSpy).toHaveBeenCalledWith("mx_has_access_token", "true"); + expect(localStorageSetSpy).toHaveBeenCalledWith("mx_has_refresh_token", "true"); expect(localStorageSetSpy).toHaveBeenCalledWith("mx_device_id", deviceId); }); diff --git a/test/utils/oidc/authorize-test.ts b/test/utils/oidc/authorize-test.ts index 7a554562e9b..8953b6a9cd4 100644 --- a/test/utils/oidc/authorize-test.ts +++ b/test/utils/oidc/authorize-test.ts @@ -130,6 +130,7 @@ describe("OIDC authorization", () => { expect(result).toEqual({ accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token, homeserverUrl, identityServerUrl, }); From b33e3479fd99406e2a3fc37dddb500b5346324d1 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Thu, 13 Jul 2023 13:02:01 +1200 Subject: [PATCH 08/12] rest restore session with pickle key --- test/Lifecycle-test.ts | 131 +++++++++++++++++++++++++++++++++-------- 1 file changed, 108 insertions(+), 23 deletions(-) diff --git a/test/Lifecycle-test.ts b/test/Lifecycle-test.ts index 6dbe89527b3..c93c5060d2a 100644 --- a/test/Lifecycle-test.ts +++ b/test/Lifecycle-test.ts @@ -32,9 +32,7 @@ const webCrypto = new Crypto(); const windowCrypto = window.crypto; describe("Lifecycle", () => { - const mockPlatform = mockPlatformPeg({ - getPickleKey: jest.fn(), - }); + const mockPlatform = mockPlatformPeg(); const realLocalStorage = global.localStorage; @@ -55,8 +53,6 @@ describe("Lifecycle", () => { }); beforeEach(() => { - mockPlatform.getPickleKey.mockResolvedValue(null); - // stub this jest.spyOn(MatrixClientPeg, "replaceUsingCreds").mockImplementation(() => {}); jest.spyOn(MatrixClientPeg, "start").mockResolvedValue(undefined); @@ -88,8 +84,16 @@ describe("Lifecycle", () => { .mockImplementation((key: unknown) => mockStore[key as string] ?? null); jest.spyOn(localStorage.__proto__, "removeItem") .mockClear() - .mockImplementation((key: unknown) => mockStore[key as string] ?? null); - jest.spyOn(localStorage.__proto__, "setItem").mockClear(); + .mockImplementation((key: unknown) => { + const { [key as string]: toRemove, ...newStore } = mockStore; + mockStore = newStore; + return toRemove; + }); + jest.spyOn(localStorage.__proto__, "setItem") + .mockClear() + .mockImplementation((key: unknown, value: unknown) => { + mockStore[key as string] = value; + }); }; const initSessionStorageMock = (mockStore: Record = {}): void => { @@ -98,8 +102,16 @@ describe("Lifecycle", () => { .mockImplementation((key: unknown) => mockStore[key as string] ?? null); jest.spyOn(sessionStorage.__proto__, "removeItem") .mockClear() - .mockImplementation((key: unknown) => mockStore[key as string] ?? null); - jest.spyOn(sessionStorage.__proto__, "setItem").mockClear(); + .mockImplementation((key: unknown) => { + const { [key as string]: toRemove, ...newStore } = mockStore; + mockStore = newStore; + return toRemove; + }); + jest.spyOn(sessionStorage.__proto__, "setItem") + .mockClear() + .mockImplementation((key: unknown, value: unknown) => { + mockStore[key as string] = value; + }); jest.spyOn(sessionStorage.__proto__, "clear").mockClear(); }; @@ -110,7 +122,16 @@ describe("Lifecycle", () => { // @ts-ignore mock type async (table: string, key: string) => mockStore[table]?.[key] ?? null, ); - jest.spyOn(StorageManager, "idbSave").mockClear().mockResolvedValue(undefined); + jest.spyOn(StorageManager, "idbSave") + .mockClear() + .mockImplementation( + // @ts-ignore mock type + async (tableKey: string, key: string, value: unknown) => { + const table = mockStore[tableKey] || {}; + table[key as string] = value; + mockStore[tableKey] = table; + }, + ); jest.spyOn(StorageManager, "idbDelete").mockClear().mockResolvedValue(undefined); }; @@ -130,6 +151,19 @@ describe("Lifecycle", () => { mx_access_token: accessToken, }, }; + const credentials = { + homeserverUrl, + identityServerUrl, + userId, + deviceId, + accessToken, + }; + + const encryptedTokenShapedObject = { + ciphertext: expect.any(String), + iv: expect.any(String), + mac: expect.any(String), + }; describe("restoreFromLocalStorage()", () => { beforeEach(() => { @@ -250,6 +284,65 @@ describe("Lifecycle", () => { expect(MatrixClientPeg.start).toHaveBeenCalled(); }); }); + + describe("with a pickle key", () => { + beforeEach(async () => { + initLocalStorageMock({}); + initIdbMock({}); + // setup storage with a session with encrypted token + await setLoggedIn(credentials); + }); + + it("should persist credentials", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true"); + + // token encrypted and persisted + expect(StorageManager.idbSave).toHaveBeenCalledWith( + "account", + "mx_access_token", + encryptedTokenShapedObject, + ); + }); + + it("should persist access token when idb is not available", async () => { + // dont fail for pickle key persist + jest.spyOn(StorageManager, "idbSave").mockImplementation( + async (table: string, key: string | string[]) => { + if (table === "account" && key === "mx_access_token") { + throw new Error("oups"); + } + }, + ); + + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(StorageManager.idbSave).toHaveBeenCalledWith( + "account", + "mx_access_token", + encryptedTokenShapedObject, + ); + // put accessToken in localstorage as fallback + expect(localStorage.setItem).toHaveBeenCalledWith("mx_access_token", accessToken); + }); + + it("should create new matrix client with credentials", async () => { + expect(await restoreFromLocalStorage()).toEqual(true); + + expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith({ + userId, + // decrypted accessToken + accessToken, + homeserverUrl, + identityServerUrl, + deviceId, + freshLogin: true, + guest: false, + pickleKey: expect.any(String), + }); + }); + }); }); }); @@ -269,14 +362,6 @@ describe("Lifecycle", () => { jest.spyOn(mockPlatform, "createPickleKey"); }); - const credentials = { - homeserverUrl, - identityServerUrl, - userId, - deviceId, - accessToken, - }; - it("should remove fresh login flag from session storage", async () => { await setLoggedIn(credentials); @@ -370,11 +455,11 @@ describe("Lifecycle", () => { expect(localStorage.setItem).toHaveBeenCalledWith("mx_device_id", deviceId); expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_pickle_key", "true"); - expect(StorageManager.idbSave).toHaveBeenCalledWith("account", "mx_access_token", { - ciphertext: expect.any(String), - iv: expect.any(String), - mac: expect.any(String), - }); + expect(StorageManager.idbSave).toHaveBeenCalledWith( + "account", + "mx_access_token", + encryptedTokenShapedObject, + ); expect(StorageManager.idbSave).toHaveBeenCalledWith( "pickleKey", [userId, deviceId], From 70ddb4aba2f177a48703e9456b6e8155bff96ba6 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Jul 2023 09:24:57 +1200 Subject: [PATCH 09/12] comments --- src/Lifecycle.ts | 23 ++++++++++++++++++----- src/utils/oidc/authorize.ts | 23 ++++++++++++----------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 44b9120be2a..c360c7115f2 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -70,13 +70,22 @@ import { completeOidcLogin } from "./utils/oidc/authorize"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; +/** + * Used as storage key + */ const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token"; const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token"; /** - * Used during encryption/decryption of token + * Used as initialization vector during encryption in persistTokenInStorage + * And decryption in restoreFromLocalStorage */ const ACCESS_TOKEN_NAME = "access_token"; const REFRESH_TOKEN_NAME = "refresh_token"; +/** + * Used in localstorage to store whether we expect a token in idb + */ +const HAS_ACCESS_TOKEN_STORAGE_KEY = "mx_has_access_token"; +const HAS_REFRESH_TOKEN_STORAGE_KEY = "mx_has_refresh_token"; dis.register((payload) => { if (payload.action === Action.TriggerLogout) { @@ -479,7 +488,7 @@ export async function getStoredSessionVars(): Promise> { } // if we pre-date storing "mx_has_access_token", but we retrieved an access // token, then we should say we have an access token - const hasAccessToken = localStorage.getItem(`mx_has_${ACCESS_TOKEN_NAME}`) === "true" || !!accessToken; + const hasAccessToken = localStorage.getItem(HAS_ACCESS_TOKEN_STORAGE_KEY) === "true" || !!accessToken; const userId = localStorage.getItem("mx_user_id") ?? undefined; const deviceId = localStorage.getItem("mx_device_id") ?? undefined; @@ -496,7 +505,7 @@ export async function getStoredSessionVars(): Promise> { // The pickle key is a string of unspecified length and format. For AES, we // need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES -// key. The AES key should be zeroed after it is used. +// key. The AES key should be zeroed after it is used async function pickleKeyToAesKey(pickleKey: string): Promise { const pickleKeyBuffer = new Uint8Array(pickleKey.length); for (let i = 0; i < pickleKey.length; i++) { @@ -777,16 +786,18 @@ class AbortLoginAndRebuildStorage extends Error {} * * @param storageKey key used to store the token * @param name eg "access_token" used as initialization vector during encryption - * @param token + * only used when pickleKey is present to encrypt with + * @param token the token to store, when undefined any existing token at the storageKey is removed from storage * @param pickleKey optional pickle key used to encrypt token + * @param hasTokenStorageKey used to store in localstorage whether we expect to have a token in idb, eg "mx_has_access_token" */ async function persistTokenInStorage( storageKey: string, name: string, token: string | undefined, pickleKey: IMatrixClientCreds["pickleKey"], + hasTokenStorageKey: string, ): Promise { - const hasTokenStorageKey = `mx_has_${name}`; // store whether we expect to find a token, to detect the case // where IndexedDB is blown away if (token) { @@ -849,12 +860,14 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise => { +export const completeOidcLogin = async (queryParams: QueryDict): Promise => { const { code, state } = getCodeAndStateFromQueryParams(queryParams); const { homeserverUrl, tokenResponse, identityServerUrl } = await completeAuthorizationCodeGrant(code, state); - // @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444 - return { homeserverUrl: homeserverUrl, identityServerUrl: identityServerUrl, From af481b222f397cf0ecc476f73796871a23606736 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 21 Jul 2023 11:41:10 +1200 Subject: [PATCH 10/12] prettier --- src/Lifecycle.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 31222aeedd5..9b4346796d5 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -233,9 +233,8 @@ export async function attemptDelegatedAuthLogin( */ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise { try { - const { accessToken, refreshToken, homeserverUrl, identityServerUrl, clientId, issuer } = await completeOidcLogin( - queryParams, - ); + const { accessToken, refreshToken, homeserverUrl, identityServerUrl, clientId, issuer } = + await completeOidcLogin(queryParams); const { user_id: userId, From e2d2d336d1babd01701c27d2b4f4189cac4fd94e Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 18 Sep 2023 11:51:39 +1200 Subject: [PATCH 11/12] Update src/Lifecycle.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- src/Lifecycle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 9b4346796d5..5ec1fd437ad 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -799,7 +799,7 @@ async function persistTokenInStorage( storageKey: string, name: string, token: string | undefined, - pickleKey: IMatrixClientCreds["pickleKey"], + pickleKey: string | undefined, hasTokenStorageKey: string, ): Promise { // store whether we expect to find a token, to detect the case From 1f903c947f4ef1114204a55002a3667fad9dcf30 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 18 Sep 2023 12:11:12 +1200 Subject: [PATCH 12/12] comments --- src/Lifecycle.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 1a20f60d707..c64702be219 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -71,19 +71,19 @@ import GenericToast from "./components/views/toasts/GenericToast"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; -/** - * Used as storage key +/* + * Keys used when storing the tokens in indexeddb or localstorage */ const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token"; const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token"; -/** +/* * Used as initialization vector during encryption in persistTokenInStorage * And decryption in restoreFromLocalStorage */ -const ACCESS_TOKEN_NAME = "access_token"; -const REFRESH_TOKEN_NAME = "refresh_token"; -/** - * Used in localstorage to store whether we expect a token in idb +const ACCESS_TOKEN_IV = "access_token"; +const REFRESH_TOKEN_IV = "refresh_token"; +/* + * Keys for localstorage items which indicate whether we expect a token in indexeddb. */ const HAS_ACCESS_TOKEN_STORAGE_KEY = "mx_has_access_token"; const HAS_REFRESH_TOKEN_STORAGE_KEY = "mx_has_refresh_token"; @@ -555,7 +555,7 @@ export async function getStoredSessionVars(): Promise> { // The pickle key is a string of unspecified length and format. For AES, we // need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES -// key. The AES key should be zeroed after it is used +// key. The AES key should be zeroed after it is used. async function pickleKeyToAesKey(pickleKey: string): Promise { const pickleKeyBuffer = new Uint8Array(pickleKey.length); for (let i = 0; i < pickleKey.length; i++) { @@ -624,7 +624,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): logger.log("Got pickle key"); if (typeof accessToken !== "string") { const encrKey = await pickleKeyToAesKey(pickleKey); - decryptedAccessToken = await decryptAES(accessToken, encrKey, ACCESS_TOKEN_NAME); + decryptedAccessToken = await decryptAES(accessToken, encrKey, ACCESS_TOKEN_IV); encrKey.fill(0); } } else { @@ -869,15 +869,14 @@ class AbortLoginAndRebuildStorage extends Error {} * Stores in idb, falling back to localStorage * * @param storageKey key used to store the token - * @param name eg "access_token" used as initialization vector during encryption - * only used when pickleKey is present to encrypt with + * @param initializationVector Initialization vector for encrypting the token. Only used when `pickleKey` is present * @param token the token to store, when undefined any existing token at the storageKey is removed from storage * @param pickleKey optional pickle key used to encrypt token - * @param hasTokenStorageKey used to store in localstorage whether we expect to have a token in idb, eg "mx_has_access_token" + * @param hasTokenStorageKey Localstorage key for an item which stores whether we expect to have a token in indexeddb, eg "mx_has_access_token". */ async function persistTokenInStorage( storageKey: string, - name: string, + initializationVector: string, token: string | undefined, pickleKey: string | undefined, hasTokenStorageKey: string, @@ -898,7 +897,7 @@ async function persistTokenInStorage( } // try to encrypt the access token using the pickle key const encrKey = await pickleKeyToAesKey(pickleKey); - encryptedToken = await encryptAES(token, encrKey, name); + encryptedToken = await encryptAES(token, encrKey, initializationVector); encrKey.fill(0); } catch (e) { logger.warn("Could not encrypt access token", e); @@ -941,14 +940,14 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise