Merge pull request #520 from vector-im/bwindels/fix-139
Keep backup of e2ee identity in localStorage when idb gets cleared
This commit is contained in:
commit
f8f4bb4eac
15 changed files with 322 additions and 31 deletions
|
@ -237,6 +237,10 @@ export class Session {
|
|||
return this._sessionBackup;
|
||||
}
|
||||
|
||||
get hasIdentity() {
|
||||
return !!this._e2eeAccount;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async createIdentity(log) {
|
||||
if (this._olm) {
|
||||
|
|
|
@ -233,7 +233,7 @@ export class SessionContainer {
|
|||
platform: this._platform,
|
||||
});
|
||||
await this._session.load(log);
|
||||
if (isNewLogin) {
|
||||
if (!this._session.hasIdentity) {
|
||||
this._status.set(LoadStatus.SessionSetup);
|
||||
await log.wrap("createIdentity", log => this._session.createIdentity(log));
|
||||
}
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
import {KeyDescription, Key} from "./common.js";
|
||||
import {keyFromPassphrase} from "./passphrase.js";
|
||||
import {keyFromRecoveryKey} from "./recoveryKey.js";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
|
||||
|
||||
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;
|
||||
|
||||
async function readDefaultKeyDescription(storage) {
|
||||
const txn = await storage.readTxn([
|
||||
|
@ -35,11 +38,11 @@ async function readDefaultKeyDescription(storage) {
|
|||
}
|
||||
|
||||
export async function writeKey(key, txn) {
|
||||
txn.session.set("ssssKey", {id: key.id, binaryKey: key.binaryKey});
|
||||
txn.session.set(SSSS_KEY, {id: key.id, binaryKey: key.binaryKey});
|
||||
}
|
||||
|
||||
export async function readKey(txn) {
|
||||
const keyData = await txn.session.get("ssssKey");
|
||||
const keyData = await txn.session.get(SSSS_KEY);
|
||||
if (!keyData) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -19,9 +19,12 @@ import {StorageError} from "../common";
|
|||
import {LogItem} from "../../../logging/LogItem.js";
|
||||
import {IDBKey} from "./Transaction";
|
||||
|
||||
// this is the part of the Transaction class API that is used here and in the Store subclass,
|
||||
// to make it easier to replace it with alternative implementations in schema.ts and unit tests
|
||||
export interface ITransaction {
|
||||
idbFactory: IDBFactory;
|
||||
IDBKeyRange: typeof IDBKeyRange;
|
||||
databaseName: string;
|
||||
addWriteError(error: StorageError, refItem: LogItem | undefined, operationName: string, keys: IDBKey[] | undefined);
|
||||
}
|
||||
|
||||
|
@ -55,6 +58,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 +276,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) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -33,10 +33,10 @@ export class IDBError extends StorageError {
|
|||
storeName: string;
|
||||
databaseName: string;
|
||||
|
||||
constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) {
|
||||
const source = "source" in sourceOrCursor ? sourceOrCursor.source : sourceOrCursor;
|
||||
const storeName = _sourceName(source);
|
||||
const databaseName = _sourceDatabase(source);
|
||||
constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore | null, cause: DOMException | null = null) {
|
||||
const source = (sourceOrCursor && "source" in sourceOrCursor) ? sourceOrCursor.source : sourceOrCursor;
|
||||
const storeName = source ? _sourceName(source) : "";
|
||||
const databaseName = source ? _sourceDatabase(source) : "";
|
||||
let fullMessage = `${message} on ${databaseName}.${storeName}`;
|
||||
if (cause) {
|
||||
fullMessage += ": ";
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
import {IDOMStorage} from "./types";
|
||||
import {ITransaction} from "./QueryTarget";
|
||||
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";
|
||||
import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js";
|
||||
import {SummaryData} from "../../room/RoomSummary";
|
||||
import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore";
|
||||
import {RoomStateEntry} from "./stores/RoomStateStore";
|
||||
import {SessionStore} from "./stores/SessionStore";
|
||||
import {Store} from "./Store";
|
||||
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,
|
||||
|
@ -21,7 +28,9 @@ export const schema = [
|
|||
createArchivedRoomSummaryStore,
|
||||
migrateOperationScopeIndex,
|
||||
createTimelineRelationsStore,
|
||||
fixMissingRoomsInUserIdentities
|
||||
fixMissingRoomsInUserIdentities,
|
||||
changeSSSSKeyPrefix,
|
||||
backupAndRestoreE2EEAccountToLocalStorage
|
||||
];
|
||||
// TODO: how to deal with git merge conflicts of this array?
|
||||
|
||||
|
@ -64,7 +73,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 +82,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 +165,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 => {
|
||||
|
@ -200,3 +209,37 @@ async function fixMissingRoomsInUserIdentities(db, txn, log) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// v12 move ssssKey to e2ee:ssssKey so it will get backed up in the next step
|
||||
async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) {
|
||||
const session = txn.objectStore("session");
|
||||
const ssssKey = await reqAsPromise(session.get("ssssKey"));
|
||||
if (ssssKey) {
|
||||
session.put({key: `${SESSION_E2EE_KEY_PREFIX}ssssKey`, value: ssssKey});
|
||||
}
|
||||
}
|
||||
// v13
|
||||
async function backupAndRestoreE2EEAccountToLocalStorage(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: LogItem) {
|
||||
const session = txn.objectStore("session");
|
||||
// the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction),
|
||||
// the only thing we should need here is the databaseName though, so we mock it out.
|
||||
// ideally we should have an easier way to go from the idb primitive layer to the specific store classes where
|
||||
// we implement logic, but for now we need this.
|
||||
const databaseNameHelper: ITransaction = {
|
||||
databaseName: db.name,
|
||||
get idbFactory(): IDBFactory { throw new Error("unused");},
|
||||
get IDBKeyRange(): typeof IDBKeyRange { throw new Error("unused");},
|
||||
addWriteError() {},
|
||||
};
|
||||
const sessionStore = new SessionStore(new Store(session, databaseNameHelper), localStorage);
|
||||
// if we already have an e2ee identity, write a backup to local storage.
|
||||
// further updates to e2ee keys in the session store will also write to local storage from 0.2.15 on,
|
||||
// but here we make sure a backup is immediately created after installing the update and we don't wait until
|
||||
// the olm account needs to change
|
||||
sessionStore.writeE2EEIdentityToLocalStorage();
|
||||
// and if we already have a backup, restore it now for any missing key in idb.
|
||||
// this will restore the backup every time the idb database is dropped as it will
|
||||
// run through all the migration steps when recreating it.
|
||||
const restored = await sessionStore.tryRestoreE2EEIdentityFromLocalStorage(log);
|
||||
log.set("restored", restored);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ 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";
|
||||
import {LogItem} from "../../../../logging/LogItem.js";
|
||||
import {parse, stringify} from "../../../../utils/typedJSON";
|
||||
|
||||
export interface SessionEntry {
|
||||
key: string;
|
||||
|
@ -22,9 +26,15 @@ 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;
|
||||
}
|
||||
|
||||
private get _localStorageKeyPrefix(): string {
|
||||
return `${this._sessionStore.databaseName}.session.`;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
|
@ -34,15 +44,65 @@ 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._localStorageKeyPrefix + key;
|
||||
const lsValue = stringify(value);
|
||||
this._localStorage.setItem(lsKey, lsValue);
|
||||
} catch (err) {
|
||||
console.error("could not write to localStorage", err);
|
||||
}
|
||||
}
|
||||
|
||||
writeE2EEIdentityToLocalStorage() {
|
||||
this._sessionStore.iterateValues(undefined, (entry: SessionEntry, key: string) => {
|
||||
if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) {
|
||||
this._writeKeyToLocalStorage(key, entry.value);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async tryRestoreE2EEIdentityFromLocalStorage(log: LogItem): Promise<boolean> {
|
||||
let success = false;
|
||||
const lsPrefix = this._localStorageKeyPrefix;
|
||||
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 = parse(this._localStorage.getItem(lsKey)!);
|
||||
const key = lsKey.substr(lsPrefix.length);
|
||||
// we check if we don't have this key already, as we don't want to override anything
|
||||
const hasKey = (await this._sessionStore.getKey(key)) === key;
|
||||
log.set(key, !hasKey);
|
||||
if (!hasKey) {
|
||||
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._localStorageKeyPrefix + key);
|
||||
}
|
||||
this._sessionStore.delete(key);
|
||||
}
|
||||
}
|
||||
|
|
23
src/matrix/storage/idb/types.ts
Normal file
23
src/matrix/storage/idb/types.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
96
src/utils/typedJSON.ts
Normal file
96
src/utils/typedJSON.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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 function stringify(value: any): string {
|
||||
return JSON.stringify(encodeValue(value));
|
||||
}
|
||||
|
||||
export function parse(value: string): any {
|
||||
return decodeValue(JSON.parse(value));
|
||||
}
|
||||
|
||||
function encodeValue(value: any): any {
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
// TypedArray
|
||||
if (value.byteLength) {
|
||||
return {_type: value.constructor.name, value: Array.from(value)};
|
||||
}
|
||||
let newObj = {};
|
||||
for (const prop in value) {
|
||||
if (value.hasOwnProperty(prop)) {
|
||||
newObj[prop] = encodeValue(value[prop]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeValue(value: any): any {
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
if (typeof value._type === "string") {
|
||||
switch (value._type) {
|
||||
case "Int8Array": return Int8Array.from(value.value);
|
||||
case "Uint8Array": return Uint8Array.from(value.value);
|
||||
case "Uint8ClampedArray": return Uint8ClampedArray.from(value.value);
|
||||
case "Int16Array": return Int16Array.from(value.value);
|
||||
case "Uint16Array": return Uint16Array.from(value.value);
|
||||
case "Int32Array": return Int32Array.from(value.value);
|
||||
case "Uint32Array": return Uint32Array.from(value.value);
|
||||
case "Float32Array": return Float32Array.from(value.value);
|
||||
case "Float64Array": return Float64Array.from(value.value);
|
||||
case "BigInt64Array": return BigInt64Array.from(value.value);
|
||||
case "BigUint64Array": return BigUint64Array.from(value.value);
|
||||
default:
|
||||
return value.value;
|
||||
}
|
||||
}
|
||||
let newObj = {};
|
||||
for (const prop in value) {
|
||||
if (value.hasOwnProperty(prop)) {
|
||||
newObj[prop] = decodeValue(value[prop]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"Uint8Array and primitives": assert => {
|
||||
const value = {
|
||||
foo: "bar",
|
||||
bar: 5,
|
||||
baz: false,
|
||||
fuzz: new Uint8Array([3, 1, 2])
|
||||
};
|
||||
const serialized = stringify(value);
|
||||
assert.strictEqual(typeof serialized, "string");
|
||||
const deserialized = parse(serialized);
|
||||
assert.strictEqual(deserialized.foo, "bar");
|
||||
assert.strictEqual(deserialized.bar, 5);
|
||||
assert.strictEqual(deserialized.baz, false);
|
||||
assert(deserialized.fuzz instanceof Uint8Array);
|
||||
assert.strictEqual(deserialized.fuzz.length, 3);
|
||||
assert.strictEqual(deserialized.fuzz[0], 3);
|
||||
assert.strictEqual(deserialized.fuzz[1], 1);
|
||||
assert.strictEqual(deserialized.fuzz[2], 2);
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue