store e2ee session values as well in localStorage

This commit is contained in:
Bruno Windels 2021-09-29 11:49:58 +02:00
parent cd071e47e0
commit 77bd0d3f3c
11 changed files with 160 additions and 23 deletions

View file

@ -233,6 +233,7 @@ export class SessionContainer {
platform: this._platform,
});
await this._session.load(log);
// TODO: check instead storage doesn't have an identity
if (isNewLogin) {
this._status.set(LoadStatus.SessionSetup);
await log.wrap("createIdentity", log => this._session.createIdentity(log));

View file

@ -15,12 +15,12 @@ limitations under the License.
*/
import anotherjson from "../../../lib/another-json/index.js";
import {SESSION_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common.js";
import {SESSION_E2EE_KEY_PREFIX, OLM_ALGORITHM, MEGOLM_ALGORITHM} from "./common.js";
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
const ACCOUNT_SESSION_KEY = SESSION_KEY_PREFIX + "olmAccount";
const DEVICE_KEY_FLAG_SESSION_KEY = SESSION_KEY_PREFIX + "areDeviceKeysUploaded";
const SERVER_OTK_COUNT_SESSION_KEY = SESSION_KEY_PREFIX + "serverOTKCount";
const ACCOUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "olmAccount";
const DEVICE_KEY_FLAG_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "areDeviceKeysUploaded";
const SERVER_OTK_COUNT_SESSION_KEY = SESSION_E2EE_KEY_PREFIX + "serverOTKCount";
export class Account {
static async load({olm, pickleKey, hsApi, userId, deviceId, olmWorker, txn}) {

View file

@ -20,7 +20,7 @@ import {createEnum} from "../../utils/enum.js";
export const DecryptionSource = createEnum("Sync", "Timeline", "Retry");
// use common prefix so it's easy to clear properties that are not e2ee related during session clear
export const SESSION_KEY_PREFIX = "e2ee:";
export const SESSION_E2EE_KEY_PREFIX = "e2ee:";
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";

View file

@ -22,6 +22,7 @@ import {IDBKey} from "./Transaction";
export interface ITransaction {
idbFactory: IDBFactory;
IDBKeyRange: typeof IDBKeyRange;
databaseName: string;
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined);
}
@ -55,6 +56,10 @@ export class QueryTarget<T> {
return this._transaction.IDBKeyRange;
}
get databaseName(): string {
return this._transaction.databaseName;
}
_openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null> {
if (range && direction) {
return this._target.openCursor(range, direction);
@ -269,6 +274,7 @@ import {QueryTargetWrapper, Store} from "./Store";
export function tests() {
class MockTransaction extends MockIDBImpl {
get databaseName(): string { return "mockdb"; }
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined) {}
}

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {IDOMStorage} from "./types";
import {Transaction} from "./Transaction";
import { STORE_NAMES, StoreNames, StorageError } from "../common";
import { reqAsPromise } from "./utils";
@ -29,13 +30,15 @@ export class Storage {
readonly idbFactory: IDBFactory
readonly IDBKeyRange: typeof IDBKeyRange;
readonly storeNames: typeof StoreNames;
readonly localStorage: IDOMStorage;
constructor(idbDatabase: IDBDatabase, idbFactory: IDBFactory, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean, logger: BaseLogger) {
constructor(idbDatabase: IDBDatabase, idbFactory: IDBFactory, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean, localStorage: IDOMStorage, logger: BaseLogger) {
this._db = idbDatabase;
this.idbFactory = idbFactory;
this.IDBKeyRange = _IDBKeyRange;
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
this.storeNames = StoreNames;
this.localStorage = localStorage;
this.logger = logger;
}
@ -79,4 +82,8 @@ export class Storage {
close(): void {
this._db.close();
}
get databaseName(): string {
return this._db.name;
}
}

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {IDOMStorage} from "./types";
import {Storage} from "./Storage";
import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession, Export } from "./export";
@ -23,8 +24,8 @@ import { BaseLogger } from "../../../logging/BaseLogger.js";
import { LogItem } from "../../../logging/LogItem.js";
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log: LogItem) {
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log);
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, localStorage: IDOMStorage, log: LogItem) {
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, localStorage, log);
return openDatabase(sessionName(sessionId), create, schema.length, idbFactory);
}
@ -52,12 +53,14 @@ async function requestPersistedStorage(): Promise<boolean> {
export class StorageFactory {
private _serviceWorkerHandler: ServiceWorkerHandler;
private _idbFactory: IDBFactory;
private _IDBKeyRange: typeof IDBKeyRange
private _IDBKeyRange: typeof IDBKeyRange;
private _localStorage: IDOMStorage;
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, _IDBKeyRange = window.IDBKeyRange) {
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, _IDBKeyRange = window.IDBKeyRange, localStorage: IDOMStorage = window.localStorage) {
this._serviceWorkerHandler = serviceWorkerHandler;
this._idbFactory = idbFactory;
this._IDBKeyRange = _IDBKeyRange;
this._localStorage = localStorage;
}
async create(sessionId: string, log: LogItem): Promise<Storage> {
@ -70,8 +73,8 @@ export class StorageFactory {
});
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, log);
return new Storage(db, this._idbFactory, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug, log.logger);
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log);
return new Storage(db, this._idbFactory, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug, this._localStorage, log.logger);
}
delete(sessionId: string): Promise<IDBDatabase> {
@ -81,21 +84,22 @@ export class StorageFactory {
}
async export(sessionId: string, log: LogItem): Promise<Export> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, log);
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log);
return await exportSession(db);
}
async import(sessionId: string, data: Export, log: LogItem): Promise<void> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, log);
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, this._localStorage, log);
return await importSession(db, data);
}
}
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log: LogItem): Promise<void> {
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, localStorage: IDOMStorage, log: LogItem): Promise<void> {
const startIdx = oldVersion || 0;
return log.wrap({l: "storage migration", oldVersion, version}, async log => {
for(let i = startIdx; i < version; ++i) {
await log.wrap(`v${i + 1}`, log => schema[i](db, txn, log));
const migrationFunc = schema[i];
await log.wrap(`v${i + 1}`, log => migrationFunc(db, txn, localStorage, log));
}
});
}

View file

@ -73,6 +73,10 @@ export class Transaction {
return this._storage.IDBKeyRange;
}
get databaseName(): string {
return this._storage.databaseName;
}
get logger(): BaseLogger {
return this._storage.logger;
}
@ -94,7 +98,7 @@ export class Transaction {
}
get session(): SessionStore {
return this._store(StoreNames.session, idbStore => new SessionStore(idbStore));
return this._store(StoreNames.session, idbStore => new SessionStore(idbStore, this._storage.localStorage));
}
get roomSummary(): RoomSummaryStore {

View file

@ -1,3 +1,4 @@
import {IDOMStorage} from "./types";
import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils";
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
@ -7,10 +8,13 @@ import {RoomStateEntry} from "./stores/RoomStateStore";
import {SessionStore} from "./stores/SessionStore";
import {encodeScopeTypeKey} from "./stores/OperationStore";
import {MAX_UNICODE} from "./stores/common";
import {LogItem} from "../../../logging/LogItem.js";
export type MigrationFunc = (db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) => Promise<void> | void;
// FUNCTIONS SHOULD ONLY BE APPENDED!!
// the index in the array is the database version
export const schema = [
export const schema: MigrationFunc[] = [
createInitialStores,
createMemberStore,
migrateSession,
@ -64,7 +68,7 @@ async function createMemberStore(db: IDBDatabase, txn: IDBTransaction): Promise<
});
}
//v3
async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
async function migrateSession(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage): Promise<void> {
const session = txn.objectStore("session");
try {
const PRE_MIGRATION_KEY = 1;
@ -73,7 +77,7 @@ async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise<voi
session.delete(PRE_MIGRATION_KEY);
const {syncToken, syncFilterId, serverVersions} = entry.value;
// Cast ok here because only "set" is used and we don't look into return
const store = new SessionStore(session as any);
const store = new SessionStore(session as any, localStorage);
store.set("sync", {token: syncToken, filterId: syncFilterId});
store.set("serverVersions", serverVersions);
}
@ -156,7 +160,7 @@ function createTimelineRelationsStore(db: IDBDatabase) : void {
}
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
async function fixMissingRoomsInUserIdentities(db, txn, log) {
async function fixMissingRoomsInUserIdentities(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) {
const roomSummaryStore = txn.objectStore("roomSummary");
const trackedRoomIds: string[] = [];
await iterateCursor<SummaryData>(roomSummaryStore.openCursor(), roomSummary => {

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Store} from "../Store";
import {IDOMStorage} from "../types";
import {SESSION_E2EE_KEY_PREFIX} from "../../../e2ee/common.js";
export interface SessionEntry {
key: string;
@ -22,9 +24,11 @@ export interface SessionEntry {
export class SessionStore {
private _sessionStore: Store<SessionEntry>
private _localStorage: IDOMStorage;
constructor(sessionStore: Store<SessionEntry>) {
constructor(sessionStore: Store<SessionEntry>, localStorage: IDOMStorage) {
this._sessionStore = sessionStore;
this._localStorage = localStorage;
}
async get(key: string): Promise<any> {
@ -34,15 +38,60 @@ export class SessionStore {
}
}
_writeKeyToLocalStorage(key: string, value: any) {
// we backup to localStorage so when idb gets cleared for some reason, we don't lose our e2ee identity
try {
const lsKey = `${this._sessionStore.databaseName}.session.${key}`;
const lsValue = JSON.stringify(value);
this._localStorage.setItem(lsKey, lsValue);
} catch (err) {
console.error("could not write to localStorage", err);
}
}
writeToLocalStorage() {
this._sessionStore.iterateValues(undefined, (value: any, key: string) => {
if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) {
this._writeKeyToLocalStorage(key, value);
}
return false;
});
}
tryRestoreFromLocalStorage(): boolean {
let success = false;
const lsPrefix = `${this._sessionStore.databaseName}.session.`;
const prefix = `${lsPrefix}${SESSION_E2EE_KEY_PREFIX}`;
for(let i = 0; i < this._localStorage.length; i += 1) {
const lsKey = this._localStorage.key(i)!;
if (lsKey.startsWith(prefix)) {
const value = JSON.parse(this._localStorage.getItem(lsKey)!);
const key = lsKey.substr(lsPrefix.length);
this._sessionStore.put({key, value});
success = true;
}
}
return success;
}
set(key: string, value: any): void {
if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) {
this._writeKeyToLocalStorage(key, value);
}
this._sessionStore.put({key, value});
}
add(key: string, value: any): void {
if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) {
this._writeKeyToLocalStorage(key, value);
}
this._sessionStore.add({key, value});
}
remove(key: string): void {
if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) {
this._localStorage.removeItem(this._sessionStore.databaseName + key);
}
this._sessionStore.delete(key);
}
}

View file

@ -0,0 +1,23 @@
/*
Copyright 2021 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.
*/
export interface IDOMStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
key(n: number): string | null;
readonly length: number;
}

View file

@ -16,12 +16,13 @@ limitations under the License.
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
import {IDOMStorage} from "../matrix/storage/idb/types";
import {Storage} from "../matrix/storage/idb/Storage";
import {Instance as nullLogger} from "../logging/NullLogger.js";
import {openDatabase, CreateObjectStore} from "../matrix/storage/idb/utils";
export function createMockStorage(): Promise<Storage> {
return new StorageFactory(null as any, new FDBFactory(), FDBKeyRange).create("1", nullLogger.item);
return new StorageFactory(null as any, new FDBFactory(), FDBKeyRange, new MockLocalStorage()).create("1", nullLogger.item);
}
export function createMockDatabase(name: string, createObjectStore: CreateObjectStore, impl: MockIDBImpl): Promise<IDBDatabase> {
@ -39,3 +40,41 @@ export class MockIDBImpl {
return FDBKeyRange;
}
}
class MockLocalStorage implements IDOMStorage {
private _map: Map<string, string>;
constructor() {
this._map = new Map();
}
getItem(key: string): string | null {
return this._map.get(key) || null;
}
setItem(key: string, value: string) {
this._map.set(key, value);
}
removeItem(key: string): void {
this._map.delete(key);
}
get length(): number {
return this._map.size;
}
key(n: number): string | null {
const it = this._map.keys();
let i = -1;
let result;
while (i < n) {
result = it.next();
if (result.done) {
return null;
}
i += 1;
}
return result?.value || null;
}
}