555 lines
17 KiB
JavaScript
555 lines
17 KiB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
/*
|
|
Copyright 2017 - 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.
|
|
*/
|
|
|
|
import { logger } from "../../logger.js";
|
|
import { MigrationState, SESSION_BATCH_SIZE, ACCOUNT_OBJECT_KEY_MIGRATION_STATE } from "./base.js";
|
|
import { IndexedDBCryptoStore } from "./indexeddb-crypto-store.js";
|
|
const PROFILE_TRANSACTIONS = false;
|
|
|
|
/**
|
|
* Implementation of a CryptoStore which is backed by an existing
|
|
* IndexedDB connection. Generally you want IndexedDBCryptoStore
|
|
* which connects to the database and defers to one of these.
|
|
*
|
|
* @internal
|
|
*/
|
|
export class Backend {
|
|
/**
|
|
*/
|
|
constructor(db) {
|
|
_defineProperty(this, "nextTxnId", 0);
|
|
this.db = db;
|
|
// make sure we close the db on `onversionchange` - otherwise
|
|
// attempts to delete the database will block (and subsequent
|
|
// attempts to re-create it will also block).
|
|
db.onversionchange = () => {
|
|
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
|
|
db.close();
|
|
};
|
|
}
|
|
async containsData() {
|
|
throw Error("Not implemented for Backend");
|
|
}
|
|
async startup() {
|
|
// No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
|
|
// by passing us a ready IDBDatabase instance
|
|
return this;
|
|
}
|
|
async deleteAllData() {
|
|
throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead.");
|
|
}
|
|
|
|
/**
|
|
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
|
*
|
|
* Implementation of {@link CryptoStore.getMigrationState}.
|
|
*/
|
|
async getMigrationState() {
|
|
let migrationState = MigrationState.NOT_STARTED;
|
|
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
|
|
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
|
const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
|
getReq.onsuccess = () => {
|
|
migrationState = getReq.result ?? MigrationState.NOT_STARTED;
|
|
};
|
|
});
|
|
return migrationState;
|
|
}
|
|
|
|
/**
|
|
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
|
*
|
|
* Implementation of {@link CryptoStore.setMigrationState}.
|
|
*/
|
|
async setMigrationState(migrationState) {
|
|
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
|
|
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
|
objectStore.put(migrationState, ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
|
});
|
|
}
|
|
|
|
// Olm Account
|
|
|
|
getAccount(txn, func) {
|
|
const objectStore = txn.objectStore("account");
|
|
const getReq = objectStore.get("-");
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
func(getReq.result || null);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
storeAccount(txn, accountPickle) {
|
|
const objectStore = txn.objectStore("account");
|
|
objectStore.put(accountPickle, "-");
|
|
}
|
|
getCrossSigningKeys(txn, func) {
|
|
const objectStore = txn.objectStore("account");
|
|
const getReq = objectStore.get("crossSigningKeys");
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
func(getReq.result || null);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
getSecretStorePrivateKey(txn, func, type) {
|
|
const objectStore = txn.objectStore("account");
|
|
const getReq = objectStore.get(`ssss_cache:${type}`);
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
func(getReq.result || null);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
storeSecretStorePrivateKey(txn, type, key) {
|
|
const objectStore = txn.objectStore("account");
|
|
objectStore.put(key, `ssss_cache:${type}`);
|
|
}
|
|
|
|
// Olm Sessions
|
|
|
|
countEndToEndSessions(txn, func) {
|
|
const objectStore = txn.objectStore("sessions");
|
|
const countReq = objectStore.count();
|
|
countReq.onsuccess = function () {
|
|
try {
|
|
func(countReq.result);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
getEndToEndSessions(deviceKey, txn, func) {
|
|
const objectStore = txn.objectStore("sessions");
|
|
const idx = objectStore.index("deviceKey");
|
|
const getReq = idx.openCursor(deviceKey);
|
|
const results = {};
|
|
getReq.onsuccess = function () {
|
|
const cursor = getReq.result;
|
|
if (cursor) {
|
|
results[cursor.value.sessionId] = {
|
|
session: cursor.value.session,
|
|
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs
|
|
};
|
|
cursor.continue();
|
|
} else {
|
|
try {
|
|
func(results);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
|
const objectStore = txn.objectStore("sessions");
|
|
const getReq = objectStore.get([deviceKey, sessionId]);
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
if (getReq.result) {
|
|
func({
|
|
session: getReq.result.session,
|
|
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs
|
|
});
|
|
} else {
|
|
func(null);
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
|
const objectStore = txn.objectStore("sessions");
|
|
objectStore.put({
|
|
deviceKey,
|
|
sessionId,
|
|
session: sessionInfo.session,
|
|
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch a batch of Olm sessions from the database.
|
|
*
|
|
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
|
*/
|
|
async getEndToEndSessionsBatch() {
|
|
const result = [];
|
|
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_SESSIONS], txn => {
|
|
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS);
|
|
const getReq = objectStore.openCursor();
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
const cursor = getReq.result;
|
|
if (cursor) {
|
|
result.push(cursor.value);
|
|
if (result.length < SESSION_BATCH_SIZE) {
|
|
cursor.continue();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
});
|
|
if (result.length === 0) {
|
|
// No sessions left.
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Delete a batch of Olm sessions from the database.
|
|
*
|
|
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
|
*
|
|
* @internal
|
|
*/
|
|
async deleteEndToEndSessionsBatch(sessions) {
|
|
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], async txn => {
|
|
try {
|
|
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS);
|
|
for (const {
|
|
deviceKey,
|
|
sessionId
|
|
} of sessions) {
|
|
const req = objectStore.delete([deviceKey, sessionId]);
|
|
await new Promise(resolve => {
|
|
req.onsuccess = resolve;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Inbound group sessions
|
|
|
|
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
|
let session = false;
|
|
let withheld = false;
|
|
const objectStore = txn.objectStore("inbound_group_sessions");
|
|
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
if (getReq.result) {
|
|
session = getReq.result.session;
|
|
} else {
|
|
session = null;
|
|
}
|
|
if (withheld !== false) {
|
|
func(session, withheld);
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
|
|
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
|
|
withheldGetReq.onsuccess = function () {
|
|
try {
|
|
if (withheldGetReq.result) {
|
|
withheld = withheldGetReq.result.session;
|
|
} else {
|
|
withheld = null;
|
|
}
|
|
if (session !== false) {
|
|
func(session, withheld);
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
|
const objectStore = txn.objectStore("inbound_group_sessions");
|
|
objectStore.put({
|
|
senderCurve25519Key,
|
|
sessionId,
|
|
session: sessionData
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Count the number of Megolm sessions in the database.
|
|
*
|
|
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
|
*
|
|
* @internal
|
|
*/
|
|
async countEndToEndInboundGroupSessions() {
|
|
let result = 0;
|
|
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], txn => {
|
|
const sessionStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
|
const countReq = sessionStore.count();
|
|
countReq.onsuccess = () => {
|
|
result = countReq.result;
|
|
};
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Fetch a batch of Megolm sessions from the database.
|
|
*
|
|
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
|
*/
|
|
async getEndToEndInboundGroupSessionsBatch() {
|
|
const result = [];
|
|
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], txn => {
|
|
const sessionStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
|
const backupStore = txn.objectStore(IndexedDBCryptoStore.STORE_BACKUP);
|
|
const getReq = sessionStore.openCursor();
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
const cursor = getReq.result;
|
|
if (cursor) {
|
|
const backupGetReq = backupStore.get(cursor.key);
|
|
backupGetReq.onsuccess = () => {
|
|
result.push({
|
|
senderKey: cursor.value.senderCurve25519Key,
|
|
sessionId: cursor.value.sessionId,
|
|
sessionData: cursor.value.session,
|
|
needsBackup: backupGetReq.result !== undefined
|
|
});
|
|
if (result.length < SESSION_BATCH_SIZE) {
|
|
cursor.continue();
|
|
}
|
|
};
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
});
|
|
if (result.length === 0) {
|
|
// No sessions left.
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Delete a batch of Megolm sessions from the database.
|
|
*
|
|
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
|
*
|
|
* @internal
|
|
*/
|
|
async deleteEndToEndInboundGroupSessionsBatch(sessions) {
|
|
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], async txn => {
|
|
try {
|
|
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS);
|
|
for (const {
|
|
senderKey,
|
|
sessionId
|
|
} of sessions) {
|
|
const req = objectStore.delete([senderKey, sessionId]);
|
|
await new Promise(resolve => {
|
|
req.onsuccess = resolve;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
});
|
|
}
|
|
getEndToEndDeviceData(txn, func) {
|
|
const objectStore = txn.objectStore("device_data");
|
|
const getReq = objectStore.get("-");
|
|
getReq.onsuccess = function () {
|
|
try {
|
|
func(getReq.result || null);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
};
|
|
}
|
|
getEndToEndRooms(txn, func) {
|
|
const rooms = {};
|
|
const objectStore = txn.objectStore("rooms");
|
|
const getReq = objectStore.openCursor();
|
|
getReq.onsuccess = function () {
|
|
const cursor = getReq.result;
|
|
if (cursor) {
|
|
rooms[cursor.key] = cursor.value;
|
|
cursor.continue();
|
|
} else {
|
|
try {
|
|
func(rooms);
|
|
} catch (e) {
|
|
abortWithException(txn, e);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
async markSessionsNeedingBackup(sessions, txn) {
|
|
if (!txn) {
|
|
txn = this.db.transaction("sessions_needing_backup", "readwrite");
|
|
}
|
|
const objectStore = txn.objectStore("sessions_needing_backup");
|
|
await Promise.all(sessions.map(session => {
|
|
return new Promise((resolve, reject) => {
|
|
const req = objectStore.put({
|
|
senderCurve25519Key: session.senderKey,
|
|
sessionId: session.sessionId
|
|
});
|
|
req.onsuccess = resolve;
|
|
req.onerror = reject;
|
|
});
|
|
}));
|
|
}
|
|
doTxn(mode, stores, func, log = logger) {
|
|
let startTime;
|
|
let description;
|
|
if (PROFILE_TRANSACTIONS) {
|
|
const txnId = this.nextTxnId++;
|
|
startTime = Date.now();
|
|
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
|
|
log.debug(`Starting ${description}`);
|
|
}
|
|
const txn = this.db.transaction(stores, mode);
|
|
const promise = promiseifyTxn(txn);
|
|
const result = func(txn);
|
|
if (PROFILE_TRANSACTIONS) {
|
|
promise.then(() => {
|
|
const elapsedTime = Date.now() - startTime;
|
|
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
|
|
}, () => {
|
|
const elapsedTime = Date.now() - startTime;
|
|
log.error(`Failed ${description}, took ${elapsedTime} ms`);
|
|
});
|
|
}
|
|
return promise.then(() => {
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
const DB_MIGRATIONS = [db => {
|
|
createDatabase(db);
|
|
}, db => {
|
|
db.createObjectStore("account");
|
|
}, db => {
|
|
const sessionsStore = db.createObjectStore("sessions", {
|
|
keyPath: ["deviceKey", "sessionId"]
|
|
});
|
|
sessionsStore.createIndex("deviceKey", "deviceKey");
|
|
}, db => {
|
|
db.createObjectStore("inbound_group_sessions", {
|
|
keyPath: ["senderCurve25519Key", "sessionId"]
|
|
});
|
|
}, db => {
|
|
db.createObjectStore("device_data");
|
|
}, db => {
|
|
db.createObjectStore("rooms");
|
|
}, db => {
|
|
db.createObjectStore("sessions_needing_backup", {
|
|
keyPath: ["senderCurve25519Key", "sessionId"]
|
|
});
|
|
}, db => {
|
|
db.createObjectStore("inbound_group_sessions_withheld", {
|
|
keyPath: ["senderCurve25519Key", "sessionId"]
|
|
});
|
|
}, db => {
|
|
const problemsStore = db.createObjectStore("session_problems", {
|
|
keyPath: ["deviceKey", "time"]
|
|
});
|
|
problemsStore.createIndex("deviceKey", "deviceKey");
|
|
db.createObjectStore("notified_error_devices", {
|
|
keyPath: ["userId", "deviceId"]
|
|
});
|
|
}, db => {
|
|
db.createObjectStore("shared_history_inbound_group_sessions", {
|
|
keyPath: ["roomId"]
|
|
});
|
|
}, db => {
|
|
db.createObjectStore("parked_shared_history", {
|
|
keyPath: ["roomId"]
|
|
});
|
|
}
|
|
// Expand as needed.
|
|
];
|
|
export const VERSION = DB_MIGRATIONS.length;
|
|
export function upgradeDatabase(db, oldVersion) {
|
|
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion} to ${VERSION}`);
|
|
DB_MIGRATIONS.forEach((migration, index) => {
|
|
if (oldVersion <= index) migration(db);
|
|
});
|
|
}
|
|
function createDatabase(db) {
|
|
const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", {
|
|
keyPath: "requestId"
|
|
});
|
|
|
|
// we assume that the RoomKeyRequestBody will have room_id and session_id
|
|
// properties, to make the index efficient.
|
|
outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);
|
|
outgoingRoomKeyRequestsStore.createIndex("state", "state");
|
|
}
|
|
/*
|
|
* Aborts a transaction with a given exception
|
|
* The transaction promise will be rejected with this exception.
|
|
*/
|
|
function abortWithException(txn, e) {
|
|
// We cheekily stick our exception onto the transaction object here
|
|
// We could alternatively make the thing we pass back to the app
|
|
// an object containing the transaction and exception.
|
|
txn._mx_abortexception = e;
|
|
try {
|
|
txn.abort();
|
|
} catch {
|
|
// sometimes we won't be able to abort the transaction
|
|
// (ie. if it's aborted or completed)
|
|
}
|
|
}
|
|
function promiseifyTxn(txn) {
|
|
return new Promise((resolve, reject) => {
|
|
txn.oncomplete = () => {
|
|
if (txn._mx_abortexception !== undefined) {
|
|
reject(txn._mx_abortexception);
|
|
return;
|
|
}
|
|
resolve(null);
|
|
};
|
|
txn.onerror = event => {
|
|
if (txn._mx_abortexception !== undefined) {
|
|
reject(txn._mx_abortexception);
|
|
} else {
|
|
logger.log("Error performing indexeddb txn", event);
|
|
reject(txn.error);
|
|
}
|
|
};
|
|
txn.onabort = event => {
|
|
if (txn._mx_abortexception !== undefined) {
|
|
reject(txn._mx_abortexception);
|
|
} else {
|
|
logger.log("Error performing indexeddb txn", event);
|
|
reject(txn.error);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
//# sourceMappingURL=indexeddb-crypto-store-backend.js.map
|