Log clearer errors when picklekey goes missing (#27)

* tokens.ts: improve documentation

Improve variable naming and documentation on the methods in `tokens.ts`.

* rename restoreFromLocalStorage

Since the session data isn't actually stored in localstorage, this feels like a
misleading name.

* Lifecycle: bail out if picklekey is missing

Currently, if we have an accesstoken which is encrypted with a picklekey, but
the picklekey has gone missing, we carry on with no access token at all. This
is sure to blow up in some way or other later on, but in a rather cryptic way.

Instead, let's bail out early.

(This will produce a "can't restore session" error, but we normally see one of
those anyway because we can't initialise the crypto store.)
This commit is contained in:
Richard van der Hoff 2024-09-11 16:13:04 +01:00 committed by GitHub
parent d337fba76e
commit 433c14e5a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 119 additions and 77 deletions

View file

@ -15,7 +15,7 @@ import { mocked, MockedObject } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import StorageEvictedDialog from "../src/components/views/dialogs/StorageEvictedDialog";
import { logout, restoreFromLocalStorage, setLoggedIn } from "../src/Lifecycle";
import { logout, restoreSessionFromStorage, setLoggedIn } from "../src/Lifecycle";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import Modal from "../src/Modal";
import * as StorageAccess from "../src/utils/StorageAccess";
@ -137,7 +137,12 @@ describe("Lifecycle", () => {
mockStore[tableKey] = table;
},
);
jest.spyOn(StorageAccess, "idbDelete").mockClear().mockResolvedValue(undefined);
jest.spyOn(StorageAccess, "idbDelete")
.mockClear()
.mockImplementation(async (tableKey: string, key: string | string[]) => {
const table = mockStore[tableKey];
delete table?.[key as string];
});
};
const homeserverUrl = "https://server.org";
@ -172,7 +177,7 @@ describe("Lifecycle", () => {
mac: expect.any(String),
};
describe("restoreFromLocalStorage()", () => {
describe("restoreSessionFromStorage()", () => {
beforeEach(() => {
initLocalStorageMock();
initSessionStorageMock();
@ -196,18 +201,18 @@ describe("Lifecycle", () => {
// @ts-ignore dirty mocking
global.localStorage = undefined;
expect(await restoreFromLocalStorage()).toEqual(false);
expect(await restoreSessionFromStorage()).toEqual(false);
});
it("should return false when no session data is found in local storage", async () => {
expect(await restoreFromLocalStorage()).toEqual(false);
expect(await restoreSessionFromStorage()).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();
await expect(() => restoreSessionFromStorage()).rejects.toThrow();
expect(Modal.createDialog).toHaveBeenCalledWith(StorageEvictedDialog);
expect(mockClient.clearStores).toHaveBeenCalled();
});
@ -220,12 +225,12 @@ describe("Lifecycle", () => {
});
it("should ignore guest accounts when ignoreGuest is true", async () => {
expect(await restoreFromLocalStorage({ ignoreGuest: true })).toEqual(false);
expect(await restoreSessionFromStorage({ ignoreGuest: true })).toEqual(false);
expect(logger.log).toHaveBeenCalledWith(`Ignoring stored guest account: ${userId}`);
});
it("should restore guest accounts when ignoreGuest is false", async () => {
expect(await restoreFromLocalStorage({ ignoreGuest: false })).toEqual(true);
expect(await restoreSessionFromStorage({ ignoreGuest: false })).toEqual(true);
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
expect.objectContaining({
@ -245,7 +250,7 @@ describe("Lifecycle", () => {
});
it("should persist credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(localStorage.setItem).toHaveBeenCalledWith("mx_user_id", userId);
expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true");
@ -259,7 +264,7 @@ describe("Lifecycle", () => {
it("should persist access token when idb is not available", async () => {
jest.spyOn(StorageAccess, "idbSave").mockRejectedValue("oups");
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(StorageAccess.idbSave).toHaveBeenCalledWith("account", "mx_access_token", accessToken);
// put accessToken in localstorage as fallback
@ -267,7 +272,7 @@ describe("Lifecycle", () => {
});
it("should create and start new matrix client with credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
{
@ -287,13 +292,13 @@ describe("Lifecycle", () => {
});
it("should remove fresh login flag from session storage", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(sessionStorage.removeItem).toHaveBeenCalledWith("mx_fresh_login");
});
it("should start matrix client", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(MatrixClientPeg.start).toHaveBeenCalled();
});
@ -308,7 +313,7 @@ describe("Lifecycle", () => {
});
it("should persist credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
// refresh token from storage is re-persisted
expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_refresh_token", "true");
@ -316,7 +321,7 @@ describe("Lifecycle", () => {
});
it("should create new matrix client with credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
{
@ -354,7 +359,7 @@ describe("Lifecycle", () => {
});
it("should persist credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_access_token", "true");
@ -376,7 +381,7 @@ describe("Lifecycle", () => {
},
);
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(StorageAccess.idbSave).toHaveBeenCalledWith(
"account",
@ -395,7 +400,7 @@ describe("Lifecycle", () => {
});
// Perform the restore
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
// Ensure that the expected calls were made
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
@ -422,7 +427,7 @@ describe("Lifecycle", () => {
});
it("should persist credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
// refresh token from storage is re-persisted
expect(localStorage.setItem).toHaveBeenCalledWith("mx_has_refresh_token", "true");
@ -434,7 +439,7 @@ describe("Lifecycle", () => {
});
it("should create new matrix client with credentials", async () => {
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
{
@ -484,7 +489,7 @@ describe("Lifecycle", () => {
it("should create and start new matrix client with credentials", async () => {
// Perform the restore
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
// Ensure that the expected calls were made
expect(MatrixClientPeg.replaceUsingCreds).toHaveBeenCalledWith(
@ -511,7 +516,24 @@ describe("Lifecycle", () => {
initIdbMock(idbStorageSession);
mockClient.isVersionSupported.mockRejectedValue(new Error("Oh, noes, the server is down!"));
expect(await restoreFromLocalStorage()).toEqual(true);
expect(await restoreSessionFromStorage()).toEqual(true);
});
it("should throw if the token was persisted with a pickle key but there is no pickle key available now", async () => {
initLocalStorageMock(localStorageSession);
initIdbMock({});
// Create a pickle key, and store it, encrypted, in IDB.
const pickleKey = (await PlatformPeg.get()!.createPickleKey(credentials.userId, credentials.deviceId))!;
localStorage.setItem("mx_has_pickle_key", "true");
await persistAccessTokenInStorage(credentials.accessToken, pickleKey);
// Now destroy the pickle key
await PlatformPeg.get()!.destroyPickleKey(credentials.userId, credentials.deviceId);
await expect(restoreSessionFromStorage()).rejects.toThrow(
"Error decrypting secret access_token: no pickle key found.",
);
});
});
});