diff --git a/src/matrix/ssss/index.js b/src/matrix/ssss/index.js index 34ee63b8..e1baf9c9 100644 --- a/src/matrix/ssss/index.js +++ b/src/matrix/ssss/index.js @@ -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; } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index f77d7753..14e0bc10 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -2,6 +2,7 @@ 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"; +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"; @@ -25,7 +26,8 @@ export const schema: MigrationFunc[] = [ createArchivedRoomSummaryStore, migrateOperationScopeIndex, createTimelineRelationsStore, - fixMissingRoomsInUserIdentities + fixMissingRoomsInUserIdentities, + changeSSSSKeyPrefix, ]; // TODO: how to deal with git merge conflicts of this array? @@ -204,3 +206,12 @@ async function fixMissingRoomsInUserIdentities(db: IDBDatabase, txn: IDBTransact }); } } + +// 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}); + } +} diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index b811c6d8..2158dc23 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -16,6 +16,8 @@ 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; @@ -42,33 +44,38 @@ export class SessionStore { // 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); + const lsValue = 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) => { + writeE2EEIdentityToLocalStorage() { + this._sessionStore.iterateValues(undefined, (entry: SessionEntry, key: string) => { if (key.startsWith(SESSION_E2EE_KEY_PREFIX)) { - this._writeKeyToLocalStorage(key, value); + this._writeKeyToLocalStorage(key, entry.value); } return false; }); } - tryRestoreFromLocalStorage(): boolean { + async tryRestoreE2EEIdentityFromLocalStorage(log: LogItem): Promise { 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 value = parse(this._localStorage.getItem(lsKey)!); const key = lsKey.substr(lsPrefix.length); - this._sessionStore.put({key, value}); - success = true; + // 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; diff --git a/src/utils/typedJSON.ts b/src/utils/typedJSON.ts new file mode 100644 index 00000000..2a8a3c60 --- /dev/null +++ b/src/utils/typedJSON.ts @@ -0,0 +1,107 @@ +/* +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 { + switch (typeof value) { + case "object": { + if (value === null || Array.isArray(value)) { + return 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; + } + default: + return value; + } +} + +function decodeValue(value: any): any { + switch (typeof value) { + case "object": { + if (value === null || Array.isArray(value)) { + return 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; + } + default: + 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); + } + } +}