1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-08-07 21:23:00 +03:00

use random pickle key on all platforms, and store access token encrypted in IDB

This commit is contained in:
Hubert Chathi
2020-12-09 18:40:31 -05:00
parent 5f4320e80d
commit 753ec9e45a
4 changed files with 219 additions and 26 deletions

View File

@@ -21,6 +21,7 @@ limitations under the License.
import Matrix from 'matrix-js-sdk';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security";
@@ -50,6 +51,7 @@ import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics";
import CallHandler from './CallHandler';
import LifecycleCustomisations from "./customisations/Lifecycle";
import {idbLoad, idbSave, idbDelete} from "./IndexedDB";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@@ -147,20 +149,13 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
* Gets the user ID of the persisted session, if one exists. This does not validate
* that the user's credentials still work, just that they exist and that a user ID
* is associated with them. The session is not loaded.
* @returns {String} The persisted session's owner, if an owner exists. Null otherwise.
* @returns {[String, bool]} The persisted session's owner and whether the stored
* session is for a guest user, if an owner exists. If there is no stored session,
* return [null, null].
*/
export function getStoredSessionOwner(): string {
const {hsUrl, userId, accessToken} = getLocalStorageSessionVars();
return hsUrl && userId && accessToken ? userId : null;
}
/**
* @returns {bool} True if the stored session is for a guest user or false if it is
* for a real user. If there is no stored session, return null.
*/
export function getStoredSessionIsGuest(): boolean {
const sessVars = getLocalStorageSessionVars();
return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
const {hsUrl, userId, accessToken, isGuest} = await getLocalStorageSessionVars();
return hsUrl && userId && accessToken ? [userId, isGuest] : [null, null];
}
/**
@@ -197,8 +192,8 @@ export function attemptTokenLogin(
},
).then(function(creds) {
console.log("Logged in with token");
return clearStorage().then(() => {
persistCredentialsToLocalStorage(creds);
return clearStorage().then(async () => {
await persistCredentialsToLocalStorage(creds);
// remember that we just logged in
sessionStorage.setItem("mx_fresh_login", String(true));
return true;
@@ -290,10 +285,17 @@ export interface ILocalStorageSession {
* may not be valid, as it is not tested for consistency here.
* @returns {Object} Information about the session - see implementation for variables.
*/
export function getLocalStorageSessionVars(): ILocalStorageSession {
export async function getLocalStorageSessionVars(): Promise<ILocalStorageSession> {
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
const accessToken = localStorage.getItem("mx_access_token");
let accessToken = await idbLoad("account", "mx_access_token");
if (!accessToken) {
accessToken = localStorage.getItem("mx_access_token");
if (accessToken) {
await idbSave("account", "mx_access_token", accessToken);
localStorage.removeItem("mx_access_token");
}
}
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
@@ -308,6 +310,30 @@ export function getLocalStorageSessionVars(): ILocalStorageSession {
return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest};
}
// 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.
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
for (let i = 0; i < pickleKey.length; i++) {
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
}
const hkdfKey = await window.crypto.subtle.importKey(
"raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"],
);
pickleKeyBuffer.fill(0);
return new Uint8Array(await window.crypto.subtle.deriveBits(
{
name: "HKDF", hash: "SHA-256",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
salt: new Uint8Array(32), info: new Uint8Array(0),
},
hkdfKey,
256,
));
}
// returns a promise which resolves to true if a session is found in
// localstorage
//
@@ -325,7 +351,7 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
return false;
}
const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = getLocalStorageSessionVars();
const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = await getLocalStorageSessionVars();
if (accessToken && userId && hsUrl) {
if (ignoreGuest && isGuest) {
@@ -333,9 +359,15 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
return false;
}
let decryptedAccessToken = accessToken;
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
if (pickleKey) {
console.log("Got pickle key");
if (typeof accessToken !== "string") {
const encrKey = await pickleKeyToAesKey(pickleKey);
decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token");
encrKey.fill(0);
}
} else {
console.log("No pickle key available");
}
@@ -347,7 +379,7 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
await doSetLoggedIn({
userId: userId,
deviceId: deviceId,
accessToken: accessToken,
accessToken: decryptedAccessToken,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,
@@ -516,7 +548,7 @@ async function doSetLoggedIn(
if (localStorage) {
try {
persistCredentialsToLocalStorage(credentials);
await persistCredentialsToLocalStorage(credentials);
// make sure we don't think that it's a fresh login any more
sessionStorage.removeItem("mx_fresh_login");
} catch (e) {
@@ -545,18 +577,22 @@ function showStorageEvictedDialog(): Promise<boolean> {
// `instanceof`. Babel 7 supports this natively in their class handling.
class AbortLoginAndRebuildStorage extends Error { }
function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
async function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): Promise<void> {
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_access_token", credentials.accessToken);
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
if (credentials.pickleKey) {
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
const encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token");
encrKey.fill(0);
await idbSave("account", "mx_access_token", encryptedAccessToken);
localStorage.setItem("mx_has_pickle_key", String(true));
} else {
await idbSave("account", "mx_access_token", credentials.accessToken);
if (localStorage.getItem("mx_has_pickle_key")) {
console.error("Expected a pickle key, but none provided. Encryption may not work.");
}
@@ -733,6 +769,8 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
window.localStorage.clear();
await idbDelete("account", "mx_access_token");
// now restore those invites
if (!opts?.deleteEverything) {
pendingInvites.forEach(i => {