feat: complete Ancestor quote bot with production-ready Docker support
This commit is contained in:
317
node_modules/matrix-js-sdk/src/crypto/store/base.ts
generated
vendored
317
node_modules/matrix-js-sdk/src/crypto/store/base.ts
generated
vendored
@@ -14,67 +14,66 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Logger } from "../../logger.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
import { type AESEncryptedSecretStoragePayload } from "../../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
import { type ISignatures } from "../../@types/signed.ts";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { TrackingStatus } from "../DeviceList";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { IDevice } from "../deviceinfo";
|
||||
import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { DehydrationManager } from "../dehydration";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
/**
|
||||
* Internal module. Definitions for storage for the crypto module
|
||||
*/
|
||||
|
||||
export interface SecretStorePrivateKeys {
|
||||
"m.megolm_backup.v1": AESEncryptedSecretStoragePayload;
|
||||
"dehydration": {
|
||||
keyInfo: DehydrationManager["keyInfo"];
|
||||
key: IEncryptedPayload;
|
||||
deviceDisplayName: string;
|
||||
time: number;
|
||||
} | null;
|
||||
"m.megolm_backup.v1": IEncryptedPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstraction of things that can store data required for end-to-end encryption
|
||||
*/
|
||||
export interface CryptoStore {
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Unlike the rest of the methods in this interface, can be called before {@link CryptoStore#startup}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
containsData(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Initialise this crypto store.
|
||||
*
|
||||
* Typically, this involves provisioning storage, and migrating any existing data to the current version of the
|
||||
* storage schema where appropriate.
|
||||
*
|
||||
* Must be called before any of the rest of the methods in this interface.
|
||||
*/
|
||||
startup(): Promise<CryptoStore>;
|
||||
|
||||
deleteAllData(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
getMigrationState(): Promise<MigrationState>;
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
setMigrationState(migrationState: MigrationState): Promise<void>;
|
||||
getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest>;
|
||||
getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null>;
|
||||
getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null>;
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]>;
|
||||
getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]>;
|
||||
updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null>;
|
||||
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
|
||||
|
||||
// Olm Account
|
||||
getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
|
||||
storeAccount(txn: unknown, accountPickle: string): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void;
|
||||
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
|
||||
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
func: (key: SecretStorePrivateKeys[K] | null) => void,
|
||||
type: K,
|
||||
): void;
|
||||
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
|
||||
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
@@ -94,25 +93,11 @@ export interface CryptoStore {
|
||||
txn: unknown,
|
||||
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
|
||||
): void;
|
||||
|
||||
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void;
|
||||
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void;
|
||||
|
||||
/**
|
||||
* Get a batch of end-to-end sessions from the database.
|
||||
*
|
||||
* @returns A batch of Olm Sessions, or `null` if no sessions are left.
|
||||
* @internal
|
||||
*/
|
||||
getEndToEndSessionsBatch(): Promise<ISessionInfo[] | null>;
|
||||
|
||||
/**
|
||||
* Delete a batch of end-to-end sessions from the database.
|
||||
*
|
||||
* Any sessions in the list which are not found are silently ignored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
deleteEndToEndSessionsBatch(sessions: { deviceKey?: string; sessionId?: string }[]): Promise<void>;
|
||||
storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void>;
|
||||
getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null>;
|
||||
filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]>;
|
||||
|
||||
// Inbound Group Sessions
|
||||
getEndToEndInboundGroupSession(
|
||||
@@ -121,60 +106,55 @@ export interface CryptoStore {
|
||||
txn: unknown,
|
||||
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
|
||||
): void;
|
||||
getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void;
|
||||
addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void;
|
||||
storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
countEndToEndInboundGroupSessions(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get a batch of Megolm sessions from the database.
|
||||
*
|
||||
* @returns A batch of Megolm Sessions, or `null` if no sessions are left.
|
||||
* @internal
|
||||
*/
|
||||
getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null>;
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Any sessions in the list which are not found are silently ignored.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
deleteEndToEndInboundGroupSessionsBatch(sessions: { senderKey: string; sessionId: string }[]): Promise<void>;
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void;
|
||||
|
||||
// Device Data
|
||||
getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void;
|
||||
storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void;
|
||||
storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void;
|
||||
getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void;
|
||||
getSessionsNeedingBackup(limit: number): Promise<ISession[]>;
|
||||
countSessionsNeedingBackup(txn?: unknown): Promise<number>;
|
||||
unmarkSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
|
||||
markSessionsNeedingBackup(sessions: ISession[], txn?: unknown): Promise<void>;
|
||||
addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string, txn?: unknown): void;
|
||||
getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||
|
||||
// Session key backups
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: Logger): Promise<T>;
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||
}
|
||||
|
||||
export type Mode = "readonly" | "readwrite";
|
||||
|
||||
/** Data on a Megolm session */
|
||||
export interface ISession {
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
sessionData?: InboundGroupSessionData;
|
||||
}
|
||||
|
||||
/** Extended data on a Megolm session */
|
||||
export interface SessionExtended extends ISession {
|
||||
needsBackup: boolean;
|
||||
}
|
||||
|
||||
/** Data on an Olm session */
|
||||
export interface ISessionInfo {
|
||||
deviceKey?: string;
|
||||
sessionId?: string;
|
||||
@@ -195,7 +175,14 @@ export interface IDeviceData {
|
||||
syncToken?: string;
|
||||
}
|
||||
|
||||
export interface IProblem {
|
||||
type: string;
|
||||
fixed: boolean;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface IWithheld {
|
||||
// eslint-disable-next-line camelcase
|
||||
room_id: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
@@ -229,153 +216,11 @@ export interface OutgoingRoomKeyRequest {
|
||||
state: RoomKeyRequestState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys for the `account` object store to store the migration state.
|
||||
* Values are defined in `MigrationState`.
|
||||
* @internal
|
||||
*/
|
||||
export const ACCOUNT_OBJECT_KEY_MIGRATION_STATE = "migrationState";
|
||||
|
||||
/**
|
||||
* A record of which steps have been completed in the libolm to Rust Crypto migration.
|
||||
*
|
||||
* Used by {@link CryptoStore#getMigrationState} and {@link CryptoStore#setMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export enum MigrationState {
|
||||
/** No migration steps have yet been completed. */
|
||||
NOT_STARTED,
|
||||
|
||||
/** We have migrated the account data, cross-signing keys, etc. */
|
||||
INITIAL_DATA_MIGRATED,
|
||||
|
||||
/** INITIAL_DATA_MIGRATED, and in addition, we have migrated all the Olm sessions. */
|
||||
OLM_SESSIONS_MIGRATED,
|
||||
|
||||
/** OLM_SESSIONS_MIGRATED, and in addition, we have migrated all the Megolm sessions. */
|
||||
MEGOLM_SESSIONS_MIGRATED,
|
||||
|
||||
/** MEGOLM_SESSIONS_MIGRATED, and in addition, we have migrated all the room settings. */
|
||||
ROOM_SETTINGS_MIGRATED,
|
||||
|
||||
/** ROOM_SETTINGS_MIGRATED, and in addition, we have done the first own keys query in order to
|
||||
* load the public part of the keys that have been migrated */
|
||||
INITIAL_OWN_KEY_QUERY_DONE,
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of batches to be returned by {@link CryptoStore#getEndToEndSessionsBatch} and
|
||||
* {@link CryptoStore#getEndToEndInboundGroupSessionsBatch}.
|
||||
*/
|
||||
export const SESSION_BATCH_SIZE = 50;
|
||||
|
||||
export interface InboundGroupSessionData {
|
||||
room_id: string;
|
||||
/** pickled Olm.InboundGroupSession */
|
||||
session: string;
|
||||
keysClaimed?: Record<string, string>;
|
||||
/** Devices involved in forwarding this session to us (normally empty). */
|
||||
export interface ParkedSharedHistory {
|
||||
senderId: string;
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
|
||||
forwardingCurve25519KeyChain: string[];
|
||||
/** whether this session is untrusted. */
|
||||
untrusted?: boolean;
|
||||
/** whether this session exists during the room being set to shared history. */
|
||||
sharedHistory?: boolean;
|
||||
}
|
||||
|
||||
export interface ICrossSigningInfo {
|
||||
keys: Record<string, CrossSigningKeyInfo>;
|
||||
firstUse: boolean;
|
||||
crossSigningVerifiedBefore: boolean;
|
||||
}
|
||||
|
||||
export interface IRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
}
|
||||
export enum TrackingStatus {
|
||||
NotTracked,
|
||||
PendingDownload,
|
||||
DownloadInProgress,
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
* The state machine looks like:
|
||||
* ```
|
||||
*
|
||||
* | (cancellation sent)
|
||||
* | .-------------------------------------------------.
|
||||
* | | |
|
||||
* V V (cancellation requested) |
|
||||
* UNSENT -----------------------------+ |
|
||||
* | | |
|
||||
* | | |
|
||||
* | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* V | Λ
|
||||
* SENT | |
|
||||
* |-------------------------------- | --------------'
|
||||
* | | (cancellation requested with intent
|
||||
* | | to resend the original request)
|
||||
* | |
|
||||
* | (cancellation requested) |
|
||||
* V |
|
||||
* CANCELLATION_PENDING |
|
||||
* | |
|
||||
* | (cancellation sent) |
|
||||
* V |
|
||||
* (deleted) <---------------------------+
|
||||
* ```
|
||||
*/
|
||||
export enum RoomKeyRequestState {
|
||||
/** request not yet sent */
|
||||
Unsent,
|
||||
/** request sent, awaiting reply */
|
||||
Sent,
|
||||
/** reply received, cancellation not yet sent */
|
||||
CancellationPending,
|
||||
/**
|
||||
* Cancellation not yet sent and will transition to UNSENT instead of
|
||||
* being deleted once the cancellation has been sent.
|
||||
*/
|
||||
CancellationPendingAndWillResend,
|
||||
}
|
||||
|
||||
interface IRoomKey {
|
||||
room_id: string;
|
||||
algorithm: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters of a room key request. The details of the request may
|
||||
* vary with the crypto algorithm, but the management and storage layers for
|
||||
* outgoing requests expect it to have 'room_id' and 'session_id' properties.
|
||||
*/
|
||||
export interface IRoomKeyRequestBody extends IRoomKey {
|
||||
session_id: string;
|
||||
sender_key: string;
|
||||
}
|
||||
|
||||
export interface IRoomKeyRequestRecipient {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
interface IDevice {
|
||||
keys: Record<string, string>;
|
||||
algorithms: string[];
|
||||
verified: DeviceVerification;
|
||||
known: boolean;
|
||||
unsigned?: Record<string, any>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/** State of the verification of the device. */
|
||||
export enum DeviceVerification {
|
||||
Blocked = -1,
|
||||
Unverified = 0,
|
||||
Verified = 1,
|
||||
}
|
||||
|
||||
774
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
774
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store-backend.ts
generated
vendored
@@ -14,24 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Logger, logger } from "../../logger.ts";
|
||||
import { logger, PrefixedLogger } from "../../logger";
|
||||
import { deepCompare } from "../../utils";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type IDeviceData,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { IndexedDBCryptoStore } from "./indexeddb-crypto-store.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
@@ -39,8 +40,6 @@ 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 implements CryptoStore {
|
||||
private nextTxnId = 0;
|
||||
@@ -57,49 +56,306 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public async containsData(): Promise<boolean> {
|
||||
throw Error("Not implemented for Backend");
|
||||
}
|
||||
|
||||
public async startup(): Promise<CryptoStore> {
|
||||
// No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore)
|
||||
// by passing us a ready IDBDatabase instance
|
||||
return this;
|
||||
}
|
||||
|
||||
public async deleteAllData(): Promise<void> {
|
||||
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.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
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 = (): void => {
|
||||
migrationState = getReq.result ?? MigrationState.NOT_STARTED;
|
||||
};
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
|
||||
txn.oncomplete = (): void => {
|
||||
resolve(request);
|
||||
};
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
});
|
||||
return migrationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
await this.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
objectStore.put(migrationState, ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = reject;
|
||||
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
resolve(existing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* look for an existing room key request in the db
|
||||
*
|
||||
* @internal
|
||||
* @param txn - database transaction
|
||||
* @param requestBody - existing request to look for
|
||||
* @param callback - function to call with the results of the
|
||||
* search. Either passed a matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getOutgoingRoomKeyRequest(
|
||||
txn: IDBTransaction,
|
||||
requestBody: IRoomKeyRequestBody,
|
||||
callback: (req: OutgoingRoomKeyRequest | null) => void,
|
||||
): void {
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const idx = store.index("session");
|
||||
const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);
|
||||
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = cursor.value;
|
||||
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
// got a match
|
||||
callback(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// look at the next entry in the index
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states. If there are multiple
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
if (wantedStates.length === 0) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// this is a bit tortuous because we need to make sure we do the lookup
|
||||
// in a single transaction, to avoid having a race with the insertion
|
||||
// code.
|
||||
|
||||
// index into the wantedStates array
|
||||
let stateIndex = 0;
|
||||
let result: OutgoingRoomKeyRequest;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
// got a match
|
||||
result = cursor.value;
|
||||
return;
|
||||
}
|
||||
|
||||
// try the next state in the list
|
||||
stateIndex++;
|
||||
if (stateIndex >= wantedStates.length) {
|
||||
// no matches
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = store.index("state").openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns All elements in a given state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
const index = store.index("state");
|
||||
const request = index.getAll(wantedState);
|
||||
|
||||
request.onsuccess = (): void => resolve(request.result);
|
||||
request.onerror = (): void => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
let stateIndex = 0;
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (
|
||||
keyReq.recipients.some(
|
||||
(recipient: IRoomKeyRequestRecipient) =>
|
||||
recipient.userId === userId && recipient.deviceId === deviceId,
|
||||
)
|
||||
) {
|
||||
results.push(keyReq);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
// try the next state in the list
|
||||
stateIndex++;
|
||||
if (stateIndex >= wantedStates.length) {
|
||||
// no matches
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = (this.source as IDBIndex).openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
}
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
|
||||
const wantedState = wantedStates[stateIndex];
|
||||
const cursorReq = store.index("state").openCursor(wantedState);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
|
||||
return promiseifyTxn(txn).then(() => results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
let result: OutgoingRoomKeyRequest | null = null;
|
||||
|
||||
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
|
||||
const cursor = this.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${data.state}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
Object.assign(data, updates);
|
||||
cursor.update(data);
|
||||
result = data;
|
||||
}
|
||||
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
|
||||
cursorReq.onsuccess = onsuccess;
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
if (data.state != expectedState) {
|
||||
logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`);
|
||||
return;
|
||||
}
|
||||
cursor.delete();
|
||||
};
|
||||
return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
|
||||
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
|
||||
@@ -121,7 +377,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("crossSigningKeys");
|
||||
@@ -150,6 +406,11 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: IDBTransaction,
|
||||
type: K,
|
||||
@@ -224,6 +485,24 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
func(null);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndSession(
|
||||
deviceKey: string,
|
||||
sessionId: string,
|
||||
@@ -239,60 +518,74 @@ export class Backend implements CryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
await this.doTxn("readonly", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
|
||||
const objectStore = txn.objectStore(IndexedDBCryptoStore.STORE_SESSIONS);
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
result.push(cursor.value);
|
||||
if (result.length < SESSION_BATCH_SIZE) {
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
};
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const txn = this.db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
objectStore.put({
|
||||
deviceKey,
|
||||
type,
|
||||
fixed,
|
||||
time: Date.now(),
|
||||
});
|
||||
await promiseifyTxn(txn);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
let result: IProblem | null = null;
|
||||
const txn = this.db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
const index = objectStore.index("deviceKey");
|
||||
const req = index.getAll(deviceKey);
|
||||
req.onsuccess = (): void => {
|
||||
const problems = req.result;
|
||||
if (!problems.length) {
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
result = Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
result = null;
|
||||
} else {
|
||||
result = lastProblem;
|
||||
}
|
||||
};
|
||||
await promiseifyTxn(txn);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
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, <Error>e);
|
||||
}
|
||||
});
|
||||
// FIXME: we should probably prune this when devices get deleted
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const txn = this.db.transaction("notified_error_devices", "readwrite");
|
||||
const objectStore = txn.objectStore("notified_error_devices");
|
||||
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
await Promise.all(
|
||||
devices.map((device) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const { userId, deviceInfo } = device;
|
||||
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
|
||||
getReq.onsuccess = function (): void {
|
||||
if (!getReq.result) {
|
||||
objectStore.put({ userId, deviceId: deviceInfo.deviceId });
|
||||
ret.push(device);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -340,6 +633,57 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
try {
|
||||
func({
|
||||
senderKey: cursor.value.senderCurve25519Key,
|
||||
sessionId: cursor.value.sessionId,
|
||||
sessionData: cursor.value.session,
|
||||
});
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
func(null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, <Error>e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const addReq = objectStore.add({
|
||||
senderCurve25519Key,
|
||||
sessionId,
|
||||
session: sessionData,
|
||||
});
|
||||
addReq.onerror = (ev): void => {
|
||||
if (addReq.error?.name === "ConstraintError") {
|
||||
// This stops the error from triggering the txn's onerror
|
||||
ev.stopPropagation();
|
||||
// ...and this stops it from aborting the transaction
|
||||
ev.preventDefault();
|
||||
logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
|
||||
} else {
|
||||
abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
@@ -354,94 +698,17 @@ export class Backend implements CryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
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 = (): void => {
|
||||
result = countReq.result;
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<null | SessionExtended[]> {
|
||||
const result: SessionExtended[] = [];
|
||||
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 (): void {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const backupGetReq = backupStore.get(cursor.key);
|
||||
backupGetReq.onsuccess = (): void => {
|
||||
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, <Error>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
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
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, <Error>e);
|
||||
}
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
objectStore.put({
|
||||
senderCurve25519Key,
|
||||
sessionId,
|
||||
session: sessionData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -457,6 +724,16 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
|
||||
const objectStore = txn.objectStore("device_data");
|
||||
objectStore.put(deviceData, "-");
|
||||
}
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
objectStore.put(roomInfo, roomId);
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
|
||||
const objectStore = txn.objectStore("rooms");
|
||||
@@ -476,6 +753,67 @@ export class Backend implements CryptoStore {
|
||||
};
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = function (): void {
|
||||
resolve(sessions);
|
||||
};
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
const sessionStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function (): void {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
const sessionGetReq = sessionStore.get(cursor.key);
|
||||
sessionGetReq.onsuccess = function (): void {
|
||||
sessions.push({
|
||||
senderKey: sessionGetReq.result.senderCurve25519Key,
|
||||
sessionId: sessionGetReq.result.sessionId,
|
||||
sessionData: sessionGetReq.result.session,
|
||||
});
|
||||
};
|
||||
if (!limit || sessions.length < limit) {
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("sessions_needing_backup", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("sessions_needing_backup");
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = objectStore.count();
|
||||
req.onerror = reject;
|
||||
req.onsuccess = (): void => resolve(req.result);
|
||||
});
|
||||
}
|
||||
|
||||
public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
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.delete([session.senderKey, session.sessionId]);
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = reject;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("sessions_needing_backup", "readwrite");
|
||||
@@ -495,11 +833,80 @@ export class Backend implements CryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
public addSharedHistoryInboundGroupSession(
|
||||
roomId: string,
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("shared_history_inbound_group_sessions", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({ roomId, sessions });
|
||||
};
|
||||
}
|
||||
|
||||
public getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<[senderKey: string, sessionId: string][]> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("shared_history_inbound_group_sessions", "readonly");
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = (): void => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
resolve(sessions);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("parked_shared_history", "readwrite");
|
||||
}
|
||||
const objectStore = txn.objectStore("parked_shared_history");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = (): void => {
|
||||
const { parked } = req.result || { parked: [] };
|
||||
parked.push(parkedData);
|
||||
objectStore.put({ roomId, parked });
|
||||
};
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
|
||||
if (!txn) {
|
||||
txn = this.db.transaction("parked_shared_history", "readwrite");
|
||||
}
|
||||
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
|
||||
return new Promise((resolve, reject) => {
|
||||
cursorReq.onsuccess = (): void => {
|
||||
const cursor = cursorReq.result;
|
||||
if (!cursor) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
const data = cursor.value;
|
||||
cursor.delete();
|
||||
resolve(data);
|
||||
};
|
||||
cursorReq.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public doTxn<T>(
|
||||
mode: Mode,
|
||||
stores: string | string[],
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log: Logger = logger,
|
||||
log: PrefixedLogger = logger,
|
||||
): Promise<T> {
|
||||
let startTime: number;
|
||||
let description: string;
|
||||
@@ -590,7 +997,7 @@ const DB_MIGRATIONS: DbMigration[] = [
|
||||
export const VERSION = DB_MIGRATIONS.length;
|
||||
|
||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion} to ${VERSION}`);
|
||||
logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`);
|
||||
DB_MIGRATIONS.forEach((migration, index) => {
|
||||
if (oldVersion <= index) migration(db);
|
||||
});
|
||||
@@ -607,7 +1014,7 @@ function createDatabase(db: IDBDatabase): void {
|
||||
}
|
||||
|
||||
interface IWrappedIDBTransaction extends IDBTransaction {
|
||||
_mx_abortexception: Error;
|
||||
_mx_abortexception: Error; // eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -621,7 +1028,7 @@ function abortWithException(txn: IDBTransaction, e: Error): void {
|
||||
(txn as IWrappedIDBTransaction)._mx_abortexception = e;
|
||||
try {
|
||||
txn.abort();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// sometimes we won't be able to abort the transaction
|
||||
// (ie. if it's aborted or completed)
|
||||
}
|
||||
@@ -632,7 +1039,6 @@ function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
|
||||
txn.oncomplete = (): void => {
|
||||
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
|
||||
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
|
||||
return;
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
435
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
435
node_modules/matrix-js-sdk/src/crypto/store/indexeddb-crypto-store.ts
generated
vendored
@@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger, type Logger } from "../../logger.ts";
|
||||
import { LocalStorageCryptoStore } from "./localStorage-crypto-store.ts";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store.ts";
|
||||
import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend.ts";
|
||||
import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors.ts";
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers.ts";
|
||||
import { logger, PrefixedLogger } from "../../logger";
|
||||
import { LocalStorageCryptoStore } from "./localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store";
|
||||
import * as IndexedDBCryptoStoreBackend from "./indexeddb-crypto-store-backend";
|
||||
import { InvalidCryptoStoreError, InvalidCryptoStoreState } from "../../errors";
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
/*
|
||||
/**
|
||||
* Internal module. indexeddb storage for e2e.
|
||||
*/
|
||||
|
||||
@@ -58,52 +61,6 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to check if a legacy crypto store exists and has not been migrated.
|
||||
* Returns true if the store exists and has not been migrated, false otherwise.
|
||||
*/
|
||||
public static existsAndIsNotMigrated(indexedDb: IDBFactory, dbName: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let exists = true;
|
||||
const openDBRequest = indexedDb.open(dbName);
|
||||
openDBRequest.onupgradeneeded = (): void => {
|
||||
// Since we did not provide an explicit version when opening, this event
|
||||
// should only fire if the DB did not exist before at any version.
|
||||
exists = false;
|
||||
};
|
||||
openDBRequest.onblocked = (): void => reject(openDBRequest.error);
|
||||
openDBRequest.onsuccess = (): void => {
|
||||
const db = openDBRequest.result;
|
||||
if (!exists) {
|
||||
db.close();
|
||||
// The DB did not exist before, but has been created as part of this
|
||||
// existence check. Delete it now to restore previous state. Delete can
|
||||
// actually take a while to complete in some browsers, so don't wait for
|
||||
// it. This won't block future open calls that a store might issue next to
|
||||
// properly set up the DB.
|
||||
indexedDb.deleteDatabase(dbName);
|
||||
resolve(false);
|
||||
} else {
|
||||
const tx = db.transaction([IndexedDBCryptoStore.STORE_ACCOUNT], "readonly");
|
||||
const objectStore = tx.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
const getReq = objectStore.get(ACCOUNT_OBJECT_KEY_MIGRATION_STATE);
|
||||
|
||||
getReq.onsuccess = (): void => {
|
||||
const migrationState = getReq.result ?? MigrationState.NOT_STARTED;
|
||||
resolve(migrationState === MigrationState.NOT_STARTED);
|
||||
};
|
||||
|
||||
getReq.onerror = (): void => {
|
||||
reject(getReq.error);
|
||||
};
|
||||
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
openDBRequest.onerror = (): void => reject(openDBRequest.error);
|
||||
});
|
||||
}
|
||||
|
||||
private backendPromise?: Promise<CryptoStore>;
|
||||
private backend?: CryptoStore;
|
||||
|
||||
@@ -113,21 +70,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* @param indexedDB - global indexedDB instance
|
||||
* @param dbName - name of db to connect to
|
||||
*/
|
||||
public constructor(
|
||||
private readonly indexedDB: IDBFactory,
|
||||
private readonly dbName: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
return IndexedDBCryptoStore.exists(this.indexedDB, this.dbName);
|
||||
}
|
||||
public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
@@ -199,15 +142,14 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
// in this db so we should use it or nothing at all.
|
||||
throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew);
|
||||
}
|
||||
logger.warn(`unable to connect to indexeddb ${this.dbName}: falling back to localStorage store: ${e}`);
|
||||
logger.warn(
|
||||
`unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`,
|
||||
);
|
||||
|
||||
try {
|
||||
if (!(globalThis.localStorage instanceof Storage)) {
|
||||
throw new Error("localStorage is not available");
|
||||
}
|
||||
return new LocalStorageCryptoStore(globalThis.localStorage);
|
||||
return new LocalStorageCryptoStore(global.localStorage);
|
||||
} catch (e) {
|
||||
logger.warn(`Unable to open localStorage: falling back to in-memory store: ${e}`);
|
||||
logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
|
||||
return new MemoryCryptoStore();
|
||||
}
|
||||
})
|
||||
@@ -256,25 +198,107 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public getMigrationState(): Promise<MigrationState> {
|
||||
return this.backend!.getMigrationState();
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
return this.backend!.getOrAddOutgoingRoomKeyRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
return this.backend!.setMigrationState(migrationState);
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.getOutgoingRoomKeyRequest(requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states. If there are multiple
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state –
|
||||
* unlike above, return a list of all entries in one state.
|
||||
*
|
||||
* @returns Returns an array of requests in the given state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by target device and state
|
||||
*
|
||||
* @param userId - Target user ID
|
||||
* @param deviceId - Target device ID
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to a list of all the
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return this.backend!.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.updateOutgoingRoomKeyRequest(requestId, expectedState, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -311,7 +335,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
*/
|
||||
public getCrossSigningKeys(
|
||||
txn: IDBTransaction,
|
||||
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
|
||||
func: (keys: Record<string, ICrossSigningKey> | null) => void,
|
||||
): void {
|
||||
this.backend!.getCrossSigningKeys(txn, func);
|
||||
}
|
||||
@@ -329,6 +353,16 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getSecretStorePrivateKey(txn, func, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing keys back to the store
|
||||
*
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param keys - keys object as getCrossSigningKeys()
|
||||
*/
|
||||
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
|
||||
this.backend!.storeCrossSigningKeys(txn, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing private keys back to the store
|
||||
*
|
||||
@@ -395,6 +429,17 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndSessions(deviceKey, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all end-to-end sessions
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Called one for each session with
|
||||
* an object with, deviceKey, lastReceivedMessageTs, sessionId
|
||||
* and session keys.
|
||||
*/
|
||||
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
|
||||
this.backend!.getAllEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device
|
||||
* @param deviceKey - The public key of the other device.
|
||||
@@ -411,37 +456,16 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
return this.backend!.countEndToEndInboundGroupSessions();
|
||||
public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
return this.backend!.getEndToEndSessionsBatch();
|
||||
public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
return this.backend!.deleteEndToEndSessionsBatch(sessions);
|
||||
public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
return this.backend!.filterOutNotifiedErrorDevices(devices);
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -464,6 +488,35 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all inbound group sessions in the store
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Called once for each group session
|
||||
* in the store with an object having keys `{senderKey, sessionId, sessionData}`,
|
||||
* then once with null to indicate the end of the list.
|
||||
*/
|
||||
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
|
||||
this.backend!.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an end-to-end inbound group session to the store.
|
||||
* If there already exists an inbound group session with the same
|
||||
* senderCurve25519Key and sessionID, the session will not be added.
|
||||
* @param senderCurve25519Key - The sender's curve 25519 key
|
||||
* @param sessionId - The ID of the session
|
||||
* @param sessionData - The session data structure
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an end-to-end inbound group session to the store.
|
||||
* If there already exists an inbound group session with the same
|
||||
@@ -482,28 +535,51 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
* Store the state of all tracked devices
|
||||
* This contains devices for each user, a tracking state for each user
|
||||
* and a sync token matching the point in time the snapshot represents.
|
||||
* These all need to be written out in full each time such that the snapshot
|
||||
* is always consistent, so they are stored in one object.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null> {
|
||||
return this.backend!.getEndToEndInboundGroupSessionsBatch();
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
|
||||
this.backend!.storeEndToEndDeviceData(deviceData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
* Get the state of all tracked devices
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
* @param func - Function called with the
|
||||
* device data
|
||||
*/
|
||||
public deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
return this.backend!.deleteEndToEndInboundGroupSessionsBatch(sessions);
|
||||
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
|
||||
this.backend!.getEndToEndDeviceData(txn, func);
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
|
||||
/**
|
||||
* Store the end-to-end state for a room.
|
||||
* @param roomId - The room's ID.
|
||||
* @param roomInfo - The end-to-end info for the room.
|
||||
* @param txn - An active transaction. See doTxn().
|
||||
*/
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
|
||||
this.backend!.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,6 +591,37 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
this.backend!.getEndToEndRooms(txn, func);
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
/**
|
||||
* Get the inbound group sessions that need to be backed up.
|
||||
* @param limit - The maximum number of sessions to retrieve. 0
|
||||
* for no limit.
|
||||
* @returns resolves to an array of inbound group sessions
|
||||
*/
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
return this.backend!.getSessionsNeedingBackup(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the inbound group sessions that need to be backed up.
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns resolves to the number of sessions
|
||||
*/
|
||||
public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
|
||||
return this.backend!.countSessionsNeedingBackup(txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark sessions as needing to be backed up.
|
||||
* @param sessions - The sessions that need to be backed up.
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns resolves when the sessions are unmarked
|
||||
*/
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
|
||||
return this.backend!.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sessions as needing to be backed up.
|
||||
* @param sessions - The sessions that need to be backed up.
|
||||
@@ -525,6 +632,49 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
return this.backend!.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared-history group session for a room.
|
||||
* @param roomId - The room that the key belongs to
|
||||
* @param senderKey - The sender's curve 25519 key
|
||||
* @param sessionId - The ID of the session
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
*/
|
||||
public addSharedHistoryInboundGroupSession(
|
||||
roomId: string,
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
txn?: IDBTransaction,
|
||||
): void {
|
||||
this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared-history group session for a room.
|
||||
* @param roomId - The room that the key belongs to
|
||||
* @param txn - An active transaction. See doTxn(). (optional)
|
||||
* @returns Promise which resolves to an array of [senderKey, sessionId]
|
||||
*/
|
||||
public getSharedHistoryInboundGroupSessions(
|
||||
roomId: string,
|
||||
txn?: IDBTransaction,
|
||||
): Promise<[senderKey: string, sessionId: string][]> {
|
||||
return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a shared-history group session for a room we may be invited to later.
|
||||
*/
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory, txn?: IDBTransaction): void {
|
||||
this.backend!.addParkedSharedHistory(roomId, parkedData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop out all shared-history group sessions for a room.
|
||||
*/
|
||||
public takeParkedSharedHistory(roomId: string, txn?: IDBTransaction): Promise<ParkedSharedHistory[]> {
|
||||
return this.backend!.takeParkedSharedHistory(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
@@ -547,7 +697,12 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
* reject with that exception. On synchronous backends, the
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: Logger): Promise<T> {
|
||||
public doTxn<T>(
|
||||
mode: Mode,
|
||||
stores: Iterable<string>,
|
||||
func: (txn: IDBTransaction) => T,
|
||||
log?: PrefixedLogger,
|
||||
): Promise<T> {
|
||||
return this.backend!.doTxn<T>(mode, stores, func as (txn: unknown) => T, log);
|
||||
}
|
||||
}
|
||||
|
||||
343
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
343
node_modules/matrix-js-sdk/src/crypto/store/localStorage-crypto-store.ts
generated
vendored
@@ -14,22 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../../logger.ts";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store.ts";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
import { logger } from "../../logger";
|
||||
import { MemoryCryptoStore } from "./memory-crypto-store";
|
||||
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { safeSet } from "../../utils";
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -40,9 +32,10 @@ import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
*/
|
||||
|
||||
const E2E_PREFIX = "crypto.";
|
||||
const KEY_END_TO_END_MIGRATION_STATE = E2E_PREFIX + "migration";
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
|
||||
const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
@@ -52,6 +45,10 @@ function keyEndToEndSessions(deviceKey: string): string {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndSessionProblems(deviceKey: string): string {
|
||||
return E2E_PREFIX + "session.problems/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
|
||||
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
@@ -64,7 +61,7 @@ function keyEndToEndRoomsPrefix(roomId: string): string {
|
||||
return KEY_ROOMS_PREFIX + roomId;
|
||||
}
|
||||
|
||||
export class LocalStorageCryptoStore extends MemoryCryptoStore implements CryptoStore {
|
||||
export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
public static exists(store: Storage): boolean {
|
||||
const length = store.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
@@ -79,53 +76,17 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
return LocalStorageCryptoStore.exists(this.store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
return getJsonItem(this.store, KEY_END_TO_END_MIGRATION_STATE) ?? MigrationState.NOT_STARTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
setJsonItem(this.store, KEY_END_TO_END_MIGRATION_STATE, migrationState);
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(keyEndToEndSessions(""))) {
|
||||
const sessions = getJsonItem(this.store, key);
|
||||
count += Object.keys(sessions ?? {}).length;
|
||||
}
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) ++count;
|
||||
}
|
||||
func(count);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
|
||||
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
const fixedSessions: Record<string, ISessionInfo> = {};
|
||||
@@ -151,7 +112,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func: (session: ISessionInfo) => void,
|
||||
): void {
|
||||
const sessions = this._getEndToEndSessions(deviceKey);
|
||||
func(sessions[sessionId] ?? {});
|
||||
func(sessions[sessionId] || {});
|
||||
}
|
||||
|
||||
public getEndToEndSessions(
|
||||
@@ -159,7 +120,18 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
txn: unknown,
|
||||
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
|
||||
): void {
|
||||
func(this._getEndToEndSessions(deviceKey) ?? {});
|
||||
func(this._getEndToEndSessions(deviceKey) || {});
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
|
||||
const deviceKey = this.store.key(i)!.split("/")[1];
|
||||
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
|
||||
@@ -168,54 +140,56 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) {
|
||||
const deviceKey = this.store.key(i)!.split("/")[1];
|
||||
for (const session of Object.values(this._getEndToEndSessions(deviceKey))) {
|
||||
result.push(session);
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
|
||||
problems.push({ type, fixed, time: Date.now() });
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
setJsonItem(this.store, key, problems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
for (const { deviceKey, sessionId } of sessions) {
|
||||
const deviceSessions = this._getEndToEndSessions(deviceKey) || {};
|
||||
delete deviceSessions[sessionId];
|
||||
if (Object.keys(deviceSessions).length === 0) {
|
||||
// No more sessions for this device.
|
||||
this.store.removeItem(keyEndToEndSessions(deviceKey));
|
||||
} else {
|
||||
setJsonItem(this.store, keyEndToEndSessions(deviceKey), deviceSessions);
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices =
|
||||
getJsonItem<MemoryCryptoStore["notifiedErrorDevices"]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
@@ -232,6 +206,37 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
);
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
func({
|
||||
senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43),
|
||||
sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44),
|
||||
sessionData: getJsonItem(this.store, key)!,
|
||||
});
|
||||
}
|
||||
}
|
||||
func(null);
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId));
|
||||
if (!existing) {
|
||||
this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
@@ -241,80 +246,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void {
|
||||
setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<SessionExtended[] | null> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const result: SessionExtended[] = [];
|
||||
for (let i = 0; i < this.store.length; ++i) {
|
||||
const key = this.store.key(i);
|
||||
if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
|
||||
const key2 = key.slice(KEY_INBOUND_SESSION_PREFIX.length);
|
||||
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
result.push({
|
||||
senderKey: key2.slice(0, 43),
|
||||
sessionId: key2.slice(44),
|
||||
sessionData: getJsonItem(this.store, key)!,
|
||||
needsBackup: key2 in sessionsNeedingBackup,
|
||||
});
|
||||
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
|
||||
func(getJsonItem(this.store, KEY_DEVICE_DATA));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
for (const { senderKey, sessionId } of sessions) {
|
||||
const k = keyEndToEndInboundGroupSession(senderKey, sessionId);
|
||||
this.store.removeItem(k);
|
||||
}
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
|
||||
setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
|
||||
}
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
|
||||
setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
@@ -331,6 +281,47 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func(result);
|
||||
}
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
const sessions: ISession[] = [];
|
||||
|
||||
for (const session in sessionsNeedingBackup) {
|
||||
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
|
||||
// see getAllEndToEndInboundGroupSessions for the magic number explanations
|
||||
const senderKey = session.slice(0, 43);
|
||||
const sessionId = session.slice(44);
|
||||
this.getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => {
|
||||
sessions.push({
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
sessionData: sessionData!,
|
||||
});
|
||||
});
|
||||
if (limit && sessions.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
const sessionsNeedingBackup =
|
||||
getJsonItem<{
|
||||
[senderKeySessionId: string]: string;
|
||||
}>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
|
||||
for (const session of sessions) {
|
||||
delete sessionsNeedingBackup[session.senderKey + "/" + session.sessionId];
|
||||
}
|
||||
setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
const sessionsNeedingBackup =
|
||||
getJsonItem<{
|
||||
@@ -364,8 +355,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, CrossSigningKeyInfo>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
|
||||
func(keys);
|
||||
}
|
||||
|
||||
@@ -378,6 +369,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
|
||||
func(key);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
|
||||
500
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
500
node_modules/matrix-js-sdk/src/crypto/store/memory-crypto-store.ts
generated
vendored
@@ -14,61 +14,47 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { safeSet } from "../../utils.ts";
|
||||
import { logger } from "../../logger";
|
||||
import { safeSet, deepCompare, promiseTry } from "../../utils";
|
||||
import {
|
||||
type CryptoStore,
|
||||
type ISession,
|
||||
type SessionExtended,
|
||||
type ISessionInfo,
|
||||
type IWithheld,
|
||||
MigrationState,
|
||||
type Mode,
|
||||
type SecretStorePrivateKeys,
|
||||
SESSION_BATCH_SIZE,
|
||||
type InboundGroupSessionData,
|
||||
type IRoomEncryption,
|
||||
} from "./base.ts";
|
||||
import { type CrossSigningKeyInfo } from "../../crypto-api/index.ts";
|
||||
|
||||
function encodeSessionKey(senderCurve25519Key: string, sessionId: string): string {
|
||||
return encodeURIComponent(senderCurve25519Key) + "/" + encodeURIComponent(sessionId);
|
||||
}
|
||||
|
||||
function decodeSessionKey(key: string): { senderKey: string; sessionId: string } {
|
||||
const keyParts = key.split("/");
|
||||
const senderKey = decodeURIComponent(keyParts[0]);
|
||||
const sessionId = decodeURIComponent(keyParts[1]);
|
||||
return { senderKey, sessionId };
|
||||
}
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
IProblem,
|
||||
ISession,
|
||||
ISessionInfo,
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
SecretStorePrivateKeys,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
|
||||
/**
|
||||
* Internal module. in-memory storage for e2e.
|
||||
*/
|
||||
|
||||
export class MemoryCryptoStore implements CryptoStore {
|
||||
private migrationState: MigrationState = MigrationState.NOT_STARTED;
|
||||
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
|
||||
private account: string | null = null;
|
||||
private crossSigningKeys: Record<string, CrossSigningKeyInfo> | null = null;
|
||||
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
|
||||
private privateKeys: Partial<SecretStorePrivateKeys> = {};
|
||||
|
||||
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
|
||||
private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
|
||||
private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
|
||||
private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
|
||||
private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
|
||||
// Opaque device data object
|
||||
private deviceData: IDeviceData | null = null;
|
||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||
|
||||
/**
|
||||
* Returns true if this CryptoStore has ever been initialised (ie, it might contain data).
|
||||
*
|
||||
* Implementation of {@link CryptoStore.containsData}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async containsData(): Promise<boolean> {
|
||||
// If it contains anything, it should contain an account.
|
||||
return this.account !== null;
|
||||
}
|
||||
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
||||
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date.
|
||||
@@ -92,25 +78,186 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing outgoing room key request, and if none is found,
|
||||
* add a new one
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getMigrationState}.
|
||||
*
|
||||
* @internal
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}: either the
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
public async getMigrationState(): Promise<MigrationState> {
|
||||
return this.migrationState;
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
return promiseTry(() => {
|
||||
// first see if we already have an entry for this request.
|
||||
const existing = this._getOutgoingRoomKeyRequest(requestBody);
|
||||
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
return existing;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id);
|
||||
this.outgoingRoomKeyRequests.push(request);
|
||||
return request;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data on how much of the libolm to Rust Crypto migration has been done.
|
||||
* Look for an existing room key request
|
||||
*
|
||||
* Implementation of {@link CryptoStore.setMigrationState}.
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns resolves to the matching
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* not found
|
||||
*/
|
||||
public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
|
||||
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for existing room key request, and returns the result synchronously.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param requestBody - existing request to look for
|
||||
*
|
||||
* @returns
|
||||
* the matching request, or null if not found
|
||||
*/
|
||||
public async setMigrationState(migrationState: MigrationState): Promise<void> {
|
||||
this.migrationState = migrationState;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
|
||||
for (const existing of this.outgoingRoomKeyRequests) {
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state
|
||||
*
|
||||
* @param wantedStates - list of acceptable states
|
||||
*
|
||||
* @returns resolves to the a
|
||||
* {@link OutgoingRoomKeyRequest}, or null if
|
||||
* there are no pending requests in those states
|
||||
*/
|
||||
public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (req.state === state) {
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns All OutgoingRoomKeyRequests in state
|
||||
*/
|
||||
public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
|
||||
return Promise.resolve(this.outgoingRoomKeyRequests.filter((r) => r.state == wantedState));
|
||||
}
|
||||
|
||||
public getOutgoingRoomKeyRequestsByTarget(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (
|
||||
req.state === state &&
|
||||
req.recipients.some((recipient) => recipient.userId === userId && recipient.deviceId === deviceId)
|
||||
) {
|
||||
results.push(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and update it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
* @param updates - name/value map of updates to apply
|
||||
*
|
||||
* @returns resolves to
|
||||
* {@link OutgoingRoomKeyRequest}
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
public updateOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
updates: Partial<OutgoingRoomKeyRequest>,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
if (req.requestId !== requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (req.state !== expectedState) {
|
||||
logger.warn(
|
||||
`Cannot update room key request from ${expectedState} ` +
|
||||
`as it was already updated to ${req.state}`,
|
||||
);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
Object.assign(req, updates);
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an existing room key request by id and state, and delete it if
|
||||
* found
|
||||
*
|
||||
* @param requestId - ID of request to update
|
||||
* @param expectedState - state we expect to find the request in
|
||||
*
|
||||
* @returns resolves once the operation is completed
|
||||
*/
|
||||
public deleteOutgoingRoomKeyRequest(
|
||||
requestId: string,
|
||||
expectedState: number,
|
||||
): Promise<OutgoingRoomKeyRequest | null> {
|
||||
for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
|
||||
const req = this.outgoingRoomKeyRequests[i];
|
||||
|
||||
if (req.requestId !== requestId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (req.state != expectedState) {
|
||||
logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.outgoingRoomKeyRequests.splice(i, 1);
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -123,7 +270,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
this.account = accountPickle;
|
||||
}
|
||||
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
|
||||
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
|
||||
func(this.crossSigningKeys);
|
||||
}
|
||||
|
||||
@@ -132,10 +279,14 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
func: (key: SecretStorePrivateKeys[K] | null) => void,
|
||||
type: K,
|
||||
): void {
|
||||
const result = this.privateKeys[type];
|
||||
const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
|
||||
func(result || null);
|
||||
}
|
||||
|
||||
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
|
||||
this.crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
|
||||
txn: unknown,
|
||||
type: K,
|
||||
@@ -147,11 +298,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
// Olm Sessions
|
||||
|
||||
public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
|
||||
let count = 0;
|
||||
for (const deviceSessions of Object.values(this.sessions)) {
|
||||
count += Object.keys(deviceSessions).length;
|
||||
}
|
||||
func(count);
|
||||
func(Object.keys(this.sessions).length);
|
||||
}
|
||||
|
||||
public getEndToEndSession(
|
||||
@@ -172,6 +319,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
func(this.sessions[deviceKey] || {});
|
||||
}
|
||||
|
||||
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
|
||||
Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
|
||||
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
|
||||
func({
|
||||
...session,
|
||||
deviceKey,
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
|
||||
let deviceSessions = this.sessions[deviceKey];
|
||||
if (deviceSessions === undefined) {
|
||||
@@ -181,49 +340,50 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
safeSet(deviceSessions, sessionId, sessionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndSessionsBatch(): Promise<null | ISessionInfo[]> {
|
||||
const result: ISessionInfo[] = [];
|
||||
for (const deviceSessions of Object.values(this.sessions)) {
|
||||
for (const session of Object.values(deviceSessions)) {
|
||||
result.push(session);
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
|
||||
const problems = (this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []);
|
||||
problems.push({ type, fixed, time: Date.now() });
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Olm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndSessionsBatch(sessions: { deviceKey: string; sessionId: string }[]): Promise<void> {
|
||||
for (const { deviceKey, sessionId } of sessions) {
|
||||
const deviceSessions = this.sessions[deviceKey] || {};
|
||||
delete deviceSessions[sessionId];
|
||||
if (Object.keys(deviceSessions).length === 0) {
|
||||
// No more sessions for this device.
|
||||
delete this.sessions[deviceKey];
|
||||
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
|
||||
const problems = this.sessionProblems[deviceKey] || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
|
||||
const notifiedErrorDevices = this.notifiedErrorDevices;
|
||||
const ret: IOlmDevice[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { userId, deviceInfo } = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
@@ -234,90 +394,136 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
txn: unknown,
|
||||
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
|
||||
): void {
|
||||
const k = encodeSessionKey(senderCurve25519Key, sessionId);
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
|
||||
}
|
||||
|
||||
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
|
||||
for (const key of Object.keys(this.inboundGroupSessions)) {
|
||||
// we can't use split, as the components we are trying to split out
|
||||
// might themselves contain '/' characters. We rely on the
|
||||
// senderKey being a (32-byte) curve25519 key, base64-encoded
|
||||
// (hence 43 characters long).
|
||||
|
||||
func({
|
||||
senderKey: key.slice(0, 43),
|
||||
sessionId: key.slice(44),
|
||||
sessionData: this.inboundGroupSessions[key],
|
||||
});
|
||||
}
|
||||
func(null);
|
||||
}
|
||||
|
||||
public addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
if (this.inboundGroupSessions[k] === undefined) {
|
||||
this.inboundGroupSessions[k] = sessionData;
|
||||
}
|
||||
}
|
||||
|
||||
public storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: InboundGroupSessionData,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = encodeSessionKey(senderCurve25519Key, sessionId);
|
||||
this.inboundGroupSessions[k] = sessionData;
|
||||
this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of Megolm sessions in the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.countEndToEndInboundGroupSessions}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async countEndToEndInboundGroupSessions(): Promise<number> {
|
||||
return Object.keys(this.inboundGroupSessions).length;
|
||||
public storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key: string,
|
||||
sessionId: string,
|
||||
sessionData: IWithheld,
|
||||
txn: unknown,
|
||||
): void {
|
||||
const k = senderCurve25519Key + "/" + sessionId;
|
||||
this.inboundGroupSessionsWithheld[k] = sessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.getEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getEndToEndInboundGroupSessionsBatch(): Promise<null | SessionExtended[]> {
|
||||
const result: SessionExtended[] = [];
|
||||
for (const [key, session] of Object.entries(this.inboundGroupSessions)) {
|
||||
result.push({
|
||||
...decodeSessionKey(key),
|
||||
sessionData: session,
|
||||
needsBackup: key in this.sessionsNeedingBackup,
|
||||
});
|
||||
if (result.length >= SESSION_BATCH_SIZE) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// Device Data
|
||||
|
||||
if (result.length === 0) {
|
||||
// No sessions left.
|
||||
return null;
|
||||
}
|
||||
|
||||
// There are fewer sessions than the batch size; return the final batch of sessions.
|
||||
return result;
|
||||
public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
|
||||
func(this.deviceData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a batch of Megolm sessions from the database.
|
||||
*
|
||||
* Implementation of {@link CryptoStore.deleteEndToEndInboundGroupSessionsBatch}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async deleteEndToEndInboundGroupSessionsBatch(
|
||||
sessions: { senderKey: string; sessionId: string }[],
|
||||
): Promise<void> {
|
||||
for (const { senderKey, sessionId } of sessions) {
|
||||
const k = encodeSessionKey(senderKey, sessionId);
|
||||
delete this.inboundGroupSessions[k];
|
||||
}
|
||||
public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
|
||||
this.deviceData = deviceData;
|
||||
}
|
||||
|
||||
// E2E rooms
|
||||
|
||||
public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
|
||||
this.rooms[roomId] = roomInfo;
|
||||
}
|
||||
|
||||
public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
|
||||
func(this.rooms);
|
||||
}
|
||||
|
||||
public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
|
||||
const sessions: ISession[] = [];
|
||||
for (const session in this.sessionsNeedingBackup) {
|
||||
if (this.inboundGroupSessions[session]) {
|
||||
sessions.push({
|
||||
senderKey: session.slice(0, 43),
|
||||
sessionId: session.slice(44),
|
||||
sessionData: this.inboundGroupSessions[session],
|
||||
});
|
||||
if (limit && session.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sessions);
|
||||
}
|
||||
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
|
||||
}
|
||||
|
||||
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = session.senderKey + "/" + session.sessionId;
|
||||
delete this.sessionsNeedingBackup[sessionKey];
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
|
||||
for (const session of sessions) {
|
||||
const sessionKey = encodeSessionKey(session.senderKey, session.sessionId);
|
||||
const sessionKey = session.senderKey + "/" + session.sessionId;
|
||||
this.sessionsNeedingBackup[sessionKey] = true;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
|
||||
const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
|
||||
sessions.push([senderKey, sessionId]);
|
||||
this.sharedHistoryInboundGroupSessions[roomId] = sessions;
|
||||
}
|
||||
|
||||
public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
|
||||
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
||||
}
|
||||
|
||||
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
parked.push(parkedData);
|
||||
this.parkedSharedHistory.set(roomId, parked);
|
||||
}
|
||||
|
||||
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
|
||||
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||
this.parkedSharedHistory.delete(roomId);
|
||||
return Promise.resolve(parked);
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
||||
|
||||
Reference in New Issue
Block a user