1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Convert crypto/store/* to Typescript

This commit is contained in:
Michael Telatynski
2021-07-24 11:35:30 +01:00
parent dc154a00a8
commit 011a2acfd2
18 changed files with 873 additions and 520 deletions

View File

@@ -74,3 +74,11 @@ export enum HistoryVisibility {
Shared = "shared", Shared = "shared",
WorldReadable = "world_readable", WorldReadable = "world_readable",
} }
// XXX move to OlmDevice when converted
export interface InboundGroupSessionData {
room_id: string; // eslint-disable-line camelcase
session: string;
keysClaimed: Record<string, string>;
forwardingCurve25519KeyChain: string[];
}

View File

@@ -146,10 +146,10 @@ import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@
import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces"; import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "./@types/spaces";
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules"; import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
import { IThreepid } from "./@types/threepids"; import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
export type Store = IStore; export type Store = IStore;
export type SessionStore = WebStorageSessionStore; export type SessionStore = WebStorageSessionStore;
export type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
export type Callback = (err: Error | any | null, data?: any) => void; export type Callback = (err: Error | any | null, data?: any) => void;
export type ResetTimelineCallback = (roomId: string) => boolean; export type ResetTimelineCallback = (roomId: string) => boolean;

View File

@@ -28,10 +28,11 @@ import { decryptAES, encryptAES } from './aes';
import { PkSigning } from "@matrix-org/olm"; import { PkSigning } from "@matrix-org/olm";
import { DeviceInfo } from "./deviceinfo"; import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage"; import { SecretStorage } from "./SecretStorage";
import { CryptoStore, ICrossSigningKey, ISignedKey, MatrixClient } from "../client"; import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix"; import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -47,6 +48,12 @@ export interface ICacheCallbacks {
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>; storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
} }
export interface ICrossSigningInfo {
keys: Record<string, ICrossSigningKey>;
firstUse: boolean;
crossSigningVerifiedBefore: boolean;
}
export class CrossSigningInfo extends EventEmitter { export class CrossSigningInfo extends EventEmitter {
public keys: Record<string, ICrossSigningKey> = {}; public keys: Record<string, ICrossSigningKey> = {};
public firstUse = true; public firstUse = true;
@@ -75,7 +82,7 @@ export class CrossSigningInfo extends EventEmitter {
super(); super();
} }
public static fromStorage(obj: object, userId: string): CrossSigningInfo { public static fromStorage(obj: ICrossSigningInfo, userId: string): CrossSigningInfo {
const res = new CrossSigningInfo(userId); const res = new CrossSigningInfo(userId);
for (const prop in obj) { for (const prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {
@@ -85,7 +92,7 @@ export class CrossSigningInfo extends EventEmitter {
return res; return res;
} }
public toStorage(): object { public toStorage(): ICrossSigningInfo {
return { return {
keys: this.keys, keys: this.keys,
firstUse: this.firstUse, firstUse: this.firstUse,

View File

@@ -24,12 +24,13 @@ import { EventEmitter } from 'events';
import { logger } from '../logger'; import { logger } from '../logger';
import { DeviceInfo, IDevice } from './deviceinfo'; import { DeviceInfo, IDevice } from './deviceinfo';
import { CrossSigningInfo } from './CrossSigning'; import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning';
import * as olmlib from './olmlib'; import * as olmlib from './olmlib';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { chunkPromises, defer, IDeferred, sleep } from '../utils'; import { chunkPromises, defer, IDeferred, sleep } from '../utils';
import { MatrixClient, CryptoStore } from "../client"; import { MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import { CryptoStore } from "./store/base";
/* State transition diagram for DeviceList.deviceTrackingStatus /* State transition diagram for DeviceList.deviceTrackingStatus
* *
@@ -52,7 +53,7 @@ import { OlmDevice } from "./OlmDevice";
*/ */
// constants for DeviceList.deviceTrackingStatus // constants for DeviceList.deviceTrackingStatus
enum TrackingStatus { export enum TrackingStatus {
NotTracked, NotTracked,
PendingDownload, PendingDownload,
DownloadInProgress, DownloadInProgress,
@@ -65,32 +66,22 @@ export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
*/ */
export class DeviceList extends EventEmitter { export class DeviceList extends EventEmitter {
// userId -> { private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {};
// deviceId -> {
// [device info]
// }
// }
private devices: Record<string, Record<string, IDevice>> = {};
// userId -> { public crossSigningInfo: { [userId: string]: ICrossSigningInfo } = {};
// [key info]
// }
public crossSigningInfo: Record<string, object> = {};
// map of identity keys to the user who owns it // map of identity keys to the user who owns it
private userByIdentityKey: Record<string, string> = {}; private userByIdentityKey: Record<string, string> = {};
// which users we are tracking device status for. // which users we are tracking device status for.
// userId -> TRACKING_STATUS_* private deviceTrackingStatus: { [userId: string]: TrackingStatus } = {}; // loaded from storage in load()
private deviceTrackingStatus: Record<string, TrackingStatus> = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was written, // The 'next_batch' sync token at the point the data was written,
// ie. a token representing the point immediately after the // ie. a token representing the point immediately after the
// moment represented by the snapshot in the db. // moment represented by the snapshot in the db.
private syncToken: string = null; private syncToken: string = null;
// userId -> promise private keyDownloadsInProgressByUser: { [userId: string]: Promise<void> } = {};
private keyDownloadsInProgressByUser: Record<string, Promise<void>> = {};
// Set whenever changes are made other than setting the sync token // Set whenever changes are made other than setting the sync token
private dirty = false; private dirty = false;
@@ -375,7 +366,7 @@ export class DeviceList extends EventEmitter {
return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); return CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId);
} }
public storeCrossSigningForUser(userId: string, info: CrossSigningInfo): void { public storeCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
this.crossSigningInfo[userId] = info; this.crossSigningInfo[userId] = info;
this.dirty = true; this.dirty = true;
} }
@@ -603,7 +594,7 @@ export class DeviceList extends EventEmitter {
} }
} }
public setRawStoredCrossSigningForUser(userId: string, info: object): void { public setRawStoredCrossSigningForUser(userId: string, info: ICrossSigningInfo): void {
this.crossSigningInfo[userId] = info; this.crossSigningInfo[userId] = info;
} }
@@ -864,9 +855,7 @@ class DeviceListUpdateSerialiser {
crossSigning.setKeys(crossSigningResponse); crossSigning.setKeys(crossSigningResponse);
this.deviceList.setRawStoredCrossSigningForUser( this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
userId, crossSigning.toStorage(),
);
// NB. Unlike most events in the js-sdk, this one is internal to the // NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted // js-sdk and is not re-emitted

View File

@@ -949,7 +949,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* data stored in the session store about an inbound group session * data stored in the session store about an inbound group session
* *
* @typedef {Object} InboundGroupSessionData * @typedef {Object} InboundGroupSessionData
* @property {string} room_Id * @property {string} room_id
* @property {string} session pickled Olm.InboundGroupSession * @property {string} session pickled Olm.InboundGroupSession
* @property {Object<string, string>} keysClaimed * @property {Object<string, string>} keysClaimed
* @property {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding * @property {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding

View File

@@ -15,9 +15,9 @@ limitations under the License.
*/ */
import { logger } from '../logger'; import { logger } from '../logger';
import { CryptoStore, MatrixClient } from "../client"; import { MatrixClient } from "../client";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
import { OutgoingRoomKeyRequest } from './store/base'; import { CryptoStore, OutgoingRoomKeyRequest } from './store/base';
import { EventType } from "../@types/event"; import { EventType } from "../@types/event";
/** /**
@@ -263,9 +263,8 @@ export class OutgoingRoomKeyRequestManager {
'deleting unnecessary room key request for ' + 'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody), stringifyRequestBody(requestBody),
); );
return this.cryptoStore.deleteOutgoingRoomKeyRequest( return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent)
req.requestId, RoomKeyRequestState.Unsent, .then(); // match Promise<void> signature
);
case RoomKeyRequestState.Sent: { case RoomKeyRequestState.Sent: {
// send a cancellation. // send a cancellation.
@@ -325,7 +324,7 @@ export class OutgoingRoomKeyRequestManager {
* @return {Promise} resolves to a list of all the * @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest} * {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/ */
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): OutgoingRoomKeyRequest[] { public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise<OutgoingRoomKeyRequest[]> {
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
} }
@@ -415,7 +414,7 @@ export class OutgoingRoomKeyRequestManager {
} }
// given a RoomKeyRequest, send it and update the request record // given a RoomKeyRequest, send it and update the request record
private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<void> { private async sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<void> {
logger.log( logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` + `Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` + ` from ${stringifyRecipientList(req.recipients)}` +
@@ -429,19 +428,19 @@ export class OutgoingRoomKeyRequestManager {
body: req.requestBody, body: req.requestBody,
}; };
return this.sendMessageToDevices( await this.sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId);
requestMessage, req.recipients, req.requestTxnId || req.requestId, await this.cryptoStore.updateOutgoingRoomKeyRequest(
).then(() => { req.requestId, RoomKeyRequestState.Unsent,
return this.cryptoStore.updateOutgoingRoomKeyRequest( { state: RoomKeyRequestState.Sent },
req.requestId, RoomKeyRequestState.Unsent, );
{ state: RoomKeyRequestState.Sent },
);
});
} }
// Given a RoomKeyRequest, cancel it and delete the request record unless // Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT. // andResend is set, in which case transition to UNSENT.
private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<void> { private async sendOutgoingRoomKeyRequestCancellation(
req: OutgoingRoomKeyRequest,
andResend = false,
): Promise<void> {
logger.log( logger.log(
`Sending cancellation for key request for ` + `Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRequestBody(req.requestBody)} to ` +
@@ -455,21 +454,19 @@ export class OutgoingRoomKeyRequestManager {
request_id: req.requestId, request_id: req.requestId,
}; };
return this.sendMessageToDevices( await this.sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId);
requestMessage, req.recipients, req.cancellationTxnId, if (andResend) {
).then(() => { // We want to resend, so transition to UNSENT
if (andResend) { await this.cryptoStore.updateOutgoingRoomKeyRequest(
// We want to resend, so transition to UNSENT req.requestId,
return this.cryptoStore.updateOutgoingRoomKeyRequest( RoomKeyRequestState.CancellationPendingAndWillResend,
req.requestId, { state: RoomKeyRequestState.Unsent },
RoomKeyRequestState.CancellationPendingAndWillResend, );
{ state: RoomKeyRequestState.Unsent }, } else {
); await this.cryptoStore.deleteOutgoingRoomKeyRequest(
}
return this.cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, RoomKeyRequestState.CancellationPending, req.requestId, RoomKeyRequestState.CancellationPending,
); );
}); }
} }
// send a RoomKeyRequest to a list of recipients // send a RoomKeyRequest to a list of recipients

View File

@@ -20,8 +20,8 @@ limitations under the License.
* Manages the list of encrypted rooms * Manages the list of encrypted rooms
*/ */
import { CryptoStore } from './store/base';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { CryptoStore } from "../client";
/* eslint-disable camelcase */ /* eslint-disable camelcase */
export interface IRoomEncryption { export interface IRoomEncryption {

View File

@@ -61,7 +61,7 @@ interface IBlockedMap {
}; };
} }
interface IOlmDevice<T = DeviceInfo> { export interface IOlmDevice<T = DeviceInfo> {
userId: string; userId: string;
deviceInfo: T; deviceInfo: T;
} }

View File

@@ -59,12 +59,13 @@ import { IStore } from "../store";
import { Room } from "../models/room"; import { Room } from "../models/room";
import { RoomMember } from "../models/room-member"; import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client"; import { MatrixClient, IKeysUploadResponse, SessionStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base"; import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { IRoomEncryption, RoomList } from "./RoomList"; import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api"; import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync"; import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -1410,9 +1411,7 @@ export class Crypto extends EventEmitter {
crossSigning.updateCrossSigningVerifiedBefore( crossSigning.updateCrossSigningVerifiedBefore(
this.checkUserTrust(userId).isCrossSigningVerified(), this.checkUserTrust(userId).isCrossSigningVerified(),
); );
this.deviceList.setRawStoredCrossSigningForUser( this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
userId, crossSigning.toStorage(),
);
} }
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));

View File

@@ -1,5 +1,33 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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 "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/** /**
* Internal module. Defintions for storage for the crypto module * Internal module. Definitions for storage for the crypto module
* *
* @module * @module
*/ */
@@ -9,9 +37,141 @@
* *
* @interface CryptoStore * @interface CryptoStore
*/ */
export interface CryptoStore {
startup(): Promise<CryptoStore>;
deleteAllData(): 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>;
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index"; // Olm Account
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager"; getAccount(txn: unknown, func: (accountPickle: string) => void);
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void;
getSecretStorePrivateKey<T>(txn: unknown, func: (key: IEncryptedPayload | null) => T, type: string): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void;
getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void;
getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void;
storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): 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(
senderCurve25519Key: string,
sessionId: string,
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;
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?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]>;
// Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
}
export type Mode = "readonly" | "readwrite";
export interface ISession {
senderKey: string;
sessionId: string;
sessionData?: InboundGroupSessionData;
}
export interface ISessionInfo {
deviceKey?: string;
sessionId?: string;
session?: string;
lastReceivedMessageTs?: number;
}
export interface IDeviceData {
devices: {
[ userId: string ]: {
[ deviceId: string ]: IDevice;
};
};
trackingStatus: {
[ userId: string ]: TrackingStatus;
};
crossSigningInfo?: Record<string, ICrossSigningInfo>;
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;
}
/** /**
* Represents an outgoing room key request * Represents an outgoing room key request

View File

@@ -1,7 +1,5 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -16,8 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { logger } from '../../logger'; import { logger, PrefixedLogger } from '../../logger';
import * as utils from "../../utils"; import * as utils from "../../utils";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
export const VERSION = 10; export const VERSION = 10;
const PROFILE_TRANSACTIONS = false; const PROFILE_TRANSACTIONS = false;
@@ -29,18 +43,17 @@ const PROFILE_TRANSACTIONS = false;
* *
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
export class Backend { export class Backend implements CryptoStore {
private nextTxnId = 0;
/** /**
* @param {IDBDatabase} db * @param {IDBDatabase} db
*/ */
constructor(db) { constructor(private db: IDBDatabase) {
this._db = db;
this._nextTxnId = 0;
// make sure we close the db on `onversionchange` - otherwise // make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent // attempts to delete the database will block (and subsequent
// attempts to re-create it will also block). // attempts to re-create it will also block).
db.onversionchange = (ev) => { db.onversionchange = () => {
logger.log(`versionchange for indexeddb ${this._dbName}: closing`); logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close(); db.close();
}; };
@@ -56,11 +69,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one. * same instance as passed in, or the existing one.
*/ */
getOrAddOutgoingRoomKeyRequest(request) { public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
const requestBody = request.requestBody; const requestBody = request.requestBody;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
txn.onerror = reject; txn.onerror = reject;
// first see if we already have an entry for this request. // first see if we already have an entry for this request.
@@ -99,9 +112,9 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found * not found
*/ */
getOutgoingRoomKeyRequest(requestBody) { public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
txn.onerror = reject; txn.onerror = reject;
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => { this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
@@ -122,7 +135,12 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found. * not found.
*/ */
_getOutgoingRoomKeyRequest(txn, requestBody, callback) { // 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 store = txn.objectStore("outgoingRoomKeyRequests");
const idx = store.index("session"); const idx = store.index("session");
@@ -131,8 +149,8 @@ export class Backend {
requestBody.session_id, requestBody.session_id,
]); ]);
cursorReq.onsuccess = (ev) => { cursorReq.onsuccess = () => {
const cursor = ev.target.result; const cursor = cursorReq.result;
if (!cursor) { if (!cursor) {
// no match found // no match found
callback(null); callback(null);
@@ -162,7 +180,7 @@ export class Backend {
* there are no pending requests in those states. If there are multiple * there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen. * requests in those states, an arbitrary one is chosen.
*/ */
getOutgoingRoomKeyRequestByState(wantedStates) { public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
if (wantedStates.length === 0) { if (wantedStates.length === 0) {
return Promise.resolve(null); return Promise.resolve(null);
} }
@@ -195,7 +213,7 @@ export class Backend {
cursorReq.onsuccess = onsuccess; cursorReq.onsuccess = onsuccess;
} }
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests"); const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex]; const wantedState = wantedStates[stateIndex];
@@ -210,19 +228,23 @@ export class Backend {
* @param {Number} wantedState * @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state * @return {Promise<Array<*>>} All elements in a given state
*/ */
getAllOutgoingRoomKeyRequestsByState(wantedState) { public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests"); const store = txn.objectStore("outgoingRoomKeyRequests");
const index = store.index("state"); const index = store.index("state");
const request = index.getAll(wantedState); const request = index.getAll(wantedState);
request.onsuccess = (ev) => resolve(ev.target.result); request.onsuccess = () => resolve(request.result);
request.onerror = (ev) => reject(ev.target.error); request.onerror = () => reject(request.error);
}); });
} }
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
let stateIndex = 0; let stateIndex = 0;
const results = []; const results = [];
@@ -248,7 +270,7 @@ export class Backend {
} }
} }
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests"); const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex]; const wantedState = wantedStates[stateIndex];
@@ -270,7 +292,11 @@ export class Backend {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest} * {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found * updated request, or null if no matching row was found
*/ */
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { public updateOutgoingRoomKeyRequest(
requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
let result = null; let result = null;
function onsuccess(ev) { function onsuccess(ev) {
@@ -291,9 +317,8 @@ export class Backend {
result = data; result = data;
} }
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests") const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
.openCursor(requestId);
cursorReq.onsuccess = onsuccess; cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result); return promiseifyTxn(txn).then(() => result);
} }
@@ -307,12 +332,14 @@ export class Backend {
* *
* @returns {Promise} resolves once the operation is completed * @returns {Promise} resolves once the operation is completed
*/ */
deleteOutgoingRoomKeyRequest(requestId, expectedState) { public deleteOutgoingRoomKeyRequest(
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); requestId: string,
const cursorReq = txn.objectStore("outgoingRoomKeyRequests") expectedState: number,
.openCursor(requestId); ): Promise<OutgoingRoomKeyRequest | null> {
cursorReq.onsuccess = (ev) => { const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursor = ev.target.result; const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) { if (!cursor) {
return; return;
} }
@@ -326,12 +353,12 @@ export class Backend {
} }
cursor.delete(); cursor.delete();
}; };
return promiseifyTxn(txn); return promiseifyTxn<OutgoingRoomKeyRequest | null>(txn);
} }
// Olm Account // Olm Account
getAccount(txn, func) { public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-"); const getReq = objectStore.get("-");
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -343,12 +370,12 @@ export class Backend {
}; };
} }
storeAccount(txn, newData) { public storeAccount(txn: IDBTransaction, accountPickle: string): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
objectStore.put(newData, "-"); objectStore.put(accountPickle, "-");
} }
getCrossSigningKeys(txn, func) { public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys"); const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -360,7 +387,11 @@ export class Backend {
}; };
} }
getSecretStorePrivateKey(txn, func, type) { public getSecretStorePrivateKey<T>(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => T,
type: string,
): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`); const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -372,19 +403,19 @@ export class Backend {
}; };
} }
storeCrossSigningKeys(txn, keys) { public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys"); objectStore.put(keys, "crossSigningKeys");
} }
storeSecretStorePrivateKey(txn, type, key) { public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`); objectStore.put(key, `ssss_cache:${type}`);
} }
// Olm Sessions // Olm Sessions
countEndToEndSessions(txn, func) { public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count(); const countReq = objectStore.count();
countReq.onsuccess = function() { countReq.onsuccess = function() {
@@ -396,7 +427,11 @@ export class Backend {
}; };
} }
getEndToEndSessions(deviceKey, txn, func) { public getEndToEndSessions(
deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey"); const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey); const getReq = idx.openCursor(deviceKey);
@@ -419,7 +454,12 @@ export class Backend {
}; };
} }
getEndToEndSession(deviceKey, sessionId, txn, func) { public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]); const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -438,7 +478,7 @@ export class Backend {
}; };
} }
getAllEndToEndSessions(txn, func) { public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor(); const getReq = objectStore.openCursor();
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -456,7 +496,12 @@ export class Backend {
}; };
} }
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { public storeEndToEndSession(
deviceKey: string,
sessionId: string,
sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
objectStore.put({ objectStore.put({
deviceKey, deviceKey,
@@ -466,8 +511,8 @@ export class Backend {
}); });
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) { public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const txn = this._db.transaction("session_problems", "readwrite"); const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems"); const objectStore = txn.objectStore("session_problems");
objectStore.put({ objectStore.put({
deviceKey, deviceKey,
@@ -478,13 +523,13 @@ export class Backend {
return promiseifyTxn(txn); return promiseifyTxn(txn);
} }
async getEndToEndSessionProblem(deviceKey, timestamp) { public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
let result; let result;
const txn = this._db.transaction("session_problems", "readwrite"); const txn = this.db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems"); const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey"); const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey); const req = index.getAll(deviceKey);
req.onsuccess = (event) => { req.onsuccess = () => {
const problems = req.result; const problems = req.result;
if (!problems.length) { if (!problems.length) {
result = null; result = null;
@@ -511,14 +556,14 @@ export class Backend {
} }
// FIXME: we should probably prune this when devices get deleted // FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) { public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const txn = this._db.transaction("notified_error_devices", "readwrite"); const txn = this.db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices"); const objectStore = txn.objectStore("notified_error_devices");
const ret = []; const ret: IOlmDevice[] = [];
await Promise.all(devices.map((device) => { await Promise.all(devices.map((device) => {
return new Promise((resolve) => { return new Promise<void>((resolve) => {
const { userId, deviceInfo } = device; const { userId, deviceInfo } = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]); const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -536,9 +581,14 @@ export class Backend {
// Inbound group sessions // Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { public getEndToEndInboundGroupSession(
let session = false; senderCurve25519Key: string,
let withheld = false; sessionId: string,
txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
let session: InboundGroupSessionData | boolean = false;
let withheld: IWithheld | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]); const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -549,7 +599,7 @@ export class Backend {
session = null; session = null;
} }
if (withheld !== false) { if (withheld !== false) {
func(session, withheld); func(session as InboundGroupSessionData, withheld as IWithheld);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, e);
@@ -566,7 +616,7 @@ export class Backend {
withheld = null; withheld = null;
} }
if (session !== false) { if (session !== false) {
func(session, withheld); func(session as InboundGroupSessionData, withheld as IWithheld);
} }
} catch (e) { } catch (e) {
abortWithException(txn, e); abortWithException(txn, e);
@@ -574,7 +624,7 @@ export class Backend {
}; };
} }
getAllEndToEndInboundGroupSessions(txn, func) { public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor(); const getReq = objectStore.openCursor();
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -600,7 +650,12 @@ export class Backend {
}; };
} }
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({ const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData, senderCurve25519Key, sessionId, session: sessionData,
@@ -623,23 +678,31 @@ export class Backend {
}; };
} }
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions"); const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({ objectStore.put({
senderCurve25519Key, sessionId, session: sessionData, senderCurve25519Key, sessionId, session: sessionData,
}); });
} }
storeEndToEndInboundGroupSessionWithheld( public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn, senderCurve25519Key: string,
) { sessionId: string,
sessionData: IWithheld,
txn: IDBTransaction,
): void {
const objectStore = txn.objectStore("inbound_group_sessions_withheld"); const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({ objectStore.put({
senderCurve25519Key, sessionId, session: sessionData, senderCurve25519Key, sessionId, session: sessionData,
}); });
} }
getEndToEndDeviceData(txn, func) { public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data"); const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-"); const getReq = objectStore.get("-");
getReq.onsuccess = function() { getReq.onsuccess = function() {
@@ -651,24 +714,24 @@ export class Backend {
}; };
} }
storeEndToEndDeviceData(deviceData, txn) { public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
const objectStore = txn.objectStore("device_data"); const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-"); objectStore.put(deviceData, "-");
} }
storeEndToEndRoom(roomId, roomInfo, txn) { public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
const objectStore = txn.objectStore("rooms"); const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId); objectStore.put(roomInfo, roomId);
} }
getEndToEndRooms(txn, func) { public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const rooms = {}; const rooms = {};
const objectStore = txn.objectStore("rooms"); const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor(); const getReq = objectStore.openCursor();
getReq.onsuccess = function() { getReq.onsuccess = function() {
const cursor = getReq.result; const cursor = getReq.result;
if (cursor) { if (cursor) {
rooms[cursor.key] = cursor.value; rooms[cursor.key as string] = cursor.value;
cursor.continue(); cursor.continue();
} else { } else {
try { try {
@@ -682,11 +745,11 @@ export class Backend {
// session backups // session backups
getSessionsNeedingBackup(limit) { public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const sessions = []; const sessions = [];
const txn = this._db.transaction( const txn = this.db.transaction(
["sessions_needing_backup", "inbound_group_sessions"], ["sessions_needing_backup", "inbound_group_sessions"],
"readonly", "readonly",
); );
@@ -716,9 +779,9 @@ export class Backend {
}); });
} }
countSessionsNeedingBackup(txn) { public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
if (!txn) { if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly"); txn = this.db.transaction("sessions_needing_backup", "readonly");
} }
const objectStore = txn.objectStore("sessions_needing_backup"); const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -728,12 +791,12 @@ export class Backend {
}); });
} }
unmarkSessionsNeedingBackup(sessions, txn) { public async unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) { if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite"); txn = this.db.transaction("sessions_needing_backup", "readwrite");
} }
const objectStore = txn.objectStore("sessions_needing_backup"); const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => { await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]); const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve; req.onsuccess = resolve;
@@ -742,12 +805,12 @@ export class Backend {
})); }));
} }
markSessionsNeedingBackup(sessions, txn) { public async markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
if (!txn) { if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite"); txn = this.db.transaction("sessions_needing_backup", "readwrite");
} }
const objectStore = txn.objectStore("sessions_needing_backup"); const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => { await Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = objectStore.put({ const req = objectStore.put({
senderCurve25519Key: session.senderKey, senderCurve25519Key: session.senderKey,
@@ -759,9 +822,14 @@ export class Backend {
})); }));
} }
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { public addSharedHistoryInboundGroupSession(
roomId: string,
senderKey: string,
sessionId: string,
txn?: IDBTransaction,
): void {
if (!txn) { if (!txn) {
txn = this._db.transaction( txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readwrite", "shared_history_inbound_group_sessions", "readwrite",
); );
} }
@@ -774,9 +842,12 @@ export class Backend {
}; };
} }
getSharedHistoryInboundGroupSessions(roomId, txn) { public getSharedHistoryInboundGroupSessions(
roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
if (!txn) { if (!txn) {
txn = this._db.transaction( txn = this.db.transaction(
"shared_history_inbound_group_sessions", "readonly", "shared_history_inbound_group_sessions", "readonly",
); );
} }
@@ -791,16 +862,21 @@ export class Backend {
}); });
} }
doTxn(mode, stores, func, log = logger) { public doTxn<T>(
mode: Mode,
stores: Iterable<string>,
func: (txn: IDBTransaction) => T,
log: PrefixedLogger = logger,
): Promise<T> {
let startTime; let startTime;
let description; let description;
if (PROFILE_TRANSACTIONS) { if (PROFILE_TRANSACTIONS) {
const txnId = this._nextTxnId++; const txnId = this.nextTxnId++;
startTime = Date.now(); startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`; description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`); log.debug(`Starting ${description}`);
} }
const txn = this._db.transaction(stores, mode); const txn = this.db.transaction(stores, mode);
const promise = promiseifyTxn(txn); const promise = promiseifyTxn(txn);
const result = func(txn); const result = func(txn);
if (PROFILE_TRANSACTIONS) { if (PROFILE_TRANSACTIONS) {
@@ -818,7 +894,7 @@ export class Backend {
} }
} }
export function upgradeDatabase(db, oldVersion) { export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log( logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}` `Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`, + ` to ${VERSION}`,
@@ -874,7 +950,7 @@ export function upgradeDatabase(db, oldVersion) {
// Expand as needed. // Expand as needed.
} }
function createDatabase(db) { function createDatabase(db: IDBDatabase): void {
const outgoingRoomKeyRequestsStore = const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" }); db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
@@ -887,15 +963,19 @@ function createDatabase(db) {
outgoingRoomKeyRequestsStore.createIndex("state", "state"); outgoingRoomKeyRequestsStore.createIndex("state", "state");
} }
interface IWrappedIDBTransaction extends IDBTransaction {
_mx_abortexception: Error; // eslint-disable-line camelcase
}
/* /*
* Aborts a transaction with a given exception * Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception. * The transaction promise will be rejected with this exception.
*/ */
function abortWithException(txn, e) { function abortWithException(txn: IDBTransaction, e: Error) {
// We cheekily stick our exception onto the transaction object here // We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app // We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception. // an object containing the transaction and exception.
txn._mx_abortexception = e; (txn as IWrappedIDBTransaction)._mx_abortexception = e;
try { try {
txn.abort(); txn.abort();
} catch (e) { } catch (e) {
@@ -904,28 +984,28 @@ function abortWithException(txn, e) {
} }
} }
function promiseifyTxn(txn) { function promiseifyTxn<T>(txn: IDBTransaction): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.oncomplete = () => { txn.oncomplete = () => {
if (txn._mx_abortexception !== undefined) { if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject(txn._mx_abortexception); reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} }
resolve(); resolve(null);
}; };
txn.onerror = (event) => { txn.onerror = (event) => {
if (txn._mx_abortexception !== undefined) { if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject(txn._mx_abortexception); reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else { } else {
logger.log("Error performing indexeddb txn", event); logger.log("Error performing indexeddb txn", event);
reject(event.target.error); reject(txn.error);
} }
}; };
txn.onabort = (event) => { txn.onabort = (event) => {
if (txn._mx_abortexception !== undefined) { if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject(txn._mx_abortexception); reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else { } else {
logger.log("Error performing indexeddb txn", event); logger.log("Error performing indexeddb txn", event);
reject(event.target.error); reject(txn.error);
} }
}; };
}); });

View File

@@ -1,7 +1,5 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -16,12 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { logger } from '../../logger'; import { logger, PrefixedLogger } from '../../logger';
import { LocalStorageCryptoStore } from './localStorage-crypto-store'; import { LocalStorageCryptoStore } from './localStorage-crypto-store';
import { MemoryCryptoStore } from './memory-crypto-store'; import { MemoryCryptoStore } from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend'; import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import { InvalidCryptoStoreError } from '../../errors'; import { InvalidCryptoStoreError } from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers"; import * as IndexedDBHelpers from "../../indexeddb-helpers";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/** /**
* Internal module. indexeddb storage for e2e. * Internal module. indexeddb storage for e2e.
@@ -35,23 +49,30 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
* *
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
export class IndexedDBCryptoStore { export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_ACCOUNT = 'account';
public static STORE_SESSIONS = 'sessions';
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
public static STORE_DEVICE_DATA = 'device_data';
public static STORE_ROOMS = 'rooms';
public static STORE_BACKUP = 'sessions_needing_backup';
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
private backendPromise: Promise<CryptoStore> = null;
private backend: CryptoStore = null;
/** /**
* Create a new IndexedDBCryptoStore * Create a new IndexedDBCryptoStore
* *
* @param {IDBFactory} indexedDB global indexedDB instance * @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to * @param {string} dbName name of db to connect to
*/ */
constructor(indexedDB, dbName) { constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
/** /**
* Ensure the database exists and is up-to-date, or fall back to * Ensure the database exists and is up-to-date, or fall back to
@@ -62,25 +83,23 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore * or a MemoryCryptoStore
*/ */
startup() { public startup(): Promise<CryptoStore> {
if (this._backendPromise) { if (this.backendPromise) {
return this._backendPromise; return this.backendPromise;
} }
this._backendPromise = new Promise((resolve, reject) => { this.backendPromise = new Promise<CryptoStore>((resolve, reject) => {
if (!this._indexedDB) { if (!this.indexedDB) {
reject(new Error('no indexeddb support available')); reject(new Error('no indexeddb support available'));
return; return;
} }
logger.log(`connecting to indexeddb ${this._dbName}`); logger.log(`connecting to indexeddb ${this.dbName}`);
const req = this._indexedDB.open( const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev) => {
const db = ev.target.result; const db = req.result;
const oldVersion = ev.oldVersion; const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion); IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
}; };
@@ -93,13 +112,13 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => { req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev); logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error); reject(req.error);
}; };
req.onsuccess = (r) => { req.onsuccess = () => {
const db = r.target.result; const db = req.result;
logger.log(`connected to indexeddb ${this._dbName}`); logger.log(`connected to indexeddb ${this.dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db)); resolve(new IndexedDBCryptoStoreBackend.Backend(db));
}; };
}).then((backend) => { }).then((backend) => {
@@ -114,9 +133,7 @@ export class IndexedDBCryptoStore {
], ],
(txn) => { (txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {}); backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => { }).then(() => backend,
return backend;
},
); );
}).catch((e) => { }).catch((e) => {
if (e.name === 'VersionError') { if (e.name === 'VersionError') {
@@ -126,7 +143,7 @@ export class IndexedDBCryptoStore {
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW); throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
} }
logger.warn( logger.warn(
`unable to connect to indexeddb ${this._dbName}` + `unable to connect to indexeddb ${this.dbName}` +
`: falling back to localStorage store: ${e}`, `: falling back to localStorage store: ${e}`,
); );
@@ -139,10 +156,11 @@ export class IndexedDBCryptoStore {
return new MemoryCryptoStore(); return new MemoryCryptoStore();
} }
}).then(backend => { }).then(backend => {
this._backend = backend; this.backend = backend;
return backend as CryptoStore;
}); });
return this._backendPromise; return this.backendPromise;
} }
/** /**
@@ -150,15 +168,15 @@ export class IndexedDBCryptoStore {
* *
* @returns {Promise} resolves when the store has been cleared. * @returns {Promise} resolves when the store has been cleared.
*/ */
deleteAllData() { public deleteAllData(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (!this._indexedDB) { if (!this.indexedDB) {
reject(new Error('no indexeddb support available')); reject(new Error('no indexeddb support available'));
return; return;
} }
logger.log(`Removing indexeddb instance: ${this._dbName}`); logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName); const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => { req.onblocked = () => {
logger.log( logger.log(
@@ -168,11 +186,11 @@ export class IndexedDBCryptoStore {
req.onerror = (ev) => { req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev); logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error); reject(req.error);
}; };
req.onsuccess = () => { req.onsuccess = () => {
logger.log(`Removed indexeddb instance: ${this._dbName}`); logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve(); resolve();
}; };
}).catch((e) => { }).catch((e) => {
@@ -193,8 +211,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one. * same instance as passed in, or the existing one.
*/ */
getOrAddOutgoingRoomKeyRequest(request) { public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
return this._backend.getOrAddOutgoingRoomKeyRequest(request); return this.backend.getOrAddOutgoingRoomKeyRequest(request);
} }
/** /**
@@ -207,8 +225,8 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found * not found
*/ */
getOutgoingRoomKeyRequest(requestBody) { public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return this._backend.getOutgoingRoomKeyRequest(requestBody); return this.backend.getOutgoingRoomKeyRequest(requestBody);
} }
/** /**
@@ -221,8 +239,8 @@ export class IndexedDBCryptoStore {
* there are no pending requests in those states. If there are multiple * there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen. * requests in those states, an arbitrary one is chosen.
*/ */
getOutgoingRoomKeyRequestByState(wantedStates) { public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates); return this.backend.getOutgoingRoomKeyRequestByState(wantedStates);
} }
/** /**
@@ -232,8 +250,8 @@ export class IndexedDBCryptoStore {
* @param {Number} wantedState * @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state * @return {Promise<Array<*>>} Returns an array of requests in the given state
*/ */
getAllOutgoingRoomKeyRequestsByState(wantedState) { public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState); return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
} }
/** /**
@@ -246,8 +264,12 @@ export class IndexedDBCryptoStore {
* @return {Promise} resolves to a list of all the * @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest} * {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/ */
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { public getOutgoingRoomKeyRequestsByTarget(
return this._backend.getOutgoingRoomKeyRequestsByTarget( userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
return this.backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates, userId, deviceId, wantedStates,
); );
} }
@@ -264,8 +286,12 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest} * {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found * updated request, or null if no matching row was found
*/ */
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { public updateOutgoingRoomKeyRequest(
return this._backend.updateOutgoingRoomKeyRequest( requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates, requestId, expectedState, updates,
); );
} }
@@ -279,8 +305,11 @@ export class IndexedDBCryptoStore {
* *
* @returns {Promise} resolves once the operation is completed * @returns {Promise} resolves once the operation is completed
*/ */
deleteOutgoingRoomKeyRequest(requestId, expectedState) { public deleteOutgoingRoomKeyRequest(
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); requestId: string,
expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
} }
// Olm Account // Olm Account
@@ -292,8 +321,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle * @param {function(string)} func Called with the account pickle
*/ */
getAccount(txn, func) { public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) {
this._backend.getAccount(txn, func); this.backend.getAccount(txn, func);
} }
/** /**
@@ -301,10 +330,10 @@ export class IndexedDBCryptoStore {
* This requires an active transaction. See doTxn(). * This requires an active transaction. See doTxn().
* *
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store. * @param {string} accountPickle The new account pickle to store.
*/ */
storeAccount(txn, newData) { public storeAccount(txn: IDBTransaction, accountPickle: string): void {
this._backend.storeAccount(txn, newData); this.backend.storeAccount(txn, accountPickle);
} }
/** /**
@@ -315,8 +344,8 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account keys object: * @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/ */
getCrossSigningKeys(txn, func) { public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
this._backend.getCrossSigningKeys(txn, func); this.backend.getCrossSigningKeys(txn, func);
} }
/** /**
@@ -324,8 +353,12 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the private key * @param {function(string)} func Called with the private key
* @param {string} type A key type * @param {string} type A key type
*/ */
getSecretStorePrivateKey(txn, func, type) { public getSecretStorePrivateKey<T>(
this._backend.getSecretStorePrivateKey(txn, func, type); txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => T,
type: string,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
} }
/** /**
@@ -334,8 +367,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
* @param {string} keys keys object as getCrossSigningKeys() * @param {string} keys keys object as getCrossSigningKeys()
*/ */
storeCrossSigningKeys(txn, keys) { public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
this._backend.storeCrossSigningKeys(txn, keys); this.backend.storeCrossSigningKeys(txn, keys);
} }
/** /**
@@ -345,8 +378,8 @@ export class IndexedDBCryptoStore {
* @param {string} type The type of cross-signing private key to store * @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys() * @param {string} key keys object as getCrossSigningKeys()
*/ */
storeSecretStorePrivateKey(txn, type, key) { public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
this._backend.storeSecretStorePrivateKey(txn, type, key); this.backend.storeSecretStorePrivateKey(txn, type, key);
} }
// Olm sessions // Olm sessions
@@ -356,8 +389,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions * @param {function(int)} func Called with the count of sessions
*/ */
countEndToEndSessions(txn, func) { public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
this._backend.countEndToEndSessions(txn, func); this.backend.countEndToEndSessions(txn, func);
} }
/** /**
@@ -372,8 +405,13 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received * timestamp in milliseconds at which the session last received
* a message. * a message.
*/ */
getEndToEndSession(deviceKey, sessionId, txn, func) { public getEndToEndSession(
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func); deviceKey: string,
sessionId: string,
txn: IDBTransaction,
func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSession(deviceKey, sessionId, txn, func);
} }
/** /**
@@ -387,8 +425,12 @@ export class IndexedDBCryptoStore {
* timestamp in milliseconds at which the session last received * timestamp in milliseconds at which the session last received
* a message. * a message.
*/ */
getEndToEndSessions(deviceKey, txn, func) { public getEndToEndSessions(
this._backend.getEndToEndSessions(deviceKey, txn, func); deviceKey: string,
txn: IDBTransaction,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
this.backend.getEndToEndSessions(deviceKey, txn, func);
} }
/** /**
@@ -398,8 +440,8 @@ export class IndexedDBCryptoStore {
* an object with, deviceKey, lastReceivedMessageTs, sessionId * an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys. * and session keys.
*/ */
getAllEndToEndSessions(txn, func) { public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void {
this._backend.getAllEndToEndSessions(txn, func); this.backend.getAllEndToEndSessions(txn, func);
} }
/** /**
@@ -409,22 +451,25 @@ export class IndexedDBCryptoStore {
* @param {string} sessionInfo Session information object * @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { public storeEndToEndSession(
this._backend.storeEndToEndSession( deviceKey: string,
deviceKey, sessionId, sessionInfo, txn, sessionId: string,
); sessionInfo: ISessionInfo,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn);
} }
storeEndToEndSessionProblem(deviceKey, type, fixed) { public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed); return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
} }
getEndToEndSessionProblem(deviceKey, timestamp) { public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp); return this.backend.getEndToEndSessionProblem(deviceKey, timestamp);
} }
filterOutNotifiedErrorDevices(devices) { public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
return this._backend.filterOutNotifiedErrorDevices(devices); return this.backend.filterOutNotifiedErrorDevices(devices);
} }
// Inbound group sessions // Inbound group sessions
@@ -438,10 +483,13 @@ export class IndexedDBCryptoStore {
* @param {function(object)} func Called with A map from sessionId * @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session. * to Base64 end-to-end session.
*/ */
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { public getEndToEndInboundGroupSession(
this._backend.getEndToEndInboundGroupSession( senderCurve25519Key: string,
senderCurve25519Key, sessionId, txn, func, sessionId: string,
); txn: IDBTransaction,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func);
} }
/** /**
@@ -451,8 +499,11 @@ export class IndexedDBCryptoStore {
* in the store with an object having keys {senderKey, sessionId, * in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list. * sessionData}, then once with null to indicate the end of the list.
*/ */
getAllEndToEndInboundGroupSessions(txn, func) { public getAllEndToEndInboundGroupSessions(
this._backend.getAllEndToEndInboundGroupSessions(txn, func); txn: IDBTransaction,
func: (session: ISession | null) => void,
): void {
this.backend.getAllEndToEndInboundGroupSessions(txn, func);
} }
/** /**
@@ -464,10 +515,13 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure * @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public addEndToEndInboundGroupSession(
this._backend.addEndToEndInboundGroupSession( senderCurve25519Key: string,
senderCurve25519Key, sessionId, sessionData, txn, sessionId: string,
); sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
} }
/** /**
@@ -479,18 +533,22 @@ export class IndexedDBCryptoStore {
* @param {object} sessionData The session data structure * @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public storeEndToEndInboundGroupSession(
this._backend.storeEndToEndInboundGroupSession( senderCurve25519Key: string,
senderCurve25519Key, sessionId, sessionData, txn, sessionId: string,
); sessionData: InboundGroupSessionData,
txn: IDBTransaction,
): void {
this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn);
} }
storeEndToEndInboundGroupSessionWithheld( public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn, senderCurve25519Key: string,
) { sessionId: string,
this._backend.storeEndToEndInboundGroupSessionWithheld( sessionData: IWithheld,
senderCurve25519Key, sessionId, sessionData, txn, txn: IDBTransaction,
); ): void {
this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn);
} }
// End-to-end device tracking // End-to-end device tracking
@@ -505,8 +563,8 @@ export class IndexedDBCryptoStore {
* @param {Object} deviceData * @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
storeEndToEndDeviceData(deviceData, txn) { public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void {
this._backend.storeEndToEndDeviceData(deviceData, txn); this.backend.storeEndToEndDeviceData(deviceData, txn);
} }
/** /**
@@ -516,8 +574,8 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the * @param {function(Object)} func Function called with the
* device data * device data
*/ */
getEndToEndDeviceData(txn, func) { public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
this._backend.getEndToEndDeviceData(txn, func); this.backend.getEndToEndDeviceData(txn, func);
} }
// End to End Rooms // End to End Rooms
@@ -528,8 +586,8 @@ export class IndexedDBCryptoStore {
* @param {object} roomInfo The end-to-end info for the room. * @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
*/ */
storeEndToEndRoom(roomId, roomInfo, txn) { public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void {
this._backend.storeEndToEndRoom(roomId, roomInfo, txn); this.backend.storeEndToEndRoom(roomId, roomInfo, txn);
} }
/** /**
@@ -537,20 +595,20 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms * @param {function(Object)} func Function called with the end to end encrypted rooms
*/ */
getEndToEndRooms(txn, func) { public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record<string, IRoomEncryption>) => void): void {
this._backend.getEndToEndRooms(txn, func); this.backend.getEndToEndRooms(txn, func);
} }
// session backups // session backups
/** /**
* Get the inbound group sessions that need to be backed up. * Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0 * @param {number} limit The maximum number of sessions to retrieve. 0
* for no limit. * for no limit.
* @returns {Promise} resolves to an array of inbound group sessions * @returns {Promise} resolves to an array of inbound group sessions
*/ */
getSessionsNeedingBackup(limit) { public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
return this._backend.getSessionsNeedingBackup(limit); return this.backend.getSessionsNeedingBackup(limit);
} }
/** /**
@@ -558,8 +616,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional) * @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions * @returns {Promise} resolves to the number of sessions
*/ */
countSessionsNeedingBackup(txn) { public countSessionsNeedingBackup(txn?: IDBTransaction): Promise<number> {
return this._backend.countSessionsNeedingBackup(txn); return this.backend.countSessionsNeedingBackup(txn);
} }
/** /**
@@ -568,8 +626,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional) * @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked * @returns {Promise} resolves when the sessions are unmarked
*/ */
unmarkSessionsNeedingBackup(sessions, txn) { public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this._backend.unmarkSessionsNeedingBackup(sessions, txn); return this.backend.unmarkSessionsNeedingBackup(sessions, txn);
} }
/** /**
@@ -578,8 +636,8 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional) * @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked * @returns {Promise} resolves when the sessions are marked
*/ */
markSessionsNeedingBackup(sessions, txn) { public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise<void> {
return this._backend.markSessionsNeedingBackup(sessions, txn); return this.backend.markSessionsNeedingBackup(sessions, txn);
} }
/** /**
@@ -589,10 +647,13 @@ export class IndexedDBCryptoStore {
* @param {string} sessionId The ID of the session * @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional) * @param {*} txn An active transaction. See doTxn(). (optional)
*/ */
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { public addSharedHistoryInboundGroupSession(
this._backend.addSharedHistoryInboundGroupSession( roomId: string,
roomId, senderKey, sessionId, txn, senderKey: string,
); sessionId: string,
txn?: IDBTransaction,
): void {
this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn);
} }
/** /**
@@ -601,8 +662,11 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn(). (optional) * @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId] * @returns {Promise} Resolves to an array of [senderKey, sessionId]
*/ */
getSharedHistoryInboundGroupSessions(roomId, txn) { public getSharedHistoryInboundGroupSessions(
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn); roomId: string,
txn?: IDBTransaction,
): Promise<[senderKey: string, sessionId: string][]> {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
} }
/** /**
@@ -627,18 +691,7 @@ export class IndexedDBCryptoStore {
* reject with that exception. On synchronous backends, the * reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method. * exception will propagate to the caller of the getFoo method.
*/ */
doTxn(mode, stores, func, log) { doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise<T> {
return this._backend.doTxn(mode, stores, func, log); return this.backend.doTxn(mode, stores, func, log);
} }
} }
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2017, 2018 New Vector Ltd Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -17,6 +16,12 @@ limitations under the License.
import { logger } from '../../logger'; import { logger } from '../../logger';
import { MemoryCryptoStore } from './memory-crypto-store'; import { MemoryCryptoStore } from './memory-crypto-store';
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/** /**
* Internal module. Partial localStorage backed storage for e2e. * Internal module. Partial localStorage backed storage for e2e.
@@ -38,23 +43,23 @@ const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.w
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) { function keyEndToEndSessions(deviceKey: string): string {
return E2E_PREFIX + "sessions/" + deviceKey; return E2E_PREFIX + "sessions/" + deviceKey;
} }
function keyEndToEndSessionProblems(deviceKey) { function keyEndToEndSessionProblems(deviceKey: string): string {
return E2E_PREFIX + "session.problems/" + deviceKey; return E2E_PREFIX + "session.problems/" + deviceKey;
} }
function keyEndToEndInboundGroupSession(senderKey, sessionId) { function keyEndToEndInboundGroupSession(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
} }
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) { function keyEndToEndInboundGroupSessionWithheld(senderKey: string, sessionId: string): string {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId; return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
} }
function keyEndToEndRoomsPrefix(roomId) { function keyEndToEndRoomsPrefix(roomId: string): string {
return KEY_ROOMS_PREFIX + roomId; return KEY_ROOMS_PREFIX + roomId;
} }
@@ -62,24 +67,23 @@ function keyEndToEndRoomsPrefix(roomId) {
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
export class LocalStorageCryptoStore extends MemoryCryptoStore { export class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) { public static exists(store: Storage): boolean {
super(); const length = store.length;
this.store = webStore;
}
static exists(webStore) {
const length = webStore.length;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if (webStore.key(i).startsWith(E2E_PREFIX)) { if (store.key(i).startsWith(E2E_PREFIX)) {
return true; return true;
} }
} }
return false; return false;
} }
constructor(private readonly store: Storage) {
super();
}
// Olm Sessions // Olm Sessions
countEndToEndSessions(txn, func) { public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
let count = 0; let count = 0;
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
@@ -87,9 +91,10 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(count); func(count);
} }
_getEndToEndSessions(deviceKey, txn, func) { // eslint-disable-next-line @typescript-eslint/naming-convention
private _getEndToEndSessions(deviceKey: string): Record<string, ISessionInfo> {
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {}; const fixedSessions: Record<string, ISessionInfo> = {};
// fix up any old sessions to be objects rather than just the base64 pickle // fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) { for (const [sid, val] of Object.entries(sessions || {})) {
@@ -105,16 +110,25 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return fixedSessions; return fixedSessions;
} }
getEndToEndSession(deviceKey, sessionId, txn, func) { public getEndToEndSession(
deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const sessions = this._getEndToEndSessions(deviceKey); const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {}); func(sessions[sessionId] || {});
} }
getEndToEndSessions(deviceKey, txn, func) { public getEndToEndSessions(
deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this._getEndToEndSessions(deviceKey) || {}); func(this._getEndToEndSessions(deviceKey) || {});
} }
getAllEndToEndSessions(txn, func) { public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1]; const deviceKey = this.store.key(i).split('/')[1];
@@ -125,17 +139,15 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
} }
} }
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
const sessions = this._getEndToEndSessions(deviceKey) || {}; const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = sessionInfo; sessions[sessionId] = sessionInfo;
setJsonItem( setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions);
this.store, keyEndToEndSessions(deviceKey), sessions,
);
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) { public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const key = keyEndToEndSessionProblems(deviceKey); const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || []; const problems = getJsonItem<IProblem[]>(this.store, key) || [];
problems.push({ type, fixed, time: Date.now() }); problems.push({ type, fixed, time: Date.now() });
problems.sort((a, b) => { problems.sort((a, b) => {
return a.time - b.time; return a.time - b.time;
@@ -143,9 +155,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, key, problems); setJsonItem(this.store, key, problems);
} }
async getEndToEndSessionProblem(deviceKey, timestamp) { async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const key = keyEndToEndSessionProblems(deviceKey); const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || []; const problems = getJsonItem<IProblem[]>(this.store, key) || [];
if (!problems.length) { if (!problems.length) {
return null; return null;
} }
@@ -162,9 +174,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
} }
} }
async filterOutNotifiedErrorDevices(devices) { public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = const notifiedErrorDevices = getJsonItem<string[]>(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = []; const ret = [];
for (const device of devices) { for (const device of devices) {
@@ -187,7 +198,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
func( func(
getJsonItem( getJsonItem(
this.store, this.store,
@@ -200,7 +216,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
getAllEndToEndInboundGroupSessions(txn, func) { public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i); const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
@@ -219,7 +235,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(null); func(null);
} }
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const existing = getJsonItem( const existing = getJsonItem(
this.store, this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -231,7 +252,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
} }
} }
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public storeEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
setJsonItem( setJsonItem(
this.store, this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
@@ -239,9 +265,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
storeEndToEndInboundGroupSessionWithheld( public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn, senderCurve25519Key: string,
) { sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void {
setJsonItem( setJsonItem(
this.store, this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
@@ -249,25 +278,19 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
getEndToEndDeviceData(txn, func) { public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
func(getJsonItem( func(getJsonItem(this.store, KEY_DEVICE_DATA));
this.store, KEY_DEVICE_DATA,
));
} }
storeEndToEndDeviceData(deviceData, txn) { public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
setJsonItem( setJsonItem(this.store, KEY_DEVICE_DATA, deviceData);
this.store, KEY_DEVICE_DATA, deviceData,
);
} }
storeEndToEndRoom(roomId, roomInfo, txn) { public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
setJsonItem( setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo);
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
} }
getEndToEndRooms(txn, func) { public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
const result = {}; const result = {};
const prefix = keyEndToEndRoomsPrefix(''); const prefix = keyEndToEndRoomsPrefix('');
@@ -281,9 +304,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(result); func(result);
} }
getSessionsNeedingBackup(limit) { public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessionsNeedingBackup const sessionsNeedingBackup = getJsonItem<string[]>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = []; const sessions = [];
for (const session in sessionsNeedingBackup) { for (const session in sessionsNeedingBackup) {
@@ -309,13 +331,12 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(sessions); return Promise.resolve(sessions);
} }
countSessionsNeedingBackup() { public countSessionsNeedingBackup(): Promise<number> {
const sessionsNeedingBackup const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length); return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
} }
unmarkSessionsNeedingBackup(sessions) { public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) { for (const session of sessions) {
@@ -327,7 +348,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(); return Promise.resolve();
} }
markSessionsNeedingBackup(sessions) { public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
const sessionsNeedingBackup const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) { for (const session of sessions) {
@@ -344,52 +365,46 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
* *
* @returns {Promise} Promise which resolves when the store has been cleared. * @returns {Promise} Promise which resolves when the store has been cleared.
*/ */
deleteAllData() { public deleteAllData(): Promise<void> {
this.store.removeItem(KEY_END_TO_END_ACCOUNT); this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve(); return Promise.resolve();
} }
// Olm account // Olm account
getAccount(txn, func) { public getAccount(txn: unknown, func: (accountPickle: string) => void): void {
const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(account); func(accountPickle);
} }
storeAccount(txn, newData) { public storeAccount(txn: unknown, accountPickle: string): void {
setJsonItem( setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
this.store, KEY_END_TO_END_ACCOUNT, newData,
);
} }
getCrossSigningKeys(txn, func) { public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys); func(keys);
} }
getSecretStorePrivateKey(txn, func, type) { public getSecretStorePrivateKey<T>(txn: unknown, func: (key: IEncryptedPayload | null) => T, type: string): T {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`); const key = getJsonItem<IEncryptedPayload>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key); func(key);
} }
storeCrossSigningKeys(txn, keys) { public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
setJsonItem( setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
} }
storeSecretStorePrivateKey(txn, type, key) { public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
setJsonItem( setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
this.store, E2E_PREFIX + `ssss_cache.${type}`, key,
);
} }
doTxn(mode, stores, func) { doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
return Promise.resolve(func(null)); return Promise.resolve(func(null));
} }
} }
function getJsonItem(store, key) { function getJsonItem<T>(store: Storage, key: string): T | null {
try { try {
// if the key is absent, store.getItem() returns null, and // if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null. // JSON.parse(null) === null, so this returns null.
@@ -401,6 +416,6 @@ function getJsonItem(store, key) {
return null; return null;
} }
function setJsonItem(store, key, val) { function setJsonItem<T>(store: Storage, key: string, val: T): void {
store.setItem(key, JSON.stringify(val)); store.setItem(key, JSON.stringify(val));
} }

View File

@@ -1,7 +1,5 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -18,6 +16,22 @@ limitations under the License.
import { logger } from '../../logger'; import { logger } from '../../logger';
import * as utils from "../../utils"; import * as utils from "../../utils";
import {
CryptoStore,
IDeviceData,
IProblem,
ISession,
ISessionInfo,
IWithheld,
Mode,
OutgoingRoomKeyRequest,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../../@types/partials";
import { IEncryptedPayload } from "../aes";
/** /**
* Internal module. in-memory storage for e2e. * Internal module. in-memory storage for e2e.
@@ -28,32 +42,22 @@ import * as utils from "../../utils";
/** /**
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
export class MemoryCryptoStore { export class MemoryCryptoStore implements CryptoStore {
constructor() { private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
this._outgoingRoomKeyRequests = []; private account: string = null;
this._account = null; private crossSigningKeys: Record<string, ICrossSigningKey> = null;
this._crossSigningKeys = null; private privateKeys: Record<string, IEncryptedPayload> = {};
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}} private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
this._sessions = {}; private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
// Map of {devicekey -> array of problems} private notifiedErrorDevices: { [userId: string]: { [deviceId: string]: boolean } } = {};
this._sessionProblems = {}; private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {};
// Map of {userId -> deviceId -> true} private inboundGroupSessionsWithheld: Record<string, IWithheld> = {};
this._notifiedErrorDevices = {}; // Opaque device data object
// Map of {senderCurve25519Key+'/'+sessionId -> session data object} private deviceData: IDeviceData = null;
this._inboundGroupSessions = {}; private rooms: { [roomId: string]: IRoomEncryption } = {};
this._inboundGroupSessionsWithheld = {}; private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
// Opaque device data object private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
// roomId -> array of [senderKey, sessionId]
this._sharedHistoryInboundGroupSessions = {};
}
/** /**
* Ensure the database exists and is up-to-date. * Ensure the database exists and is up-to-date.
@@ -62,7 +66,7 @@ export class MemoryCryptoStore {
* *
* @return {Promise} resolves to the store. * @return {Promise} resolves to the store.
*/ */
async startup() { public async startup(): Promise<CryptoStore> {
// No startup work to do for the memory store. // No startup work to do for the memory store.
return this; return this;
} }
@@ -72,7 +76,7 @@ export class MemoryCryptoStore {
* *
* @returns {Promise} Promise which resolves when the store has been cleared. * @returns {Promise} Promise which resolves when the store has been cleared.
*/ */
deleteAllData() { public deleteAllData(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
@@ -86,7 +90,7 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one. * same instance as passed in, or the existing one.
*/ */
getOrAddOutgoingRoomKeyRequest(request) { public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
const requestBody = request.requestBody; const requestBody = request.requestBody;
return utils.promiseTry(() => { return utils.promiseTry(() => {
@@ -109,7 +113,7 @@ export class MemoryCryptoStore {
`enqueueing key request for ${requestBody.room_id} / ` + `enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id, requestBody.session_id,
); );
this._outgoingRoomKeyRequests.push(request); this.outgoingRoomKeyRequests.push(request);
return request; return request;
}); });
} }
@@ -124,7 +128,7 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found * not found
*/ */
getOutgoingRoomKeyRequest(requestBody) { public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<OutgoingRoomKeyRequest | null> {
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody)); return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
} }
@@ -139,8 +143,9 @@ export class MemoryCryptoStore {
* @return {module:crypto/store/base~OutgoingRoomKeyRequest?} * @return {module:crypto/store/base~OutgoingRoomKeyRequest?}
* the matching request, or null if not found * the matching request, or null if not found
*/ */
_getOutgoingRoomKeyRequest(requestBody) { // eslint-disable-next-line @typescript-eslint/naming-convention
for (const existing of this._outgoingRoomKeyRequests) { private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
for (const existing of this.outgoingRoomKeyRequests) {
if (utils.deepCompare(existing.requestBody, requestBody)) { if (utils.deepCompare(existing.requestBody, requestBody)) {
return existing; return existing;
} }
@@ -157,8 +162,8 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states * there are no pending requests in those states
*/ */
getOutgoingRoomKeyRequestByState(wantedStates) { public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise<OutgoingRoomKeyRequest | null> {
for (const req of this._outgoingRoomKeyRequests) { for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) { for (const state of wantedStates) {
if (req.state === state) { if (req.state === state) {
return Promise.resolve(req); return Promise.resolve(req);
@@ -173,18 +178,22 @@ export class MemoryCryptoStore {
* @param {Number} wantedState * @param {Number} wantedState
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state * @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
*/ */
getAllOutgoingRoomKeyRequestsByState(wantedState) { public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise<OutgoingRoomKeyRequest[]> {
return Promise.resolve( return Promise.resolve(
this._outgoingRoomKeyRequests.filter( this.outgoingRoomKeyRequests.filter(
(r) => r.state == wantedState, (r) => r.state == wantedState,
), ),
); );
} }
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { public getOutgoingRoomKeyRequestsByTarget(
userId: string,
deviceId: string,
wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> {
const results = []; const results = [];
for (const req of this._outgoingRoomKeyRequests) { for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) { for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({ userId, deviceId })) { if (req.state === state && req.recipients.includes({ userId, deviceId })) {
results.push(req); results.push(req);
@@ -206,8 +215,12 @@ export class MemoryCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest} * {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found * updated request, or null if no matching row was found
*/ */
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { public updateOutgoingRoomKeyRequest(
for (const req of this._outgoingRoomKeyRequests) { requestId: string,
expectedState: number,
updates: Partial<OutgoingRoomKeyRequest>,
): Promise<OutgoingRoomKeyRequest | null> {
for (const req of this.outgoingRoomKeyRequests) {
if (req.requestId !== requestId) { if (req.requestId !== requestId) {
continue; continue;
} }
@@ -235,9 +248,12 @@ export class MemoryCryptoStore {
* *
* @returns {Promise} resolves once the operation is completed * @returns {Promise} resolves once the operation is completed
*/ */
deleteOutgoingRoomKeyRequest(requestId, expectedState) { public deleteOutgoingRoomKeyRequest(
for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) { requestId: string,
const req = this._outgoingRoomKeyRequests[i]; expectedState: number,
): Promise<OutgoingRoomKeyRequest | null> {
for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) {
const req = this.outgoingRoomKeyRequests[i];
if (req.requestId !== requestId) { if (req.requestId !== requestId) {
continue; continue;
@@ -251,7 +267,7 @@ export class MemoryCryptoStore {
return Promise.resolve(null); return Promise.resolve(null);
} }
this._outgoingRoomKeyRequests.splice(i, 1); this.outgoingRoomKeyRequests.splice(i, 1);
return Promise.resolve(req); return Promise.resolve(req);
} }
@@ -260,48 +276,57 @@ export class MemoryCryptoStore {
// Olm Account // Olm Account
getAccount(txn, func) { public getAccount(txn: unknown, func: (accountPickle: string) => void) {
func(this._account); func(this.account);
} }
storeAccount(txn, newData) { public storeAccount(txn: unknown, accountPickle: string): void {
this._account = newData; this.account = accountPickle;
} }
getCrossSigningKeys(txn, func) { public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
func(this._crossSigningKeys); func(this.crossSigningKeys);
} }
getSecretStorePrivateKey(txn, func, type) { public getSecretStorePrivateKey<T>(txn: unknown, func: (key: IEncryptedPayload | null) => T, type: string): T {
const result = this._privateKeys[type]; const result = this.privateKeys[type];
return func(result || null); return func(result || null);
} }
storeCrossSigningKeys(txn, keys) { public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
this._crossSigningKeys = keys; this.crossSigningKeys = keys;
} }
storeSecretStorePrivateKey(txn, type, key) { public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
this._privateKeys[type] = key; this.privateKeys[type] = key;
} }
// Olm Sessions // Olm Sessions
countEndToEndSessions(txn, func) { public countEndToEndSessions(txn: unknown, func: (count: number) => void): void {
return Object.keys(this._sessions).length; return Object.keys(this.sessions).length;
} }
getEndToEndSession(deviceKey, sessionId, txn, func) { public getEndToEndSession(
const deviceSessions = this._sessions[deviceKey] || {}; deviceKey: string,
sessionId: string,
txn: unknown,
func: (session: ISessionInfo) => void,
): void {
const deviceSessions = this.sessions[deviceKey] || {};
func(deviceSessions[sessionId] || null); func(deviceSessions[sessionId] || null);
} }
getEndToEndSessions(deviceKey, txn, func) { public getEndToEndSessions(
func(this._sessions[deviceKey] || {}); deviceKey: string,
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this.sessions[deviceKey] || {});
} }
getAllEndToEndSessions(txn, func) { public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => { Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => {
Object.entries(deviceSessions).forEach(([sessionId, session]) => { Object.entries(deviceSessions).forEach(([sessionId, session]) => {
func({ func({
...session, ...session,
@@ -312,26 +337,25 @@ export class MemoryCryptoStore {
}); });
} }
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { public storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void {
let deviceSessions = this._sessions[deviceKey]; let deviceSessions = this.sessions[deviceKey];
if (deviceSessions === undefined) { if (deviceSessions === undefined) {
deviceSessions = {}; deviceSessions = {};
this._sessions[deviceKey] = deviceSessions; this.sessions[deviceKey] = deviceSessions;
} }
deviceSessions[sessionId] = sessionInfo; deviceSessions[sessionId] = sessionInfo;
} }
async storeEndToEndSessionProblem(deviceKey, type, fixed) { public async storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
const problems = this._sessionProblems[deviceKey] const problems = this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || [];
= this._sessionProblems[deviceKey] || [];
problems.push({ type, fixed, time: Date.now() }); problems.push({ type, fixed, time: Date.now() });
problems.sort((a, b) => { problems.sort((a, b) => {
return a.time - b.time; return a.time - b.time;
}); });
} }
async getEndToEndSessionProblem(deviceKey, timestamp) { public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const problems = this._sessionProblems[deviceKey] || []; const problems = this.sessionProblems[deviceKey] || [];
if (!problems.length) { if (!problems.length) {
return null; return null;
} }
@@ -348,9 +372,9 @@ export class MemoryCryptoStore {
} }
} }
async filterOutNotifiedErrorDevices(devices) { public async filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise<IOlmDevice[]> {
const notifiedErrorDevices = this._notifiedErrorDevices; const notifiedErrorDevices = this.notifiedErrorDevices;
const ret = []; const ret: IOlmDevice[] = [];
for (const device of devices) { for (const device of devices) {
const { userId, deviceInfo } = device; const { userId, deviceInfo } = device;
@@ -370,16 +394,24 @@ export class MemoryCryptoStore {
// Inbound Group Sessions // Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { public getEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
const k = senderCurve25519Key+'/'+sessionId; const k = senderCurve25519Key+'/'+sessionId;
func( func(
this._inboundGroupSessions[k] || null, this.inboundGroupSessions[k] || null,
this._inboundGroupSessionsWithheld[k] || null, this.inboundGroupSessionsWithheld[k] || null,
); );
} }
getAllEndToEndInboundGroupSessions(txn, func) { public getAllEndToEndInboundGroupSessions(
for (const key of Object.keys(this._inboundGroupSessions)) { 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 // we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the // might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded // senderKey being a (32-byte) curve25519 key, base64-encoded
@@ -388,58 +420,71 @@ export class MemoryCryptoStore {
func({ func({
senderKey: key.substr(0, 43), senderKey: key.substr(0, 43),
sessionId: key.substr(44), sessionId: key.substr(44),
sessionData: this._inboundGroupSessions[key], sessionData: this.inboundGroupSessions[key],
}); });
} }
func(null); func(null);
} }
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public addEndToEndInboundGroupSession(
senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const k = senderCurve25519Key+'/'+sessionId; const k = senderCurve25519Key+'/'+sessionId;
if (this._inboundGroupSessions[k] === undefined) { if (this.inboundGroupSessions[k] === undefined) {
this._inboundGroupSessions[k] = sessionData; this.inboundGroupSessions[k] = sessionData;
} }
} }
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { public storeEndToEndInboundGroupSession(
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData; senderCurve25519Key: string,
sessionId: string,
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
this.inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
} }
storeEndToEndInboundGroupSessionWithheld( public storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn, senderCurve25519Key: string,
) { sessionId: string,
sessionData: IWithheld,
txn: unknown,
): void {
const k = senderCurve25519Key+'/'+sessionId; const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData; this.inboundGroupSessionsWithheld[k] = sessionData;
} }
// Device Data // Device Data
getEndToEndDeviceData(txn, func) { public getEndToEndDeviceData(txn: unknown, func: (deviceData: IDeviceData | null) => void): void {
func(this._deviceData); func(this.deviceData);
} }
storeEndToEndDeviceData(deviceData, txn) { public storeEndToEndDeviceData(deviceData: IDeviceData, txn: unknown): void {
this._deviceData = deviceData; this.deviceData = deviceData;
} }
// E2E rooms // E2E rooms
storeEndToEndRoom(roomId, roomInfo, txn) { public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: unknown): void {
this._rooms[roomId] = roomInfo; this.rooms[roomId] = roomInfo;
} }
getEndToEndRooms(txn, func) { public getEndToEndRooms(txn: unknown, func: (rooms: Record<string, IRoomEncryption>) => void): void {
func(this._rooms); func(this.rooms);
} }
getSessionsNeedingBackup(limit) { public getSessionsNeedingBackup(limit: number): Promise<ISession[]> {
const sessions = []; const sessions: ISession[] = [];
for (const session in this._sessionsNeedingBackup) { for (const session in this.sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) { if (this.inboundGroupSessions[session]) {
sessions.push({ sessions.push({
senderKey: session.substr(0, 43), senderKey: session.substr(0, 43),
sessionId: session.substr(44), sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session], sessionData: this.inboundGroupSessions[session],
}); });
if (limit && session.length >= limit) { if (limit && session.length >= limit) {
break; break;
@@ -449,39 +494,39 @@ export class MemoryCryptoStore {
return Promise.resolve(sessions); return Promise.resolve(sessions);
} }
countSessionsNeedingBackup() { public countSessionsNeedingBackup(): Promise<number> {
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length); return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length);
} }
unmarkSessionsNeedingBackup(sessions) { public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) { for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId; const sessionKey = session.senderKey + '/' + session.sessionId;
delete this._sessionsNeedingBackup[sessionKey]; delete this.sessionsNeedingBackup[sessionKey];
} }
return Promise.resolve(); return Promise.resolve();
} }
markSessionsNeedingBackup(sessions) { public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) { for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId; const sessionKey = session.senderKey + '/' + session.sessionId;
this._sessionsNeedingBackup[sessionKey] = true; this.sessionsNeedingBackup[sessionKey] = true;
} }
return Promise.resolve(); return Promise.resolve();
} }
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) { public addSharedHistoryInboundGroupSession(roomId: string, senderKey: string, sessionId: string): void {
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || []; const sessions = this.sharedHistoryInboundGroupSessions[roomId] || [];
sessions.push([senderKey, sessionId]); sessions.push([senderKey, sessionId]);
this._sharedHistoryInboundGroupSessions[roomId] = sessions; this.sharedHistoryInboundGroupSessions[roomId] = sessions;
} }
getSharedHistoryInboundGroupSessions(roomId) { public getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []); return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
} }
// Session key backups // Session key backups
doTxn(mode, stores, func) { public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
return Promise.resolve(func(null)); return Promise.resolve(func(null));
} }
} }

View File

@@ -22,8 +22,8 @@ limitations under the License.
* @param {string} dbName The database name to test for * @param {string} dbName The database name to test for
* @returns {boolean} Whether the database exists * @returns {boolean} Whether the database exists
*/ */
export function exists(indexedDB, dbName) { export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
let exists = true; let exists = true;
const req = indexedDB.open(dbName); const req = indexedDB.open(dbName);
req.onupgradeneeded = () => { req.onupgradeneeded = () => {
@@ -31,7 +31,7 @@ export function exists(indexedDB, dbName) {
// should only fire if the DB did not exist before at any version. // should only fire if the DB did not exist before at any version.
exists = false; exists = false;
}; };
req.onblocked = () => reject(); req.onblocked = () => reject(req.error);
req.onsuccess = () => { req.onsuccess = () => {
const db = req.result; const db = req.result;
db.close(); db.close();
@@ -45,6 +45,6 @@ export function exists(indexedDB, dbName) {
} }
resolve(exists); resolve(exists);
}; };
req.onerror = ev => reject(ev.target.error); req.onerror = ev => reject(req.error);
}); });
} }

View File

@@ -62,7 +62,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE); export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG); logger.setLevel(log.levels.DEBUG);
interface PrefixedLogger extends Logger { export interface PrefixedLogger extends Logger {
withPrefix?: (prefix: string) => PrefixedLogger; withPrefix?: (prefix: string) => PrefixedLogger;
prefix?: string; prefix?: string;
} }

View File

@@ -117,7 +117,7 @@ function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCurso
} }
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
public static exists(indexedDB: IDBFactory, dbName: string): boolean { public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
dbName = "matrix-js-sdk:" + (dbName || "default"); dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName); return IndexedDBHelpers.exists(indexedDB, dbName);
} }

View File

@@ -47,7 +47,7 @@ interface IOpts extends IBaseOpts {
} }
export class IndexedDBStore extends MemoryStore { export class IndexedDBStore extends MemoryStore {
static exists(indexedDB: IDBFactory, dbName: string): boolean { static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
} }