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

Commit 433c14e

Browse files
authored
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.)
1 parent d337fba commit 433c14e

File tree

4 files changed

+119
-77
lines changed

4 files changed

+119
-77
lines changed

src/Lifecycle.ts

+18-15
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
203203
false,
204204
).then(() => true);
205205
}
206-
const success = await restoreFromLocalStorage({
206+
const success = await restoreSessionFromStorage({
207207
ignoreGuest: Boolean(opts.ignoreGuest),
208208
});
209209
if (success) {
@@ -548,17 +548,19 @@ async function abortLogin(): Promise<void> {
548548
}
549549
}
550550

551-
// returns a promise which resolves to true if a session is found in
552-
// localstorage
553-
//
554-
// N.B. Lifecycle.js should not maintain any further localStorage state, we
555-
// are moving towards using SessionStore to keep track of state related
556-
// to the current session (which is typically backed by localStorage).
557-
//
558-
// The plan is to gradually move the localStorage access done here into
559-
// SessionStore to avoid bugs where the view becomes out-of-sync with
560-
// localStorage (e.g. isGuest etc.)
561-
export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
551+
/** Attempt to restore the session from localStorage or indexeddb.
552+
*
553+
* @returns true if a session was found; false if no existing session was found.
554+
*
555+
* N.B. Lifecycle.js should not maintain any further localStorage state, we
556+
* are moving towards using SessionStore to keep track of state related
557+
* to the current session (which is typically backed by localStorage).
558+
*
559+
* The plan is to gradually move the localStorage access done here into
560+
* SessionStore to avoid bugs where the view becomes out-of-sync with
561+
* localStorage (e.g. isGuest etc.)
562+
*/
563+
export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
562564
const ignoreGuest = opts?.ignoreGuest;
563565

564566
if (!localStorage) {
@@ -582,10 +584,11 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
582584
if (pickleKey) {
583585
logger.log(`Got pickle key for ${userId}|${deviceId}`);
584586
} else {
585-
logger.log("No pickle key available");
587+
logger.log(`No pickle key available for ${userId}|${deviceId}`);
586588
}
587589
const decryptedAccessToken = await tryDecryptToken(pickleKey, accessToken, ACCESS_TOKEN_IV);
588-
const decryptedRefreshToken = await tryDecryptToken(pickleKey, refreshToken, REFRESH_TOKEN_IV);
590+
const decryptedRefreshToken =
591+
refreshToken && (await tryDecryptToken(pickleKey, refreshToken, REFRESH_TOKEN_IV));
589592

590593
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
591594
sessionStorage.removeItem("mx_fresh_login");
@@ -595,7 +598,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
595598
{
596599
userId: userId,
597600
deviceId: deviceId,
598-
accessToken: decryptedAccessToken!,
601+
accessToken: decryptedAccessToken,
599602
refreshToken: decryptedRefreshToken,
600603
homeserverUrl: hsUrl,
601604
identityServerUrl: isUrl,

src/components/structures/MatrixChat.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
367367
// Create and start the client
368368
// accesses the new credentials just set in storage during attemptDelegatedAuthLogin
369369
// and sets logged in state
370-
await Lifecycle.restoreFromLocalStorage({ ignoreGuest: true });
370+
await Lifecycle.restoreSessionFromStorage({ ignoreGuest: true });
371371
await this.postLoginSetup();
372372
return;
373373
}

src/utils/tokens/tokens.ts

+56-39
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import * as StorageAccess from "../StorageAccess";
1616
*/
1717

1818
/*
19-
* Keys used when storing the tokens in indexeddb or localstorage
19+
* Names used when storing the tokens in indexeddb or localstorage
2020
*/
2121
export const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token";
2222
export const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token";
2323
/*
24-
* Used as initialization vector during encryption in persistTokenInStorage
25-
* And decryption in restoreFromLocalStorage
24+
* Names of the tokens. Used as part of the calculation to derive AES keys during encryption in persistTokenInStorage,
25+
* and decryption in restoreSessionFromStorage.
2626
*/
2727
export const ACCESS_TOKEN_IV = "access_token";
2828
export const REFRESH_TOKEN_IV = "refresh_token";
@@ -60,50 +60,63 @@ async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
6060
);
6161
}
6262

63-
const isEncryptedPayload = (token?: IEncryptedPayload | string | undefined): token is IEncryptedPayload => {
64-
return !!token && typeof token !== "string";
65-
};
6663
/**
6764
* Try to decrypt a token retrieved from storage
68-
* Where token is not encrypted (plain text) returns the plain text token
69-
* Where token is encrypted, attempts decryption. Returns successfully decrypted token, else undefined.
70-
* @param pickleKey pickle key used during encryption of token, or undefined
71-
* @param token
72-
* @param tokenIv initialization vector used during encryption of token eg ACCESS_TOKEN_IV
73-
* @returns the decrypted token, or the plain text token. Returns undefined when token cannot be decrypted
65+
*
66+
* Where token is not encrypted (plain text) returns the plain text token.
67+
*
68+
* Where token is encrypted, attempts decryption. Returns successfully decrypted token, or throws if
69+
* decryption failed.
70+
*
71+
* @param pickleKey Pickle key: used to derive the encryption key, or undefined if the token is not encrypted.
72+
* Must be the same as provided to {@link persistTokenInStorage}.
73+
* @param token token to be decrypted.
74+
* @param tokenName Name of the token. Used in logging, but also used as an input when generating the actual AES key,
75+
* so the same value must be provided to {@link persistTokenInStorage}.
76+
*
77+
* @returns the decrypted token, or the plain text token.
7478
*/
7579
export async function tryDecryptToken(
7680
pickleKey: string | undefined,
77-
token: IEncryptedPayload | string | undefined,
78-
tokenIv: string,
79-
): Promise<string | undefined> {
80-
if (pickleKey && isEncryptedPayload(token)) {
81-
const encrKey = await pickleKeyToAesKey(pickleKey);
82-
const decryptedToken = await decryptAES(token, encrKey, tokenIv);
83-
encrKey.fill(0);
84-
return decryptedToken;
85-
}
86-
// if the token wasn't encrypted (plain string) just return it back
81+
token: IEncryptedPayload | string,
82+
tokenName: string,
83+
): Promise<string> {
8784
if (typeof token === "string") {
85+
// Looks like an unencrypted token
8886
return token;
8987
}
90-
// otherwise return undefined
88+
89+
// Otherwise, it must be an encrypted token.
90+
if (!pickleKey) {
91+
throw new Error(`Error decrypting secret ${tokenName}: no pickle key found.`);
92+
}
93+
94+
const encrKey = await pickleKeyToAesKey(pickleKey);
95+
const decryptedToken = await decryptAES(token, encrKey, tokenName);
96+
encrKey.fill(0);
97+
return decryptedToken;
9198
}
9299

93100
/**
94101
* Persist a token in storage
95-
* When pickle key is present, will attempt to encrypt the token
96-
* Stores in idb, falling back to localStorage
97102
*
98-
* @param storageKey key used to store the token
99-
* @param initializationVector Initialization vector for encrypting the token. Only used when `pickleKey` is present
100-
* @param token the token to store, when undefined any existing token at the storageKey is removed from storage
101-
* @param pickleKey optional pickle key used to encrypt token
102-
* @param hasTokenStorageKey Localstorage key for an item which stores whether we expect to have a token in indexeddb, eg "mx_has_access_token".
103+
* When pickle key is present, will attempt to encrypt the token. If encryption fails (typically because
104+
* WebCrypto is unavailable), the key will be stored unencrypted.
105+
*
106+
* Stores in IndexedDB, falling back to localStorage.
107+
*
108+
* @param storageKey key used to store the token. Note: not an encryption key; rather a localstorage or indexeddb key.
109+
* @param tokenName Name of the token. Used in logging, but also used as an input when generating the actual AES key,
110+
* so the same value must be provided to {@link tryDecryptToken} when decrypting.
111+
* @param token the token to store. When undefined, any existing token at the `storageKey` is removed from storage.
112+
* @param pickleKey Pickle key: used to derive the key used to encrypt token. If `undefined`, the token will be stored
113+
* unencrypted.
114+
* @param hasTokenStorageKey Localstorage key for an item which stores whether we expect to have a token in indexeddb,
115+
* eg "mx_has_access_token".
103116
*/
104117
export async function persistTokenInStorage(
105118
storageKey: string,
106-
initializationVector: string,
119+
tokenName: string,
107120
token: string | undefined,
108121
pickleKey: string | undefined,
109122
hasTokenStorageKey: string,
@@ -122,12 +135,12 @@ export async function persistTokenInStorage(
122135
try {
123136
// try to encrypt the access token using the pickle key
124137
const encrKey = await pickleKeyToAesKey(pickleKey);
125-
encryptedToken = await encryptAES(token, encrKey, initializationVector);
138+
encryptedToken = await encryptAES(token, encrKey, tokenName);
126139
encrKey.fill(0);
127140
} catch (e) {
128141
// This is likely due to the browser not having WebCrypto or somesuch.
129142
// Warn about it, but fall back to storing the unencrypted token.
130-
logger.warn(`Could not encrypt token for ${storageKey}`, e);
143+
logger.warn(`Could not encrypt token for ${tokenName}`, e);
131144
}
132145
}
133146
try {
@@ -159,9 +172,11 @@ export async function persistTokenInStorage(
159172
}
160173

161174
/**
162-
* Wraps persistTokenInStorage with accessToken storage keys
163-
* @param token the token to store, when undefined any existing accessToken is removed from storage
164-
* @param pickleKey optional pickle key used to encrypt token
175+
* Wraps {@link persistTokenInStorage} with accessToken storage keys
176+
*
177+
* @param token - The token to store. When undefined, any existing accessToken is removed from storage.
178+
* @param pickleKey - Pickle key: used to derive the key used to encrypt token. If `undefined`, the token will be stored
179+
* unencrypted.
165180
*/
166181
export async function persistAccessTokenInStorage(
167182
token: string | undefined,
@@ -177,9 +192,11 @@ export async function persistAccessTokenInStorage(
177192
}
178193

179194
/**
180-
* Wraps persistTokenInStorage with refreshToken storage keys
181-
* @param token the token to store, when undefined any existing refreshToken is removed from storage
182-
* @param pickleKey optional pickle key used to encrypt token
195+
* Wraps {@link persistTokenInStorage} with refreshToken storage keys.
196+
*
197+
* @param token - The token to store. When undefined, any existing refreshToken is removed from storage.
198+
* @param pickleKey - Pickle key: used to derive the key used to encrypt token. If `undefined`, the token will be stored
199+
* unencrypted.
183200
*/
184201
export async function persistRefreshTokenInStorage(
185202
token: string | undefined,

0 commit comments

Comments
 (0)