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

Commit 6685403

Browse files
author
Kerry
authored
OIDC: extract token persistence functions to utils (#11690)
* extract token persistence functions to utils * add sugar
1 parent e889046 commit 6685403

File tree

2 files changed

+216
-155
lines changed

2 files changed

+216
-155
lines changed

src/Lifecycle.ts

Lines changed: 14 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ limitations under the License.
2020
import { ReactNode } from "react";
2121
import { createClient, MatrixClient, SSOAction } from "matrix-js-sdk/src/matrix";
2222
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
23-
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
23+
import { IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
2424
import { QueryDict } from "matrix-js-sdk/src/utils";
2525
import { logger } from "matrix-js-sdk/src/logger";
2626
import { MINIMUM_MATRIX_VERSION } from "matrix-js-sdk/src/version-support";
@@ -67,27 +67,21 @@ import { messageForLoginError } from "./utils/ErrorUtils";
6767
import { completeOidcLogin } from "./utils/oidc/authorize";
6868
import { persistOidcAuthenticatedSettings } from "./utils/oidc/persistOidcSettings";
6969
import GenericToast from "./components/views/toasts/GenericToast";
70+
import {
71+
ACCESS_TOKEN_IV,
72+
ACCESS_TOKEN_STORAGE_KEY,
73+
HAS_ACCESS_TOKEN_STORAGE_KEY,
74+
HAS_REFRESH_TOKEN_STORAGE_KEY,
75+
persistAccessTokenInStorage,
76+
persistRefreshTokenInStorage,
77+
REFRESH_TOKEN_IV,
78+
REFRESH_TOKEN_STORAGE_KEY,
79+
tryDecryptToken,
80+
} from "./utils/tokens/tokens";
7081

7182
const HOMESERVER_URL_KEY = "mx_hs_url";
7283
const ID_SERVER_URL_KEY = "mx_is_url";
7384

74-
/*
75-
* Keys used when storing the tokens in indexeddb or localstorage
76-
*/
77-
const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token";
78-
const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token";
79-
/*
80-
* Used as initialization vector during encryption in persistTokenInStorage
81-
* And decryption in restoreFromLocalStorage
82-
*/
83-
const ACCESS_TOKEN_IV = "access_token";
84-
const REFRESH_TOKEN_IV = "refresh_token";
85-
/*
86-
* Keys for localstorage items which indicate whether we expect a token in indexeddb.
87-
*/
88-
const HAS_ACCESS_TOKEN_STORAGE_KEY = "mx_has_access_token";
89-
const HAS_REFRESH_TOKEN_STORAGE_KEY = "mx_has_refresh_token";
90-
9185
dis.register((payload) => {
9286
if (payload.action === Action.TriggerLogout) {
9387
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
@@ -566,32 +560,6 @@ export async function getStoredSessionVars(): Promise<Partial<IStoredSession>> {
566560
return { hsUrl, isUrl, hasAccessToken, accessToken, refreshToken, hasRefreshToken, userId, deviceId, isGuest };
567561
}
568562

569-
// The pickle key is a string of unspecified length and format. For AES, we
570-
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
571-
// key. The AES key should be zeroed after it is used.
572-
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
573-
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
574-
for (let i = 0; i < pickleKey.length; i++) {
575-
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
576-
}
577-
const hkdfKey = await window.crypto.subtle.importKey("raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"]);
578-
pickleKeyBuffer.fill(0);
579-
return new Uint8Array(
580-
await window.crypto.subtle.deriveBits(
581-
{
582-
name: "HKDF",
583-
hash: "SHA-256",
584-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
585-
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
586-
salt: new Uint8Array(32),
587-
info: new Uint8Array(0),
588-
},
589-
hkdfKey,
590-
256,
591-
),
592-
);
593-
}
594-
595563
async function abortLogin(): Promise<void> {
596564
const signOut = await showStorageEvictedDialog();
597565
if (signOut) {
@@ -602,36 +570,6 @@ async function abortLogin(): Promise<void> {
602570
}
603571
}
604572

605-
const isEncryptedPayload = (token?: IEncryptedPayload | string | undefined): token is IEncryptedPayload => {
606-
return !!token && typeof token !== "string";
607-
};
608-
/**
609-
* Try to decrypt a token retrieved from storage
610-
* Where token is not encrypted (plain text) returns the plain text token
611-
* Where token is encrypted, attempts decryption. Returns successfully decrypted token, else undefined.
612-
* @param pickleKey pickle key used during encryption of token, or undefined
613-
* @param token
614-
* @param tokenIv initialization vector used during encryption of token eg ACCESS_TOKEN_IV
615-
* @returns the decrypted token, or the plain text token. Returns undefined when token cannot be decrypted
616-
*/
617-
async function tryDecryptToken(
618-
pickleKey: string | undefined,
619-
token: IEncryptedPayload | string | undefined,
620-
tokenIv: string,
621-
): Promise<string | undefined> {
622-
if (pickleKey && isEncryptedPayload(token)) {
623-
const encrKey = await pickleKeyToAesKey(pickleKey);
624-
const decryptedToken = await decryptAES(token, encrKey, tokenIv);
625-
encrKey.fill(0);
626-
return decryptedToken;
627-
}
628-
// if the token wasn't encrypted (plain string) just return it back
629-
if (typeof token === "string") {
630-
return token;
631-
}
632-
// otherwise return undefined
633-
}
634-
635573
// returns a promise which resolves to true if a session is found in
636574
// localstorage
637575
//
@@ -901,73 +839,6 @@ async function showStorageEvictedDialog(): Promise<boolean> {
901839
// `instanceof`. Babel 7 supports this natively in their class handling.
902840
class AbortLoginAndRebuildStorage extends Error {}
903841

904-
/**
905-
* Persist a token in storage
906-
* When pickle key is present, will attempt to encrypt the token
907-
* Stores in idb, falling back to localStorage
908-
*
909-
* @param storageKey key used to store the token
910-
* @param initializationVector Initialization vector for encrypting the token. Only used when `pickleKey` is present
911-
* @param token the token to store, when undefined any existing token at the storageKey is removed from storage
912-
* @param pickleKey optional pickle key used to encrypt token
913-
* @param hasTokenStorageKey Localstorage key for an item which stores whether we expect to have a token in indexeddb, eg "mx_has_access_token".
914-
*/
915-
async function persistTokenInStorage(
916-
storageKey: string,
917-
initializationVector: string,
918-
token: string | undefined,
919-
pickleKey: string | undefined,
920-
hasTokenStorageKey: string,
921-
): Promise<void> {
922-
// store whether we expect to find a token, to detect the case
923-
// where IndexedDB is blown away
924-
if (token) {
925-
localStorage.setItem(hasTokenStorageKey, "true");
926-
} else {
927-
localStorage.removeItem(hasTokenStorageKey);
928-
}
929-
930-
if (pickleKey) {
931-
let encryptedToken: IEncryptedPayload | undefined;
932-
try {
933-
if (!token) {
934-
throw new Error("No token: not attempting encryption");
935-
}
936-
// try to encrypt the access token using the pickle key
937-
const encrKey = await pickleKeyToAesKey(pickleKey);
938-
encryptedToken = await encryptAES(token, encrKey, initializationVector);
939-
encrKey.fill(0);
940-
} catch (e) {
941-
logger.warn("Could not encrypt access token", e);
942-
}
943-
try {
944-
// save either the encrypted access token, or the plain access
945-
// token if we were unable to encrypt (e.g. if the browser doesn't
946-
// have WebCrypto).
947-
await StorageManager.idbSave("account", storageKey, encryptedToken || token);
948-
} catch (e) {
949-
// if we couldn't save to indexedDB, fall back to localStorage. We
950-
// store the access token unencrypted since localStorage only saves
951-
// strings.
952-
if (!!token) {
953-
localStorage.setItem(storageKey, token);
954-
} else {
955-
localStorage.removeItem(storageKey);
956-
}
957-
}
958-
} else {
959-
try {
960-
await StorageManager.idbSave("account", storageKey, token);
961-
} catch (e) {
962-
if (!!token) {
963-
localStorage.setItem(storageKey, token);
964-
} else {
965-
localStorage.removeItem(storageKey);
966-
}
967-
}
968-
}
969-
}
970-
971842
async function persistCredentials(credentials: IMatrixClientCreds): Promise<void> {
972843
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
973844
if (credentials.identityServerUrl) {
@@ -976,20 +847,8 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
976847
localStorage.setItem("mx_user_id", credentials.userId);
977848
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
978849

979-
await persistTokenInStorage(
980-
ACCESS_TOKEN_STORAGE_KEY,
981-
ACCESS_TOKEN_IV,
982-
credentials.accessToken,
983-
credentials.pickleKey,
984-
HAS_ACCESS_TOKEN_STORAGE_KEY,
985-
);
986-
await persistTokenInStorage(
987-
REFRESH_TOKEN_STORAGE_KEY,
988-
REFRESH_TOKEN_IV,
989-
credentials.refreshToken,
990-
credentials.pickleKey,
991-
HAS_REFRESH_TOKEN_STORAGE_KEY,
992-
);
850+
await persistAccessTokenInStorage(credentials.accessToken, credentials.pickleKey);
851+
await persistRefreshTokenInStorage(credentials.refreshToken, credentials.pickleKey);
993852

994853
if (credentials.pickleKey) {
995854
localStorage.setItem("mx_has_pickle_key", String(true));

0 commit comments

Comments
 (0)