Merge pull request #1508 from matrix-org/t3chguy/ts/4
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
|
||||
@@ -21,25 +21,23 @@ import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import {
|
||||
ROOM_KEY_REQUEST_STATES,
|
||||
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
state: RoomKeyRequestState.Sent,
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
state: RoomKeyRequestState.Sent,
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C" },
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
state: RoomKeyRequestState.Unsent,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -68,9 +66,9 @@ describe.each([
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
async () => {
|
||||
const r = await
|
||||
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
|
||||
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
@@ -78,10 +76,10 @@ describe.each([
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
|
||||
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
expect(r.state).toEqual(RoomKeyRequestState.Sent);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+5
-1
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import * as Olm from "@matrix-org/olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
import "@matrix-org/olm";
|
||||
|
||||
export {};
|
||||
|
||||
@@ -34,6 +34,10 @@ declare global {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
}
|
||||
|
||||
interface Crypto {
|
||||
webkitSubtle?: Window["crypto"]["subtle"];
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
|
||||
+60
-24
@@ -47,7 +47,7 @@ import {
|
||||
PREFIX_UNSTABLE,
|
||||
retryNetworkOperation,
|
||||
} from "./http-api";
|
||||
import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, isCryptoAvailable } from './crypto';
|
||||
import { Crypto, fixBackupKey, IBootstrapCrossSigningOpts, IMegolmSessionData, isCryptoAvailable } from './crypto';
|
||||
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
|
||||
import { decodeRecoveryKey } from './crypto/recoverykey';
|
||||
import { keyFromAuthData } from './crypto/key_passphrase';
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
IKeyBackupPrepareOpts,
|
||||
IKeyBackupRestoreOpts,
|
||||
IKeyBackupRestoreResult,
|
||||
IKeyBackupVersion,
|
||||
IKeyBackupInfo,
|
||||
} from "./crypto/keybackup";
|
||||
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
|
||||
import type Request from "request";
|
||||
@@ -114,8 +114,9 @@ import url from "url";
|
||||
import { randomString } from "./randomstring";
|
||||
import { ReadStream } from "fs";
|
||||
import { WebStorageSessionStore } from "./store/session/webstorage";
|
||||
import { BackupManager, IKeyBackupCheck, TrustInfo } from "./crypto/backup";
|
||||
import { BackupManager, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace";
|
||||
import { ISignatures } from "./@types/signed";
|
||||
|
||||
export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||
export type SessionStore = WebStorageSessionStore;
|
||||
@@ -375,6 +376,39 @@ interface ICapabilities {
|
||||
"m.room_versions"?: IRoomVersionsCapability;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ICrossSigningKey {
|
||||
keys: { [algorithm: string]: string };
|
||||
signatures?: ISignatures;
|
||||
usage: string[];
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
enum CrossSigningKeyType {
|
||||
MasterKey = "master_key",
|
||||
SelfSigningKey = "self_signing_key",
|
||||
UserSigningKey = "user_signing_key",
|
||||
}
|
||||
|
||||
export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
|
||||
|
||||
export interface ISignedKey {
|
||||
keys: Record<string, string>;
|
||||
signatures: ISignatures;
|
||||
user_id: string;
|
||||
algorithms: string[];
|
||||
device_id: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
|
||||
interface IUploadKeySignaturesResponse {
|
||||
failures: Record<string, Record<string, {
|
||||
errcode: string;
|
||||
error: string;
|
||||
}>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Matrix Client. Only directly construct this if you want to use
|
||||
* custom modules. Normally, {@link createClient} should be used
|
||||
@@ -2062,7 +2096,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @return {Promise} a promise which resolves when the keys
|
||||
* have been imported
|
||||
*/
|
||||
public importRoomKeys(keys: any[], opts: IImportRoomKeysOpts): Promise<void> {
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2086,7 +2120,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* Get information about the current key backup.
|
||||
* @returns {Promise} Information object from API or null
|
||||
*/
|
||||
public getKeyBackupVersion(): Promise<IKeyBackupVersion> {
|
||||
public getKeyBackupVersion(): Promise<IKeyBackupInfo> {
|
||||
return this.http.authedRequest(
|
||||
undefined, "GET", "/room_keys/version", undefined, undefined,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
@@ -2120,7 +2154,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public isKeyBackupTrusted(info: IKeyBackupVersion): Promise<TrustInfo> {
|
||||
public isKeyBackupTrusted(info: IKeyBackupInfo): Promise<TrustInfo> {
|
||||
return this.crypto.backupManager.isKeyBackupTrusted(info);
|
||||
}
|
||||
|
||||
@@ -2143,7 +2177,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {object} info Backup information object as returned by getKeyBackupVersion
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public enableKeyBackup(info: IKeyBackupVersion): Promise<void> {
|
||||
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2180,7 +2214,7 @@ export class MatrixClient extends EventEmitter {
|
||||
public async prepareKeyBackupVersion(
|
||||
password: string,
|
||||
opts: IKeyBackupPrepareOpts = { secureSecretStorage: false },
|
||||
): Promise<IKeyBackupVersion> {
|
||||
): Promise<Pick<IPreparedKeyBackupVersion, "algorithm" | "auth_data" | "recovery_key">> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2198,7 +2232,7 @@ export class MatrixClient extends EventEmitter {
|
||||
algorithm,
|
||||
auth_data,
|
||||
recovery_key,
|
||||
} as any; // TODO: Types
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2219,7 +2253,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @returns {Promise<object>} Object with 'version' param indicating the version created
|
||||
*/
|
||||
// TODO: Fix types
|
||||
public async createKeyBackupVersion(info: IKeyBackupVersion): Promise<IKeyBackupVersion> {
|
||||
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<IKeyBackupInfo> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2313,7 +2347,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* Back up session keys to the homeserver.
|
||||
* @param {string} roomId ID of the room that the keys are for Optional.
|
||||
* @param {string} sessionId ID of the session that the keys are for Optional.
|
||||
* @param {integer} version backup version Optional.
|
||||
* @param {number} version backup version Optional.
|
||||
* @param {object} data Object keys to send
|
||||
* @return {Promise} a promise that will resolve when the keys
|
||||
* are uploaded
|
||||
@@ -2375,7 +2409,7 @@ export class MatrixClient extends EventEmitter {
|
||||
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
|
||||
* @return {Promise<Uint8Array>} key backup key
|
||||
*/
|
||||
public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupVersion): Promise<Uint8Array> {
|
||||
public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise<Uint8Array> {
|
||||
return keyFromAuthData(backupInfo.auth_data, password);
|
||||
}
|
||||
|
||||
@@ -2410,7 +2444,7 @@ export class MatrixClient extends EventEmitter {
|
||||
password: string,
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupVersion,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult> {
|
||||
const privKey = await keyFromAuthData(backupInfo.auth_data, password);
|
||||
@@ -2434,7 +2468,7 @@ export class MatrixClient extends EventEmitter {
|
||||
*/
|
||||
// TODO: Types
|
||||
public async restoreKeyBackupWithSecretStorage(
|
||||
backupInfo: IKeyBackupVersion,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
targetRoomId?: string,
|
||||
targetSessionId?: string,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
@@ -2474,20 +2508,18 @@ export class MatrixClient extends EventEmitter {
|
||||
recoveryKey: string,
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupVersion,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult> {
|
||||
const privKey = decodeRecoveryKey(recoveryKey);
|
||||
return this.restoreKeyBackup(
|
||||
privKey, targetRoomId, targetSessionId, backupInfo, opts,
|
||||
);
|
||||
return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
|
||||
}
|
||||
|
||||
// TODO: Types
|
||||
public async restoreKeyBackupWithCache(
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupVersion,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult> {
|
||||
const privKey = await this.crypto.getSessionBackupPrivateKey();
|
||||
@@ -2498,10 +2530,10 @@ export class MatrixClient extends EventEmitter {
|
||||
}
|
||||
|
||||
private async restoreKeyBackup(
|
||||
privKey: Uint8Array,
|
||||
privKey: ArrayLike<number>,
|
||||
targetRoomId: string,
|
||||
targetSessionId: string,
|
||||
backupInfo: IKeyBackupVersion,
|
||||
backupInfo: IKeyBackupInfo,
|
||||
opts?: IKeyBackupRestoreOpts,
|
||||
): Promise<IKeyBackupRestoreResult> {
|
||||
const cacheCompleteCallback = opts?.cacheCompleteCallback;
|
||||
@@ -7092,7 +7124,7 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.authedRequest(callback, "POST", "/keys/upload", undefined, content);
|
||||
}
|
||||
|
||||
public uploadKeySignatures(content: any): Promise<any> { // TODO: Types
|
||||
public uploadKeySignatures(content: KeySignatures): Promise<IUploadKeySignaturesResponse> {
|
||||
return this.http.authedRequest(
|
||||
undefined, "POST", '/keys/signatures/upload', undefined,
|
||||
content, {
|
||||
@@ -7191,7 +7223,7 @@ export class MatrixClient extends EventEmitter {
|
||||
return this.http.authedRequest(undefined, "GET", path, qps, undefined);
|
||||
}
|
||||
|
||||
public uploadDeviceSigningKeys(auth: any, keys: any): Promise<any> { // TODO: Lots of types
|
||||
public uploadDeviceSigningKeys(auth: any, keys: CrossSigningKeys): Promise<{}> { // TODO: types
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, { auth });
|
||||
return this.http.authedRequest(
|
||||
@@ -7603,7 +7635,11 @@ export class MatrixClient extends EventEmitter {
|
||||
* supplied.
|
||||
* @return {Promise} Resolves to the result object
|
||||
*/
|
||||
public sendToDevice(eventType: string, contentMap: any, txnId?: string): Promise<any> { // TODO: Types
|
||||
public sendToDevice(
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any>; } },
|
||||
txnId?: string,
|
||||
): Promise<{}> {
|
||||
const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
|
||||
$eventType: eventType,
|
||||
$txnId: txnId ? txnId : this.makeTxnId(),
|
||||
|
||||
+15
-14
@@ -28,26 +28,27 @@ import { decryptAES, encryptAES } from './aes';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { CryptoStore, MatrixClient } from "../client";
|
||||
import { CryptoStore, ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types
|
||||
function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
return Object.values(keyInfo.keys)[0];
|
||||
}
|
||||
|
||||
interface ICacheCallbacks {
|
||||
export interface ICacheCallbacks {
|
||||
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
|
||||
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
public keys: Record<string, any> = {}; // TODO types
|
||||
public keys: Record<string, ICrossSigningKey> = {};
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
// When you verify a user, any devices online at the time that receive
|
||||
@@ -296,7 +297,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
|
||||
const privateKeys: Record<string, Uint8Array> = {};
|
||||
const keys: Record<string, object> = {};
|
||||
const keys: Record<string, any> = {}; // TODO types
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
@@ -368,8 +369,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
this.keys = {};
|
||||
}
|
||||
|
||||
public setKeys(keys: Record<string, any>): void {
|
||||
const signingKeys: Record<string, object> = {};
|
||||
public setKeys(keys: Record<string, ICrossSigningKey>): void {
|
||||
const signingKeys: Record<string, ICrossSigningKey> = {};
|
||||
if (keys.master) {
|
||||
if (keys.master.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
@@ -448,7 +449,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public async signObject<T extends object>(data: T, type: string): Promise<T> {
|
||||
public async signObject<T extends object>(data: T, type: string): Promise<T & { signatures: ISignatures }> {
|
||||
if (!this.keys[type]) {
|
||||
throw new Error(
|
||||
"Attempted to sign with " + type + " key but no such key present",
|
||||
@@ -457,13 +458,13 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
const [pubkey, signing] = await this.getCrossSigningKey(type);
|
||||
try {
|
||||
pkSign(data, signing, this.userId, pubkey);
|
||||
return data;
|
||||
return data as T & { signatures: ISignatures };
|
||||
} finally {
|
||||
signing.free();
|
||||
}
|
||||
}
|
||||
|
||||
public async signUser(key: CrossSigningInfo): Promise<object> {
|
||||
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey> {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
@@ -471,7 +472,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
}
|
||||
|
||||
public async signDevice(userId: string, device: DeviceInfo): Promise<object> {
|
||||
public async signDevice(userId: string, device: DeviceInfo): Promise<ISignedKey> {
|
||||
if (userId !== this.userId) {
|
||||
throw new Error(
|
||||
`Trying to sign ${userId}'s device; can only sign our own device`,
|
||||
@@ -481,7 +482,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
logger.info("No self signing key: not signing device");
|
||||
return;
|
||||
}
|
||||
return this.signObject(
|
||||
return this.signObject<Omit<ISignedKey, "signatures">>(
|
||||
{
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
@@ -719,12 +720,12 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
|
||||
);
|
||||
}
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
key = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
store.storeSecretStorePrivateKey(txn, type, key);
|
||||
store.storeSecretStorePrivateKey(txn, type, encryptedKey);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ enum TrackingStatus {
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
type DeviceInfoMap = Record<string, Record<string, IDevice>>;
|
||||
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
|
||||
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
@@ -70,7 +70,7 @@ export class DeviceList extends EventEmitter {
|
||||
// [device info]
|
||||
// }
|
||||
// }
|
||||
private devices: DeviceInfoMap = {};
|
||||
private devices: Record<string, Record<string, IDevice>> = {};
|
||||
|
||||
// userId -> {
|
||||
// [key info]
|
||||
@@ -315,7 +315,7 @@ export class DeviceList extends EventEmitter {
|
||||
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
|
||||
const stored = {};
|
||||
const stored: DeviceInfoMap = {};
|
||||
userIds.map((u) => {
|
||||
stored[u] = {};
|
||||
const devices = this.getStoredDevicesForUser(u) || [];
|
||||
@@ -463,27 +463,11 @@ export class DeviceList extends EventEmitter {
|
||||
/**
|
||||
* Replaces the list of devices for a user with the given device list
|
||||
*
|
||||
* @param {string} u The user ID
|
||||
* @param {Object} devs New device info for user
|
||||
* @param {string} userId The user ID
|
||||
* @param {Object} devices New device info for user
|
||||
*/
|
||||
public storeDevicesForUser(u: string, devs: Record<string, IDevice>): void {
|
||||
// remove previous devices from userByIdentityKey
|
||||
if (this.devices[u] !== undefined) {
|
||||
for (const [deviceId, dev] of Object.entries(this.devices[u])) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
delete this.userByIdentityKey[identityKey];
|
||||
}
|
||||
}
|
||||
|
||||
this.devices[u] = devs;
|
||||
|
||||
// add new ones
|
||||
for (const [deviceId, dev] of Object.entries(devs)) {
|
||||
const identityKey = dev.keys['curve25519:'+deviceId];
|
||||
|
||||
this.userByIdentityKey[identityKey] = u;
|
||||
}
|
||||
public storeDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
|
||||
this.setRawStoredDevicesForUser(userId, devices);
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
@@ -859,7 +843,7 @@ class DeviceListUpdateSerialiser {
|
||||
);
|
||||
|
||||
// put the updates into the object that will be returned as our results
|
||||
const storage = {};
|
||||
const storage: Record<string, IDevice> = {};
|
||||
Object.keys(userStore).forEach((deviceId) => {
|
||||
storage[deviceId] = userStore[deviceId].toStorage();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
import { createCryptoStoreCacheCallbacks } from "./CrossSigning";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { PREFIX_UNSTABLE } from "../http-api";
|
||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||
import {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISecretStorageKeyInfo,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
keys: Record<string, ICrossSigningKey>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
|
||||
@@ -17,18 +30,23 @@ import {
|
||||
* more than once.
|
||||
*/
|
||||
export class EncryptionSetupBuilder {
|
||||
public readonly accountDataClientAdapter: AccountDataClientAdapter;
|
||||
public readonly crossSigningCallbacks: CrossSigningCallbacks;
|
||||
public readonly ssssCryptoCallbacks: SSSSCryptoCallbacks;
|
||||
|
||||
private crossSigningKeys: ICrossSigningKeys = null;
|
||||
private keySignatures: KeySignatures = null;
|
||||
private keyBackupInfo: IKeyBackupInfo = null;
|
||||
private sessionBackupPrivateKey: Uint8Array;
|
||||
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData, delegateCryptoCallbacks) {
|
||||
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks: ICryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
|
||||
this._crossSigningKeys = null;
|
||||
this._keySignatures = null;
|
||||
this._keyBackupInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +60,8 @@ export class EncryptionSetupBuilder {
|
||||
* an empty authDict, to obtain the flows.
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(authUpload, keys) {
|
||||
this._crossSigningKeys = { authUpload, keys };
|
||||
public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void {
|
||||
this.crossSigningKeys = { authUpload, keys };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +72,8 @@ export class EncryptionSetupBuilder {
|
||||
*
|
||||
* @param {Object} keyBackupInfo as received from/sent to the server
|
||||
*/
|
||||
addSessionBackup(keyBackupInfo) {
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void {
|
||||
this.keyBackupInfo = keyBackupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,8 +83,8 @@ export class EncryptionSetupBuilder {
|
||||
*
|
||||
* @param {Uint8Array} privateKey
|
||||
*/
|
||||
addSessionBackupPrivateKeyToCache(privateKey) {
|
||||
this._sessionBackupPrivateKey = privateKey;
|
||||
public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void {
|
||||
this.sessionBackupPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,14 +93,14 @@ export class EncryptionSetupBuilder {
|
||||
*
|
||||
* @param {String} userId
|
||||
* @param {String} deviceId
|
||||
* @param {String} signature
|
||||
* @param {Object} signature
|
||||
*/
|
||||
addKeySignature(userId, deviceId, signature) {
|
||||
if (!this._keySignatures) {
|
||||
this._keySignatures = {};
|
||||
public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void {
|
||||
if (!this.keySignatures) {
|
||||
this.keySignatures = {};
|
||||
}
|
||||
const userSignatures = this._keySignatures[userId] || {};
|
||||
this._keySignatures[userId] = userSignatures;
|
||||
const userSignatures = this.keySignatures[userId] || {};
|
||||
this.keySignatures[userId] = userSignatures;
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
@@ -91,7 +109,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
public setAccountData(type: string, content: object): Promise<void> {
|
||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
||||
}
|
||||
|
||||
@@ -99,13 +117,13 @@ export class EncryptionSetupBuilder {
|
||||
* builds the operation containing all the parts that have been added to the builder
|
||||
* @return {EncryptionSetupOperation}
|
||||
*/
|
||||
buildOperation() {
|
||||
const accountData = this.accountDataClientAdapter._values;
|
||||
public buildOperation(): EncryptionSetupOperation {
|
||||
const accountData = this.accountDataClientAdapter.values;
|
||||
return new EncryptionSetupOperation(
|
||||
accountData,
|
||||
this._crossSigningKeys,
|
||||
this._keyBackupInfo,
|
||||
this._keySignatures,
|
||||
this.crossSigningKeys,
|
||||
this.keyBackupInfo,
|
||||
this.keySignatures,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,9 +136,9 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Crypto} crypto
|
||||
* @return {Promise}
|
||||
*/
|
||||
async persist(crypto) {
|
||||
public async persist(crypto: Crypto): Promise<void> {
|
||||
// store private keys in cache
|
||||
if (this._crossSigningKeys) {
|
||||
if (this.crossSigningKeys) {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice);
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
@@ -132,13 +150,13 @@ export class EncryptionSetupBuilder {
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, this._crossSigningKeys.keys);
|
||||
txn, this.crossSigningKeys.keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
// store session backup key in cache
|
||||
if (this._sessionBackupPrivateKey) {
|
||||
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
|
||||
if (this.sessionBackupPrivateKey) {
|
||||
await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,58 +174,58 @@ export class EncryptionSetupOperation {
|
||||
* @param {Object} keyBackupInfo
|
||||
* @param {Object} keySignatures
|
||||
*/
|
||||
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
|
||||
this._accountData = accountData;
|
||||
this._crossSigningKeys = crossSigningKeys;
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
this._keySignatures = keySignatures;
|
||||
}
|
||||
constructor(
|
||||
private readonly accountData: Map<string, object>,
|
||||
private readonly crossSigningKeys: ICrossSigningKeys,
|
||||
private readonly keyBackupInfo: IKeyBackupInfo,
|
||||
private readonly keySignatures: KeySignatures,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Runs the (remaining part of, in the future) operation by sending requests to the server.
|
||||
* @param {Crypto} crypto
|
||||
* @param {Crypto} crypto
|
||||
*/
|
||||
async apply(crypto) {
|
||||
public async apply(crypto: Crypto): Promise<void> {
|
||||
const baseApis = crypto.baseApis;
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
||||
if (this.crossSigningKeys) {
|
||||
const keys: Partial<CrossSigningKeys> = {};
|
||||
for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
|
||||
// We must only call `uploadDeviceSigningKeys` from inside this auth
|
||||
// helper to ensure we properly handle auth errors.
|
||||
await this._crossSigningKeys.authUpload(authDict => {
|
||||
return baseApis.uploadDeviceSigningKeys(authDict, keys);
|
||||
await this.crossSigningKeys.authUpload(authDict => {
|
||||
return baseApis.uploadDeviceSigningKeys(authDict, keys as CrossSigningKeys);
|
||||
});
|
||||
|
||||
// pass the new keys to the main instance of our own CrossSigningInfo.
|
||||
crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys);
|
||||
}
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
for (const [type, content] of this._accountData) {
|
||||
if (this.accountData) {
|
||||
for (const [type, content] of this.accountData) {
|
||||
await baseApis.setAccountData(type, content);
|
||||
}
|
||||
}
|
||||
// upload first cross-signing signatures with the new key
|
||||
// (e.g. signing our own device)
|
||||
if (this._keySignatures) {
|
||||
await baseApis.uploadKeySignatures(this._keySignatures);
|
||||
if (this.keySignatures) {
|
||||
await baseApis.uploadKeySignatures(this.keySignatures);
|
||||
}
|
||||
// need to create/update key backup info
|
||||
if (this._keyBackupInfo) {
|
||||
if (this._keyBackupInfo.version) {
|
||||
if (this.keyBackupInfo) {
|
||||
if (this.keyBackupInfo.version) {
|
||||
// session backup signature
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, "PUT", "/room_keys/version/" + this.keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
algorithm: this.keyBackupInfo.algorithm,
|
||||
auth_data: this.keyBackupInfo.auth_data,
|
||||
},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
@@ -215,7 +233,7 @@ export class EncryptionSetupOperation {
|
||||
// add new key backup
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
undefined, this.keyBackupInfo,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
}
|
||||
@@ -228,20 +246,20 @@ export class EncryptionSetupOperation {
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
public readonly values = new Map<string, object>();
|
||||
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData existing account data
|
||||
* @param {Object.<String, MatrixEvent>} existingValues existing account data
|
||||
*/
|
||||
constructor(accountData) {
|
||||
constructor(private readonly existingValues: Record<string, MatrixEvent>) {
|
||||
super();
|
||||
this._existingValues = accountData;
|
||||
this._values = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
getAccountDataFromServer(type) {
|
||||
public getAccountDataFromServer(type: string): Promise<object> {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
}
|
||||
|
||||
@@ -249,12 +267,12 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
getAccountData(type) {
|
||||
const modifiedValue = this._values.get(type);
|
||||
public getAccountData(type: string): object {
|
||||
const modifiedValue = this.values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
}
|
||||
const existingValue = this._existingValues[type];
|
||||
const existingValue = this.existingValues[type];
|
||||
if (existingValue) {
|
||||
return existingValue.getContent();
|
||||
}
|
||||
@@ -266,9 +284,9 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
const lastEvent = this._values.get(type);
|
||||
this._values.set(type, content);
|
||||
public setAccountData(type: string, content: object): Promise<void> {
|
||||
const lastEvent = this.values.get(type);
|
||||
this.values.set(type, content);
|
||||
// ensure accountData is emitted on the next tick,
|
||||
// as SecretStorage listens for it while calling this method
|
||||
// and it seems to rely on this.
|
||||
@@ -284,27 +302,25 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
|
||||
* See CrossSigningInfo constructor
|
||||
*/
|
||||
class CrossSigningCallbacks {
|
||||
constructor() {
|
||||
this.privateKeys = new Map();
|
||||
}
|
||||
class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
|
||||
public readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
// cache callbacks
|
||||
getCrossSigningKeyCache(type, expectedPublicKey) {
|
||||
public getCrossSigningKeyCache(type: string, expectedPublicKey: string): Promise<Uint8Array> {
|
||||
return this.getCrossSigningKey(type, expectedPublicKey);
|
||||
}
|
||||
|
||||
storeCrossSigningKeyCache(type, key) {
|
||||
public storeCrossSigningKeyCache(type: string, key: Uint8Array): Promise<void> {
|
||||
this.privateKeys.set(type, key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// non-cache callbacks
|
||||
getCrossSigningKey(type, _expectedPubkey) {
|
||||
public getCrossSigningKey(type: string, expectedPubkey: string): Promise<Uint8Array> {
|
||||
return Promise.resolve(this.privateKeys.get(type));
|
||||
}
|
||||
|
||||
saveCrossSigningKeys(privateKeys) {
|
||||
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
|
||||
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
||||
this.privateKeys.set(type, privateKey);
|
||||
}
|
||||
@@ -316,39 +332,36 @@ class CrossSigningCallbacks {
|
||||
* the SecretStorage crypto callbacks
|
||||
*/
|
||||
class SSSSCryptoCallbacks {
|
||||
constructor(delegateCryptoCallbacks) {
|
||||
this._privateKeys = new Map();
|
||||
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
|
||||
}
|
||||
private readonly privateKeys = new Map<string, Uint8Array>();
|
||||
|
||||
async getSecretStorageKey({ keys }, name) {
|
||||
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, object> },
|
||||
name: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this._privateKeys.get(keyId);
|
||||
const privateKey = this.privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
return [keyId, privateKey];
|
||||
}
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
if (this.delegateCryptoCallbacks) {
|
||||
const result = await this.delegateCryptoCallbacks.
|
||||
getSecretStorageKey({ keys }, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
this.privateKeys.set(keyId, privateKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, keyInfo, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
|
||||
this.privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
if (
|
||||
this._delegateCryptoCallbacks &&
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
||||
) {
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
|
||||
}
|
||||
this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
|
||||
}
|
||||
}
|
||||
+171
-172
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { CryptoStore, MatrixClient } from "../client";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
|
||||
import { OutgoingRoomKeyRequest } from './store/base';
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
/**
|
||||
* Internal module. Management of outgoing room key requests.
|
||||
@@ -57,61 +61,58 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
export const ROOM_KEY_REQUEST_STATES = {
|
||||
export enum RoomKeyRequestState {
|
||||
/** request not yet sent */
|
||||
UNSENT: 0,
|
||||
|
||||
Unsent,
|
||||
/** request sent, awaiting reply */
|
||||
SENT: 1,
|
||||
|
||||
Sent,
|
||||
/** reply received, cancellation not yet sent */
|
||||
CANCELLATION_PENDING: 2,
|
||||
|
||||
CancellationPending,
|
||||
/**
|
||||
* Cancellation not yet sent and will transition to UNSENT instead of
|
||||
* being deleted once the cancellation has been sent.
|
||||
*/
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
|
||||
};
|
||||
CancellationPendingAndWillResend,
|
||||
}
|
||||
|
||||
export class OutgoingRoomKeyRequestManager {
|
||||
constructor(baseApis, deviceId, cryptoStore) {
|
||||
this._baseApis = baseApis;
|
||||
this._deviceId = deviceId;
|
||||
this._cryptoStore = cryptoStore;
|
||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||
// if the callback has been set, or if it is still running.
|
||||
private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null;
|
||||
|
||||
// handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null
|
||||
// if the callback has been set, or if it is still running.
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequests
|
||||
private sendOutgoingRoomKeyRequestsRunning = false;
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of _sendOutgoingRoomKeyRequests
|
||||
this._sendOutgoingRoomKeyRequestsRunning = false;
|
||||
private clientRunning = false;
|
||||
|
||||
this._clientRunning = false;
|
||||
}
|
||||
constructor(
|
||||
private readonly baseApis: MatrixClient,
|
||||
private readonly deviceId: string,
|
||||
private readonly cryptoStore: CryptoStore,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
start() {
|
||||
this._clientRunning = true;
|
||||
public start(): void {
|
||||
this.clientRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
stop() {
|
||||
public stop(): void {
|
||||
logger.log('stopping OutgoingRoomKeyRequestManager');
|
||||
// stop the timer on the next run
|
||||
this._clientRunning = false;
|
||||
this.clientRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send any requests that have been queued
|
||||
*/
|
||||
sendQueuedRequests() {
|
||||
this._startTimer();
|
||||
public sendQueuedRequests(): void {
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,95 +132,99 @@ export class OutgoingRoomKeyRequestManager {
|
||||
* pending list (or we have established that a similar request already
|
||||
* exists)
|
||||
*/
|
||||
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
|
||||
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||
public async queueRoomKeyRequest(
|
||||
requestBody: IRoomKeyRequestBody,
|
||||
recipients: IRoomKeyRequestRecipient[],
|
||||
resend = false,
|
||||
): Promise<void> {
|
||||
const req = await this.cryptoStore.getOutgoingRoomKeyRequest(
|
||||
requestBody,
|
||||
);
|
||||
if (!req) {
|
||||
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
|
||||
await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({
|
||||
requestBody: requestBody,
|
||||
recipients: recipients,
|
||||
requestId: this._baseApis.makeTxnId(),
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
requestId: this.baseApis.makeTxnId(),
|
||||
state: RoomKeyRequestState.Unsent,
|
||||
});
|
||||
} else {
|
||||
switch (req.state) {
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
|
||||
case ROOM_KEY_REQUEST_STATES.UNSENT:
|
||||
// nothing to do here, since we're going to send a request anyways
|
||||
return;
|
||||
case RoomKeyRequestState.CancellationPendingAndWillResend:
|
||||
case RoomKeyRequestState.Unsent:
|
||||
// nothing to do here, since we're going to send a request anyways
|
||||
return;
|
||||
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
|
||||
// existing request is about to be cancelled. If we want to
|
||||
// resend, then change the state so that it resends after
|
||||
// cancelling. Otherwise, just cancel the cancellation.
|
||||
const state = resend ?
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
|
||||
ROOM_KEY_REQUEST_STATES.SENT;
|
||||
await this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
|
||||
state,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ROOM_KEY_REQUEST_STATES.SENT: {
|
||||
// a request has already been sent. If we don't want to
|
||||
// resend, then do nothing. If we do want to, then cancel the
|
||||
// existing request and send a new one.
|
||||
if (resend) {
|
||||
const state =
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
const updatedReq =
|
||||
await this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
|
||||
state,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
// need to use a new transaction ID so that
|
||||
// the request gets sent
|
||||
requestTxnId: this._baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
if (!updatedReq) {
|
||||
// updateOutgoingRoomKeyRequest couldn't find the request
|
||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||
// raced with another tab to mark the request cancelled.
|
||||
// Try again, to make sure the request is resent.
|
||||
return await this.queueRoomKeyRequest(
|
||||
requestBody, recipients, resend,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't want to wait for the timer, so we send it
|
||||
// immediately. (We might actually end up racing with the timer,
|
||||
// but that's ok: even if we make the request twice, we'll do it
|
||||
// with the same transaction_id, so only one message will get
|
||||
// sent).
|
||||
//
|
||||
// (We also don't want to wait for the response from the server
|
||||
// here, as it will slow down processing of received keys if we
|
||||
// do.)
|
||||
try {
|
||||
await this._sendOutgoingRoomKeyRequestCancellation(
|
||||
updatedReq,
|
||||
true,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
}
|
||||
// The request has transitioned from
|
||||
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
|
||||
// still need to resend the request which is now UNSENT, so
|
||||
// start the timer if it isn't already started.
|
||||
case RoomKeyRequestState.CancellationPending: {
|
||||
// existing request is about to be cancelled. If we want to
|
||||
// resend, then change the state so that it resends after
|
||||
// cancelling. Otherwise, just cancel the cancellation.
|
||||
const state = resend ?
|
||||
RoomKeyRequestState.CancellationPendingAndWillResend :
|
||||
RoomKeyRequestState.Sent;
|
||||
await this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.CancellationPending, {
|
||||
state,
|
||||
cancellationTxnId: this.baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('unhandled state: ' + req.state);
|
||||
case RoomKeyRequestState.Sent: {
|
||||
// a request has already been sent. If we don't want to
|
||||
// resend, then do nothing. If we do want to, then cancel the
|
||||
// existing request and send a new one.
|
||||
if (resend) {
|
||||
const state =
|
||||
RoomKeyRequestState.CancellationPendingAndWillResend;
|
||||
const updatedReq =
|
||||
await this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.Sent, {
|
||||
state,
|
||||
cancellationTxnId: this.baseApis.makeTxnId(),
|
||||
// need to use a new transaction ID so that
|
||||
// the request gets sent
|
||||
requestTxnId: this.baseApis.makeTxnId(),
|
||||
},
|
||||
);
|
||||
if (!updatedReq) {
|
||||
// updateOutgoingRoomKeyRequest couldn't find the request
|
||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||
// raced with another tab to mark the request cancelled.
|
||||
// Try again, to make sure the request is resent.
|
||||
return await this.queueRoomKeyRequest(
|
||||
requestBody, recipients, resend,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't want to wait for the timer, so we send it
|
||||
// immediately. (We might actually end up racing with the timer,
|
||||
// but that's ok: even if we make the request twice, we'll do it
|
||||
// with the same transaction_id, so only one message will get
|
||||
// sent).
|
||||
//
|
||||
// (We also don't want to wait for the response from the server
|
||||
// here, as it will slow down processing of received keys if we
|
||||
// do.)
|
||||
try {
|
||||
await this.sendOutgoingRoomKeyRequestCancellation(
|
||||
updatedReq,
|
||||
true,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
}
|
||||
// The request has transitioned from
|
||||
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
|
||||
// still need to resend the request which is now UNSENT, so
|
||||
// start the timer if it isn't already started.
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('unhandled state: ' + req.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,8 +237,8 @@ export class OutgoingRoomKeyRequestManager {
|
||||
* @returns {Promise} resolves when the request has been updated in our
|
||||
* pending list.
|
||||
*/
|
||||
cancelRoomKeyRequest(requestBody) {
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||
public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise<void> {
|
||||
return this.cryptoStore.getOutgoingRoomKeyRequest(
|
||||
requestBody,
|
||||
).then((req) => {
|
||||
if (!req) {
|
||||
@@ -241,12 +246,12 @@ export class OutgoingRoomKeyRequestManager {
|
||||
return;
|
||||
}
|
||||
switch (req.state) {
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
|
||||
case RoomKeyRequestState.CancellationPending:
|
||||
case RoomKeyRequestState.CancellationPendingAndWillResend:
|
||||
// nothing to do here
|
||||
return;
|
||||
|
||||
case ROOM_KEY_REQUEST_STATES.UNSENT:
|
||||
case RoomKeyRequestState.Unsent:
|
||||
// just delete it
|
||||
|
||||
// FIXME: ghahah we may have attempted to send it, and
|
||||
@@ -258,16 +263,16 @@ export class OutgoingRoomKeyRequestManager {
|
||||
'deleting unnecessary room key request for ' +
|
||||
stringifyRequestBody(requestBody),
|
||||
);
|
||||
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
return this.cryptoStore.deleteOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.Unsent,
|
||||
);
|
||||
|
||||
case ROOM_KEY_REQUEST_STATES.SENT: {
|
||||
case RoomKeyRequestState.Sent: {
|
||||
// send a cancellation.
|
||||
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
|
||||
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
cancellationTxnId: this._baseApis.makeTxnId(),
|
||||
return this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.Sent, {
|
||||
state: RoomKeyRequestState.CancellationPending,
|
||||
cancellationTxnId: this.baseApis.makeTxnId(),
|
||||
},
|
||||
).then((updatedReq) => {
|
||||
if (!updatedReq) {
|
||||
@@ -294,14 +299,14 @@ export class OutgoingRoomKeyRequestManager {
|
||||
// (We also don't want to wait for the response from the server
|
||||
// here, as it will slow down processing of received keys if we
|
||||
// do.)
|
||||
this._sendOutgoingRoomKeyRequestCancellation(
|
||||
this.sendOutgoingRoomKeyRequestCancellation(
|
||||
updatedReq,
|
||||
).catch((e) => {
|
||||
logger.error(
|
||||
"Error sending room key request cancellation;"
|
||||
+ " will retry later.", e,
|
||||
);
|
||||
this._startTimer();
|
||||
this.startTimer();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -320,10 +325,8 @@ export class OutgoingRoomKeyRequestManager {
|
||||
* @return {Promise} resolves to a list of all the
|
||||
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||
*/
|
||||
getOutgoingSentRoomKeyRequest(userId, deviceId) {
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
|
||||
);
|
||||
public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): OutgoingRoomKeyRequest[] {
|
||||
return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,29 +336,27 @@ export class OutgoingRoomKeyRequestManager {
|
||||
* For example, after initialization or self-verification.
|
||||
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
|
||||
*/
|
||||
async cancelAndResendAllOutgoingRequests() {
|
||||
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
|
||||
ROOM_KEY_REQUEST_STATES.SENT,
|
||||
);
|
||||
public async cancelAndResendAllOutgoingRequests(): Promise<void[]> {
|
||||
const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
|
||||
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
|
||||
this.queueRoomKeyRequest(requestBody, recipients, true)));
|
||||
}
|
||||
|
||||
// start the background timer to send queued requests, if the timer isn't
|
||||
// already running
|
||||
_startTimer() {
|
||||
if (this._sendOutgoingRoomKeyRequestsTimer) {
|
||||
private startTimer(): void {
|
||||
if (this.sendOutgoingRoomKeyRequestsTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startSendingOutgoingRoomKeyRequests = () => {
|
||||
if (this._sendOutgoingRoomKeyRequestsRunning) {
|
||||
if (this.sendOutgoingRoomKeyRequestsRunning) {
|
||||
throw new Error("RoomKeyRequestSend already in progress!");
|
||||
}
|
||||
this._sendOutgoingRoomKeyRequestsRunning = true;
|
||||
this.sendOutgoingRoomKeyRequestsRunning = true;
|
||||
|
||||
this._sendOutgoingRoomKeyRequests().finally(() => {
|
||||
this._sendOutgoingRoomKeyRequestsRunning = false;
|
||||
this.sendOutgoingRoomKeyRequests().finally(() => {
|
||||
this.sendOutgoingRoomKeyRequestsRunning = false;
|
||||
}).catch((e) => {
|
||||
// this should only happen if there is an indexeddb error,
|
||||
// in which case we're a bit stuffed anyway.
|
||||
@@ -365,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
});
|
||||
};
|
||||
|
||||
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||
startSendingOutgoingRoomKeyRequests,
|
||||
SEND_KEY_REQUESTS_DELAY_MS,
|
||||
);
|
||||
@@ -374,47 +375,47 @@ export class OutgoingRoomKeyRequestManager {
|
||||
// look for and send any queued requests. Runs itself recursively until
|
||||
// there are no more requests, or there is an error (in which case, the
|
||||
// timer will be restarted before the promise resolves).
|
||||
_sendOutgoingRoomKeyRequests() {
|
||||
if (!this._clientRunning) {
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
private sendOutgoingRoomKeyRequests(): Promise<void> {
|
||||
if (!this.clientRunning) {
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
]).then((req) => {
|
||||
return this.cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
RoomKeyRequestState.CancellationPending,
|
||||
RoomKeyRequestState.CancellationPendingAndWillResend,
|
||||
RoomKeyRequestState.Unsent,
|
||||
]).then((req: OutgoingRoomKeyRequest) => {
|
||||
if (!req) {
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let prom;
|
||||
switch (req.state) {
|
||||
case ROOM_KEY_REQUEST_STATES.UNSENT:
|
||||
prom = this._sendOutgoingRoomKeyRequest(req);
|
||||
case RoomKeyRequestState.Unsent:
|
||||
prom = this.sendOutgoingRoomKeyRequest(req);
|
||||
break;
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
|
||||
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
|
||||
case RoomKeyRequestState.CancellationPending:
|
||||
prom = this.sendOutgoingRoomKeyRequestCancellation(req);
|
||||
break;
|
||||
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
|
||||
prom = this._sendOutgoingRoomKeyRequestCancellation(req, true);
|
||||
case RoomKeyRequestState.CancellationPendingAndWillResend:
|
||||
prom = this.sendOutgoingRoomKeyRequestCancellation(req, true);
|
||||
break;
|
||||
}
|
||||
|
||||
return prom.then(() => {
|
||||
// go around the loop again
|
||||
return this._sendOutgoingRoomKeyRequests();
|
||||
return this.sendOutgoingRoomKeyRequests();
|
||||
}).catch((e) => {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this.sendOutgoingRoomKeyRequestsTimer = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// given a RoomKeyRequest, send it and update the request record
|
||||
_sendOutgoingRoomKeyRequest(req) {
|
||||
private sendOutgoingRoomKeyRequest(req: OutgoingRoomKeyRequest): Promise<void> {
|
||||
logger.log(
|
||||
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
|
||||
` from ${stringifyRecipientList(req.recipients)}` +
|
||||
@@ -423,24 +424,24 @@ export class OutgoingRoomKeyRequestManager {
|
||||
|
||||
const requestMessage = {
|
||||
action: "request",
|
||||
requesting_device_id: this._deviceId,
|
||||
requesting_device_id: this.deviceId,
|
||||
request_id: req.requestId,
|
||||
body: req.requestBody,
|
||||
};
|
||||
|
||||
return this._sendMessageToDevices(
|
||||
return this.sendMessageToDevices(
|
||||
requestMessage, req.recipients, req.requestTxnId || req.requestId,
|
||||
).then(() => {
|
||||
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
{ state: ROOM_KEY_REQUEST_STATES.SENT },
|
||||
return this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.Unsent,
|
||||
{ state: RoomKeyRequestState.Sent },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Given a RoomKeyRequest, cancel it and delete the request record unless
|
||||
// andResend is set, in which case transition to UNSENT.
|
||||
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
|
||||
private sendOutgoingRoomKeyRequestCancellation(req: OutgoingRoomKeyRequest, andResend = false): Promise<void> {
|
||||
logger.log(
|
||||
`Sending cancellation for key request for ` +
|
||||
`${stringifyRequestBody(req.requestBody)} to ` +
|
||||
@@ -450,30 +451,30 @@ export class OutgoingRoomKeyRequestManager {
|
||||
|
||||
const requestMessage = {
|
||||
action: "request_cancellation",
|
||||
requesting_device_id: this._deviceId,
|
||||
requesting_device_id: this.deviceId,
|
||||
request_id: req.requestId,
|
||||
};
|
||||
|
||||
return this._sendMessageToDevices(
|
||||
return this.sendMessageToDevices(
|
||||
requestMessage, req.recipients, req.cancellationTxnId,
|
||||
).then(() => {
|
||||
if (andResend) {
|
||||
// We want to resend, so transition to UNSENT
|
||||
return this._cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
return this.cryptoStore.updateOutgoingRoomKeyRequest(
|
||||
req.requestId,
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
{ state: ROOM_KEY_REQUEST_STATES.UNSENT },
|
||||
RoomKeyRequestState.CancellationPendingAndWillResend,
|
||||
{ state: RoomKeyRequestState.Unsent },
|
||||
);
|
||||
}
|
||||
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
|
||||
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
return this.cryptoStore.deleteOutgoingRoomKeyRequest(
|
||||
req.requestId, RoomKeyRequestState.CancellationPending,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// send a RoomKeyRequest to a list of recipients
|
||||
_sendMessageToDevices(message, recipients, txnId) {
|
||||
const contentMap = {};
|
||||
private sendMessageToDevices(message, recipients, txnId: string): Promise<{}> {
|
||||
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
|
||||
for (const recip of recipients) {
|
||||
if (!contentMap[recip.userId]) {
|
||||
contentMap[recip.userId] = {};
|
||||
@@ -481,9 +482,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
contentMap[recip.userId][recip.deviceId] = message;
|
||||
}
|
||||
|
||||
return this._baseApis.sendToDevice(
|
||||
'm.room_key_request', contentMap, txnId,
|
||||
);
|
||||
return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 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.
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BinaryLike } from "crypto";
|
||||
|
||||
import { getCrypto } from '../utils';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
|
||||
@@ -21,7 +23,13 @@ const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
|
||||
// salt for HKDF, with 8 bytes of zeros
|
||||
const zerosalt = new Uint8Array(8);
|
||||
const zeroSalt = new Uint8Array(8);
|
||||
|
||||
export interface IEncryptedPayload {
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
mac: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt a string in Node.js
|
||||
@@ -31,7 +39,7 @@ const zerosalt = new Uint8Array(8);
|
||||
* @param {string} name the name of the secret
|
||||
* @param {string} ivStr the initialization vector to use
|
||||
*/
|
||||
async function encryptNode(data, key, name, ivStr) {
|
||||
async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
@@ -52,15 +60,17 @@ async function encryptNode(data, key, name, ivStr) {
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
|
||||
const ciphertext = cipher.update(data, "utf-8", "base64")
|
||||
+ cipher.final("base64");
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(data, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(ciphertext, "base64").digest("base64");
|
||||
.update(ciphertext).digest("base64");
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: ciphertext,
|
||||
ciphertext: ciphertext.toString("base64"),
|
||||
mac: hmac,
|
||||
};
|
||||
}
|
||||
@@ -75,7 +85,7 @@ async function encryptNode(data, key, name, ivStr) {
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptNode(data, key, name) {
|
||||
async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
@@ -84,7 +94,8 @@ async function decryptNode(data, key, name) {
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, '');
|
||||
.update(Buffer.from(data.ciphertext, "base64"))
|
||||
.digest("base64").replace(/=+$/g, '');
|
||||
|
||||
if (hmac !== data.mac.replace(/=+$/g, '')) {
|
||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||
@@ -93,21 +104,20 @@ async function decryptNode(data, key, name) {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-256-ctr", aesKey, decodeBase64(data.iv),
|
||||
);
|
||||
return decipher.update(data.ciphertext, "base64", "utf-8")
|
||||
+ decipher.final("utf-8");
|
||||
return decipher.update(data.ciphertext, "base64", "utf8")
|
||||
+ decipher.final("utf8");
|
||||
}
|
||||
|
||||
function deriveKeysNode(key, name) {
|
||||
function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] {
|
||||
const crypto = getCrypto();
|
||||
const prk = crypto.createHmac("sha256", zerosalt)
|
||||
.update(key).digest();
|
||||
const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest();
|
||||
|
||||
const b = Buffer.alloc(1, 1);
|
||||
const aesKey = crypto.createHmac("sha256", prk)
|
||||
.update(name, "utf-8").update(b).digest();
|
||||
.update(name, "utf8").update(b).digest();
|
||||
b[0] = 2;
|
||||
const hmacKey = crypto.createHmac("sha256", prk)
|
||||
.update(aesKey).update(name, "utf-8").update(b).digest();
|
||||
.update(aesKey).update(name, "utf8").update(b).digest();
|
||||
|
||||
return [aesKey, hmacKey];
|
||||
}
|
||||
@@ -120,7 +130,7 @@ function deriveKeysNode(key, name) {
|
||||
* @param {string} name the name of the secret
|
||||
* @param {string} ivStr the initialization vector to use
|
||||
*/
|
||||
async function encryptBrowser(data, key, name, ivStr) {
|
||||
async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
|
||||
let iv;
|
||||
if (ivStr) {
|
||||
iv = decodeBase64(ivStr);
|
||||
@@ -170,7 +180,7 @@ async function encryptBrowser(data, key, name, ivStr) {
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptBrowser(data, key, name) {
|
||||
async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
|
||||
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
|
||||
|
||||
const ciphertext = decodeBase64(data.ciphertext);
|
||||
@@ -197,7 +207,7 @@ async function decryptBrowser(data, key, name) {
|
||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
async function deriveKeysBrowser(key, name) {
|
||||
async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
|
||||
const hkdfkey = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
@@ -208,7 +218,9 @@ async function deriveKeysBrowser(key, name) {
|
||||
const keybits = await subtleCrypto.deriveBits(
|
||||
{
|
||||
name: "HKDF",
|
||||
salt: zerosalt,
|
||||
salt: zeroSalt,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
|
||||
info: (new TextEncoder().encode(name)),
|
||||
hash: "SHA-256",
|
||||
},
|
||||
@@ -241,11 +253,11 @@ async function deriveKeysBrowser(key, name) {
|
||||
return await Promise.all([aesProm, hmacProm]);
|
||||
}
|
||||
|
||||
export function encryptAES(...args) {
|
||||
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
|
||||
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
|
||||
return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr);
|
||||
}
|
||||
|
||||
export function decryptAES(...args) {
|
||||
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
|
||||
export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
|
||||
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2016 - 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.
|
||||
@@ -20,13 +20,22 @@ limitations under the License.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../client";
|
||||
import { Room } from "../../models/room";
|
||||
import { OlmDevice } from "../OlmDevice";
|
||||
import { MatrixEvent, RoomMember } from "../..";
|
||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. A map from string to {@link
|
||||
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
||||
*/
|
||||
export const ENCRYPTION_CLASSES = {};
|
||||
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {};
|
||||
|
||||
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. Map from string to {@link
|
||||
@@ -34,7 +43,17 @@ export const ENCRYPTION_CLASSES = {};
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||
*/
|
||||
export const DECRYPTION_CLASSES = {};
|
||||
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
|
||||
|
||||
interface IParams {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
crypto: Crypto;
|
||||
olmDevice: OlmDevice;
|
||||
baseApis: MatrixClient;
|
||||
roomId: string;
|
||||
config: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* base type for encryption implementations
|
||||
@@ -50,14 +69,21 @@ export const DECRYPTION_CLASSES = {};
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
export class EncryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._baseApis = params.baseApis;
|
||||
this._roomId = params.roomId;
|
||||
export abstract class EncryptionAlgorithm {
|
||||
protected readonly userId: string;
|
||||
protected readonly deviceId: string;
|
||||
protected readonly crypto: Crypto;
|
||||
protected readonly olmDevice: OlmDevice;
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: IParams) {
|
||||
this.userId = params.userId;
|
||||
this.deviceId = params.deviceId;
|
||||
this.crypto = params.crypto;
|
||||
this.olmDevice = params.olmDevice;
|
||||
this.baseApis = params.baseApis;
|
||||
this.roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,21 +92,22 @@ export class EncryptionAlgorithm {
|
||||
*
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room) {
|
||||
}
|
||||
public prepareToEncrypt(room: Room): void {}
|
||||
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
|
||||
* @public
|
||||
* @abstract
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
* @param {object} content event content
|
||||
*
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
public abstract encryptMessage(room: Room, eventType: string, content: object): Promise<object>;
|
||||
|
||||
/**
|
||||
* Called when the membership of a member of the room changes.
|
||||
@@ -89,9 +116,18 @@ export class EncryptionAlgorithm {
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
* @public
|
||||
* @abstract
|
||||
*/
|
||||
onRoomMembership(event, member, oldMembership) {
|
||||
}
|
||||
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
|
||||
|
||||
public reshareKeyWithDevice?(
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
device: DeviceInfo,
|
||||
): Promise<void>;
|
||||
|
||||
public forceDiscardSession?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,13 +142,19 @@ export class EncryptionAlgorithm {
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
export class DecryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._baseApis = params.baseApis;
|
||||
this._roomId = params.roomId;
|
||||
export abstract class DecryptionAlgorithm {
|
||||
protected readonly userId: string;
|
||||
protected readonly crypto: Crypto;
|
||||
protected readonly olmDevice: OlmDevice;
|
||||
protected readonly baseApis: MatrixClient;
|
||||
protected readonly roomId: string;
|
||||
|
||||
constructor(params: DecryptionClassParams) {
|
||||
this.userId = params.userId;
|
||||
this.crypto = params.crypto;
|
||||
this.olmDevice = params.olmDevice;
|
||||
this.baseApis = params.baseApis;
|
||||
this.roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,6 +169,7 @@ export class DecryptionAlgorithm {
|
||||
* resolves once we have finished decrypting. Rejects with an
|
||||
* `algorithms.DecryptionError` if there is a problem decrypting the event.
|
||||
*/
|
||||
public abstract decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
||||
|
||||
/**
|
||||
* Handle a key event
|
||||
@@ -135,7 +178,7 @@ export class DecryptionAlgorithm {
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} params event key event
|
||||
*/
|
||||
onRoomKeyEvent(params) {
|
||||
public onRoomKeyEvent(params: MatrixEvent): void {
|
||||
// ignore by default
|
||||
}
|
||||
|
||||
@@ -143,8 +186,9 @@ export class DecryptionAlgorithm {
|
||||
* Import a room key
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
* @param {object} opts object
|
||||
*/
|
||||
importRoomKey(session) {
|
||||
public async importRoomKey(session: IMegolmSessionData, opts: object): Promise<void> {
|
||||
// ignore by default
|
||||
}
|
||||
|
||||
@@ -155,7 +199,7 @@ export class DecryptionAlgorithm {
|
||||
* @return {Promise<boolean>} true if we have the keys and could (theoretically) share
|
||||
* them; else false.
|
||||
*/
|
||||
hasKeysForKeyRequest(keyRequest) {
|
||||
public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
@@ -164,7 +208,7 @@ export class DecryptionAlgorithm {
|
||||
*
|
||||
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
|
||||
*/
|
||||
shareKeysWithDevice(keyRequest) {
|
||||
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
|
||||
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
|
||||
}
|
||||
|
||||
@@ -174,9 +218,13 @@ export class DecryptionAlgorithm {
|
||||
*
|
||||
* @param {string} senderKey the sender's key
|
||||
*/
|
||||
async retryDecryptionFromSender(senderKey) {
|
||||
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
||||
// ignore by default
|
||||
return false;
|
||||
}
|
||||
|
||||
public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
|
||||
public sendSharedHistoryInboundSessions?(devicesByUser: Record<string, DeviceInfo[]>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,22 +239,21 @@ export class DecryptionAlgorithm {
|
||||
* @extends Error
|
||||
*/
|
||||
export class DecryptionError extends Error {
|
||||
constructor(code, msg, details) {
|
||||
public readonly detailedString: string;
|
||||
|
||||
constructor(public readonly code: string, msg: string, details?: Record<string, string>) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.name = 'DecryptionError';
|
||||
this.detailedString = _detailedStringForDecryptionError(this, details);
|
||||
this.detailedString = detailedStringForDecryptionError(this, details);
|
||||
}
|
||||
}
|
||||
|
||||
function _detailedStringForDecryptionError(err, details) {
|
||||
function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string {
|
||||
let result = err.name + '[msg: ' + err.message;
|
||||
|
||||
if (details) {
|
||||
result += ', ' +
|
||||
Object.keys(details).map(
|
||||
(k) => k + ': ' + details[k],
|
||||
).join(', ');
|
||||
result += ', ' + Object.keys(details).map((k) => k + ': ' + details[k]).join(', ');
|
||||
}
|
||||
|
||||
result += ']';
|
||||
@@ -224,7 +271,7 @@ function _detailedStringForDecryptionError(err, details) {
|
||||
* @extends Error
|
||||
*/
|
||||
export class UnknownDeviceError extends Error {
|
||||
constructor(msg, devices) {
|
||||
constructor(msg: string, public readonly devices: Record<string, Record<string, object>>) {
|
||||
super(msg);
|
||||
this.name = "UnknownDeviceError";
|
||||
this.devices = devices;
|
||||
@@ -244,7 +291,11 @@ export class UnknownDeviceError extends Error {
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* implementation
|
||||
*/
|
||||
export function registerAlgorithm(algorithm, encryptor, decryptor) {
|
||||
export function registerAlgorithm(
|
||||
algorithm: string,
|
||||
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
||||
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
||||
): void {
|
||||
ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 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.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,361 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import { polyfillSuper } from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
registerAlgorithm,
|
||||
} from "./base";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
/**
|
||||
* Olm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*/
|
||||
function OlmEncryption(params) {
|
||||
polyfillSuper(this, EncryptionAlgorithm, params);
|
||||
this._sessionPrepared = false;
|
||||
this._prepPromise = null;
|
||||
}
|
||||
utils.inherits(OlmEncryption, EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._sessionPrepared) {
|
||||
// prep already done
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const self = this;
|
||||
this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function(res) {
|
||||
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
}).then(function() {
|
||||
self._sessionPrepared = true;
|
||||
}).finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} content plaintext event content
|
||||
*
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
// just as you are sending a secret message?
|
||||
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
|
||||
const users = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
const self = this;
|
||||
await this._ensureSession(users);
|
||||
|
||||
const payloadFields = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
const devices = self._crypto.getStoredDevicesForUser(userId);
|
||||
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const deviceInfo = devices[j];
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
self._userId, self._deviceId, self._olmDevice,
|
||||
userId, deviceInfo, payloadFields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises).then(() => encryptedContent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Olm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||
*/
|
||||
function OlmDecryption(params) {
|
||||
polyfillSuper(this, DecryptionAlgorithm, params);
|
||||
}
|
||||
utils.inherits(OlmDecryption, DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* returns a promise which resolves to a
|
||||
* {@link module:crypto~EventDecryptionResult} once we have finished
|
||||
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
|
||||
* problem decrypting the event.
|
||||
*/
|
||||
OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
const content = event.getWireContent();
|
||||
const deviceKey = content.sender_key;
|
||||
const ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new DecryptionError(
|
||||
"OLM_MISSING_CIPHERTEXT",
|
||||
"Missing ciphertext",
|
||||
);
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
|
||||
throw new DecryptionError(
|
||||
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
|
||||
"Not included in recipients",
|
||||
);
|
||||
}
|
||||
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||
let payloadString;
|
||||
|
||||
try {
|
||||
payloadString = await this._decryptMessage(deviceKey, message);
|
||||
} catch (e) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ENCRYPTED_MESSAGE",
|
||||
"Bad Encrypted Message", {
|
||||
sender: deviceKey,
|
||||
err: e,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(payloadString);
|
||||
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this._userId) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT",
|
||||
"Message was intented for " + payload.recipient,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT_KEY",
|
||||
"Message not intended for this device", {
|
||||
intended: payload.recipient_keys.ed25519,
|
||||
our_key: this._olmDevice.deviceEd25519Key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// check that the original sender matches what the homeserver told us, to
|
||||
// avoid people masquerading as others.
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender != event.getSender()) {
|
||||
throw new DecryptionError(
|
||||
"OLM_FORWARDED_MESSAGE",
|
||||
"Message forwarded from " + payload.sender, {
|
||||
reported_sender: event.getSender(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ROOM",
|
||||
"Message intended for room " + payload.room_id, {
|
||||
reported_room: event.room_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const claimedKeys = payload.keys || {};
|
||||
|
||||
return {
|
||||
clearEvent: payload,
|
||||
senderCurve25519Key: deviceKey,
|
||||
claimedEd25519Key: claimedKeys.ed25519 || null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to decrypt an Olm message
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
|
||||
* @param {object} message message object, with 'type' and 'body' fields
|
||||
*
|
||||
* @return {string} payload, if decrypted successfully.
|
||||
*/
|
||||
OlmDecryption.prototype._decryptMessage = async function(
|
||||
theirDeviceIdentityKey, message,
|
||||
) {
|
||||
// This is a wrapper that serialises decryptions of prekey messages, because
|
||||
// otherwise we race between deciding we have no active sessions for the message
|
||||
// and creating a new one, which we can only do once because it removes the OTK.
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message: we can safely just try & decrypt it
|
||||
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
} else {
|
||||
const myPromise = this._olmDevice._olmPrekeyPromise.then(() => {
|
||||
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
});
|
||||
// we want the error, but don't propagate it to the next decryption
|
||||
this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||
return await myPromise;
|
||||
}
|
||||
};
|
||||
|
||||
OlmDecryption.prototype._reallyDecryptMessage = async function(
|
||||
theirDeviceIdentityKey, message,
|
||||
) {
|
||||
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
|
||||
theirDeviceIdentityKey,
|
||||
);
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
try {
|
||||
const payload = await this._olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
logger.log(
|
||||
"Decrypted Olm message from " + theirDeviceIdentityKey +
|
||||
" with session " + sessionId,
|
||||
);
|
||||
return payload;
|
||||
} catch (e) {
|
||||
const foundSession = await this._olmDevice.matchesSession(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
|
||||
if (foundSession) {
|
||||
// decryption failed, but it was a prekey message matching this
|
||||
// session, so it should have worked.
|
||||
throw new Error(
|
||||
"Error decrypting prekey message with existing session id " +
|
||||
sessionId + ": " + e.message,
|
||||
);
|
||||
}
|
||||
|
||||
// otherwise it's probably a message for another session; carry on, but
|
||||
// keep a record of the error
|
||||
decryptionErrors[sessionId] = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message, so it should have matched an existing session, but it
|
||||
// didn't work.
|
||||
|
||||
if (sessionIds.length === 0) {
|
||||
throw new Error("No existing sessions");
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Error decrypting non-prekey message with existing sessions: " +
|
||||
JSON.stringify(decryptionErrors),
|
||||
);
|
||||
}
|
||||
|
||||
// prekey message which doesn't match any existing sessions: make a new
|
||||
// session.
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await this._olmDevice.createInboundSession(
|
||||
theirDeviceIdentityKey, message.type, message.body,
|
||||
);
|
||||
} catch (e) {
|
||||
decryptionErrors["(new)"] = e.message;
|
||||
throw new Error(
|
||||
"Error decrypting prekey message: " +
|
||||
JSON.stringify(decryptionErrors),
|
||||
);
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"created new inbound Olm session ID " +
|
||||
res.session_id + " with " + theirDeviceIdentityKey,
|
||||
);
|
||||
return res.payload;
|
||||
};
|
||||
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
Copyright 2016 - 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import * as olmlib from "../olmlib";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
registerAlgorithm,
|
||||
} from "./base";
|
||||
import { Room } from '../../models/room';
|
||||
import { MatrixEvent } from "../..";
|
||||
import { IEventDecryptionResult } from "../index";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
interface IMessage {
|
||||
type: number | string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Olm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*/
|
||||
class OlmEncryption extends EncryptionAlgorithm {
|
||||
private sessionPrepared = false;
|
||||
private prepPromise: Promise<void> = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
private ensureSession(roomMembers: string[]): Promise<void> {
|
||||
if (this.prepPromise) {
|
||||
// prep already in progress
|
||||
return this.prepPromise;
|
||||
}
|
||||
|
||||
if (this.sessionPrepared) {
|
||||
// prep already done
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.prepPromise = this.crypto.downloadKeys(roomMembers).then((res) => {
|
||||
return this.crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
}).then(() => {
|
||||
this.sessionPrepared = true;
|
||||
}).finally(() => {
|
||||
this.prepPromise = null;
|
||||
});
|
||||
|
||||
return this.prepPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} content plaintext event content
|
||||
*
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
public async encryptMessage(room: Room, eventType: string, content: object): Promise<object> {
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
// just as you are sending a secret message?
|
||||
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
|
||||
const users = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
await this.ensureSession(users);
|
||||
|
||||
const payloadFields = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
const userId = users[i];
|
||||
const devices = this.crypto.getStoredDevicesForUser(userId);
|
||||
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const deviceInfo = devices[j];
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
if (key == this.olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this.userId, this.deviceId, this.olmDevice,
|
||||
userId, deviceInfo, payloadFields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises).then(() => encryptedContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Olm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||
*/
|
||||
class OlmDecryption extends DecryptionAlgorithm {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* returns a promise which resolves to a
|
||||
* {@link module:crypto~EventDecryptionResult} once we have finished
|
||||
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
|
||||
* problem decrypting the event.
|
||||
*/
|
||||
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
|
||||
const content = event.getWireContent();
|
||||
const deviceKey = content.sender_key;
|
||||
const ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new DecryptionError(
|
||||
"OLM_MISSING_CIPHERTEXT",
|
||||
"Missing ciphertext",
|
||||
);
|
||||
}
|
||||
|
||||
if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) {
|
||||
throw new DecryptionError(
|
||||
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
|
||||
"Not included in recipients",
|
||||
);
|
||||
}
|
||||
const message = ciphertext[this.olmDevice.deviceCurve25519Key];
|
||||
let payloadString;
|
||||
|
||||
try {
|
||||
payloadString = await this.decryptMessage(deviceKey, message);
|
||||
} catch (e) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ENCRYPTED_MESSAGE",
|
||||
"Bad Encrypted Message", {
|
||||
sender: deviceKey,
|
||||
err: e,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(payloadString);
|
||||
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this.userId) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT",
|
||||
"Message was intented for " + payload.recipient,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT_KEY",
|
||||
"Message not intended for this device", {
|
||||
intended: payload.recipient_keys.ed25519,
|
||||
our_key: this.olmDevice.deviceEd25519Key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// check that the original sender matches what the homeserver told us, to
|
||||
// avoid people masquerading as others.
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender != event.getSender()) {
|
||||
throw new DecryptionError(
|
||||
"OLM_FORWARDED_MESSAGE",
|
||||
"Message forwarded from " + payload.sender, {
|
||||
reported_sender: event.getSender(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ROOM",
|
||||
"Message intended for room " + payload.room_id, {
|
||||
reported_room: event.getRoomId(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const claimedKeys = payload.keys || {};
|
||||
|
||||
return {
|
||||
clearEvent: payload,
|
||||
senderCurve25519Key: deviceKey,
|
||||
claimedEd25519Key: claimedKeys.ed25519 || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decrypt an Olm message
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
|
||||
* @param {object} message message object, with 'type' and 'body' fields
|
||||
*
|
||||
* @return {string} payload, if decrypted successfully.
|
||||
*/
|
||||
private async decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
|
||||
// This is a wrapper that serialises decryptions of prekey messages, because
|
||||
// otherwise we race between deciding we have no active sessions for the message
|
||||
// and creating a new one, which we can only do once because it removes the OTK.
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message: we can safely just try & decrypt it
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
} else {
|
||||
const myPromise = this.olmDevice._olmPrekeyPromise.then(() => {
|
||||
return this.reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
});
|
||||
// we want the error, but don't propagate it to the next decryption
|
||||
this.olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||
return await myPromise;
|
||||
}
|
||||
}
|
||||
|
||||
private async reallyDecryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise<string> {
|
||||
const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
try {
|
||||
const payload = await this.olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
logger.log(
|
||||
"Decrypted Olm message from " + theirDeviceIdentityKey +
|
||||
" with session " + sessionId,
|
||||
);
|
||||
return payload;
|
||||
} catch (e) {
|
||||
const foundSession = await this.olmDevice.matchesSession(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
|
||||
if (foundSession) {
|
||||
// decryption failed, but it was a prekey message matching this
|
||||
// session, so it should have worked.
|
||||
throw new Error(
|
||||
"Error decrypting prekey message with existing session id " +
|
||||
sessionId + ": " + e.message,
|
||||
);
|
||||
}
|
||||
|
||||
// otherwise it's probably a message for another session; carry on, but
|
||||
// keep a record of the error
|
||||
decryptionErrors[sessionId] = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message, so it should have matched an existing session, but it
|
||||
// didn't work.
|
||||
|
||||
if (sessionIds.length === 0) {
|
||||
throw new Error("No existing sessions");
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Error decrypting non-prekey message with existing sessions: " +
|
||||
JSON.stringify(decryptionErrors),
|
||||
);
|
||||
}
|
||||
|
||||
// prekey message which doesn't match any existing sessions: make a new
|
||||
// session.
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await this.olmDevice.createInboundSession(
|
||||
theirDeviceIdentityKey, message.type, message.body,
|
||||
);
|
||||
} catch (e) {
|
||||
decryptionErrors["(new)"] = e.message;
|
||||
throw new Error(
|
||||
"Error decrypting prekey message: " +
|
||||
JSON.stringify(decryptionErrors),
|
||||
);
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"created new inbound Olm session ID " +
|
||||
res.session_id + " with " + theirDeviceIdentityKey,
|
||||
);
|
||||
return res.payload;
|
||||
}
|
||||
}
|
||||
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
+2
-2
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { IKeyBackupVersion } from "./keybackup";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
@@ -85,7 +85,7 @@ export interface ICreateSecretStorageOpts {
|
||||
* The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
*/
|
||||
keyBackupInfo?: IKeyBackupVersion;
|
||||
keyBackupInfo?: IKeyBackupInfo;
|
||||
|
||||
/**
|
||||
* If true, a new key backup version will be
|
||||
|
||||
+25
-20
@@ -29,16 +29,11 @@ import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
type AuthData = Record<string, any>;
|
||||
|
||||
type BackupInfo = {
|
||||
algorithm: string,
|
||||
auth_data: AuthData, // eslint-disable-line camelcase
|
||||
[properties: string]: any,
|
||||
};
|
||||
type AuthData = IKeyBackupInfo["auth_data"];
|
||||
|
||||
type SigInfo = {
|
||||
deviceId: string,
|
||||
@@ -54,13 +49,22 @@ export type TrustInfo = {
|
||||
};
|
||||
|
||||
export interface IKeyBackupCheck {
|
||||
backupInfo: BackupInfo;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
trustInfo: TrustInfo;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IPreparedKeyBackupVersion {
|
||||
algorithm: string;
|
||||
auth_data: AuthData;
|
||||
recovery_key: string;
|
||||
privateKey: Uint8Array;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/** A function used to get the secret key for a backup.
|
||||
*/
|
||||
type GetKey = () => Promise<Uint8Array>;
|
||||
type GetKey = () => Promise<ArrayLike<number>>;
|
||||
|
||||
interface BackupAlgorithmClass {
|
||||
algorithmName: string;
|
||||
@@ -77,7 +81,7 @@ interface BackupAlgorithm {
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: Uint8Array): Promise<boolean>;
|
||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
@@ -86,7 +90,7 @@ interface BackupAlgorithm {
|
||||
*/
|
||||
export class BackupManager {
|
||||
private algorithm: BackupAlgorithm | undefined;
|
||||
public backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version
|
||||
public backupInfo: IKeyBackupInfo | undefined; // The info dict from /room_keys/version
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
@@ -98,7 +102,7 @@ export class BackupManager {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
|
||||
public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
@@ -106,7 +110,7 @@ export class BackupManager {
|
||||
return await Algorithm.init(info.auth_data, getKey);
|
||||
}
|
||||
|
||||
public async enableKeyBackup(info: BackupInfo): Promise<void> {
|
||||
public async enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
|
||||
this.backupInfo = info;
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
@@ -145,7 +149,8 @@ export class BackupManager {
|
||||
public async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
): Promise<BackupInfo> {
|
||||
// eslint-disable-next-line camelcase
|
||||
): Promise<IPreparedKeyBackupVersion> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
@@ -161,7 +166,7 @@ export class BackupManager {
|
||||
};
|
||||
}
|
||||
|
||||
public async createKeyBackupVersion(info: BackupInfo): Promise<void> {
|
||||
public async createKeyBackupVersion(info: IKeyBackupInfo): Promise<void> {
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
}
|
||||
|
||||
@@ -171,14 +176,14 @@ export class BackupManager {
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
|
||||
public async checkAndStart(): Promise<IKeyBackupCheck> {
|
||||
logger.log("Checking key backup status...");
|
||||
if (this.baseApis.isGuest()) {
|
||||
logger.log("Skipping key backup check since user is guest");
|
||||
this.checkedForBackup = true;
|
||||
return null;
|
||||
}
|
||||
let backupInfo: BackupInfo;
|
||||
let backupInfo: IKeyBackupInfo;
|
||||
try {
|
||||
backupInfo = await this.baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
@@ -255,7 +260,7 @@ export class BackupManager {
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise<TrustInfo> {
|
||||
public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise<TrustInfo> {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
@@ -569,7 +574,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: AuthData = {};
|
||||
const authData: Partial<AuthData> = {};
|
||||
if (!key) {
|
||||
authData.public_key = decryption.generate_key();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
@@ -585,7 +590,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
|
||||
return [
|
||||
decryption.get_private_key(),
|
||||
authData,
|
||||
authData as AuthData,
|
||||
];
|
||||
} finally {
|
||||
decryption.free();
|
||||
|
||||
@@ -44,7 +44,7 @@ interface DeviceKeys {
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
interface OneTimeKey {
|
||||
export interface OneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
/**
|
||||
* @module crypto/deviceinfo
|
||||
*/
|
||||
@@ -24,7 +26,7 @@ export interface IDevice {
|
||||
verified: DeviceVerification;
|
||||
known: boolean;
|
||||
unsigned?: Record<string, any>;
|
||||
signatures?: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
enum DeviceVerification {
|
||||
@@ -90,7 +92,7 @@ export class DeviceInfo {
|
||||
public verified = DeviceVerification.Unverified;
|
||||
public known = false;
|
||||
public unsigned: Record<string, any> = {};
|
||||
public signatures: Record<string, string> = {};
|
||||
public signatures: ISignatures = {};
|
||||
|
||||
constructor(public readonly deviceId: string) {}
|
||||
|
||||
|
||||
+26
-40
@@ -28,7 +28,7 @@ import { ReEmitter } from '../ReEmitter';
|
||||
import { logger } from '../logger';
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import * as olmlib from "./olmlib";
|
||||
import { DeviceList } from "./DeviceList";
|
||||
import { DeviceInfoMap, DeviceList } from "./DeviceList";
|
||||
import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||
@@ -52,10 +52,11 @@ import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore } from "../client";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import type { RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -91,7 +92,7 @@ interface IInitOpts {
|
||||
|
||||
export interface IBootstrapCrossSigningOpts {
|
||||
setupNewCrossSigning?: boolean;
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => void): Promise<void>;
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => {}): Promise<void>;
|
||||
}
|
||||
|
||||
interface IBootstrapSecretStorageOpts {
|
||||
@@ -111,18 +112,19 @@ interface IRoomKey {
|
||||
algorithm: string;
|
||||
}
|
||||
|
||||
interface IRoomKeyRequestBody extends IRoomKey {
|
||||
export interface IRoomKeyRequestBody extends IRoomKey {
|
||||
session_id: string;
|
||||
sender_key: string
|
||||
}
|
||||
|
||||
interface IMegolmSessionData {
|
||||
export interface IMegolmSessionData {
|
||||
sender_key: string;
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_keys: Record<string, string>;
|
||||
room_id: string;
|
||||
session_id: string;
|
||||
session_key: string;
|
||||
algorithm: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -138,11 +140,6 @@ interface IDeviceVerificationUpgrade {
|
||||
* could be established
|
||||
*/
|
||||
|
||||
interface IOlmSessionResult {
|
||||
device: DeviceInfo;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
interface IUserOlmSession {
|
||||
deviceIdKey: string;
|
||||
sessions: {
|
||||
@@ -162,7 +159,7 @@ interface ISyncDeviceLists {
|
||||
left: string[];
|
||||
}
|
||||
|
||||
interface IRoomKeyRequestRecipient {
|
||||
export interface IRoomKeyRequestRecipient {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
}
|
||||
@@ -172,7 +169,7 @@ interface ISignableObject {
|
||||
unsigned?: object
|
||||
}
|
||||
|
||||
interface IEventDecryptionResult {
|
||||
export interface IEventDecryptionResult {
|
||||
clearEvent: object;
|
||||
senderCurve25519Key?: string;
|
||||
claimedEd25519Key?: string;
|
||||
@@ -197,7 +194,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
private readonly reEmitter: ReEmitter;
|
||||
private readonly verificationMethods: any; // TODO types
|
||||
private readonly supportedAlgorithms: DecryptionAlgorithm[];
|
||||
private readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
private readonly inRoomVerificationRequests: InRoomRequests;
|
||||
@@ -281,7 +278,7 @@ export class Crypto extends EventEmitter {
|
||||
* or a class that implements a verification method.
|
||||
*/
|
||||
constructor(
|
||||
private readonly baseApis: MatrixClient,
|
||||
public readonly baseApis: MatrixClient,
|
||||
public readonly sessionStore: SessionStore,
|
||||
private readonly userId: string,
|
||||
private readonly deviceId: string,
|
||||
@@ -627,7 +624,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
// Cross-sign own device
|
||||
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
|
||||
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
|
||||
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device) as ISignedKey;
|
||||
builder.addKeySignature(this.userId, this.deviceId, deviceSignature);
|
||||
|
||||
// Sign message key backup with cross-signing master key
|
||||
@@ -937,7 +934,7 @@ export class Crypto extends EventEmitter {
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
|
||||
|
||||
// create keyBackupInfo object to add to builder
|
||||
const data = {
|
||||
const data: IKeyBackupInfo = {
|
||||
algorithm: info.algorithm,
|
||||
auth_data: info.auth_data,
|
||||
};
|
||||
@@ -1079,17 +1076,17 @@ export class Crypto extends EventEmitter {
|
||||
* @param {Uint8Array} key the private key
|
||||
* @returns {Promise} so you can catch failures
|
||||
*/
|
||||
public async storeSessionBackupPrivateKey(key: Uint8Array): Promise<void> {
|
||||
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||
}
|
||||
const pickleKey = Buffer.from(this.olmDevice._pickleKey);
|
||||
key = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
||||
const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
|
||||
return this.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key);
|
||||
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1926,10 +1923,7 @@ export class Crypto extends EventEmitter {
|
||||
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
public downloadKeys(
|
||||
userIds: string[],
|
||||
forceDownload?: boolean,
|
||||
): Promise<Record<string, Record<string, IDevice>>> {
|
||||
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
|
||||
return this.deviceList.downloadKeys(userIds, forceDownload);
|
||||
}
|
||||
|
||||
@@ -2573,7 +2567,7 @@ export class Crypto extends EventEmitter {
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
ensureOlmSessionsForUsers(users: string[]): Promise<IOlmSessionResult> {
|
||||
ensureOlmSessionsForUsers(users: string[]): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser = {};
|
||||
|
||||
for (let i = 0; i < users.length; ++i) {
|
||||
@@ -2598,9 +2592,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
return olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, devicesByUser,
|
||||
);
|
||||
return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2636,7 +2628,7 @@ export class Crypto extends EventEmitter {
|
||||
* @param {Function} opts.progressCallback called with an object which has a stage param
|
||||
* @return {Promise} a promise which resolves once the keys have been imported
|
||||
*/
|
||||
public importRoomKeys(keys: IRoomKey[], opts: any = {}): Promise<any> { // TODO types
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: any = {}): Promise<any> { // TODO types
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
const total = keys.length;
|
||||
@@ -2850,8 +2842,8 @@ export class Crypto extends EventEmitter {
|
||||
* Re-send any outgoing key requests, eg after verification
|
||||
* @returns {Promise}
|
||||
*/
|
||||
public cancelAndResendAllOutgoingKeyRequests(): Promise<void> {
|
||||
return this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests();
|
||||
public async cancelAndResendAllOutgoingKeyRequests(): Promise<void> {
|
||||
await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3259,9 +3251,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
const devicesByUser = {};
|
||||
devicesByUser[sender] = [device];
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice, this.baseApis, devicesByUser, true,
|
||||
);
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
this.lastNewSessionForced[sender][deviceKey] = Date.now();
|
||||
|
||||
@@ -3300,9 +3290,7 @@ export class Crypto extends EventEmitter {
|
||||
// it. This won't always be the case though so we need to re-send any that have already been sent
|
||||
// to avoid races.
|
||||
const requestsToResend =
|
||||
await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
|
||||
sender, device.deviceId,
|
||||
);
|
||||
await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(sender, device.deviceId);
|
||||
for (const keyReq of requestsToResend) {
|
||||
this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true);
|
||||
}
|
||||
@@ -3440,9 +3428,7 @@ export class Crypto extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
await encryptor.reshareKeyWithDevice(
|
||||
body.sender_key, body.session_id, userId, device,
|
||||
);
|
||||
await encryptor.reshareKeyWithDevice(body.sender_key, body.session_id, userId, device);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Failed to re-share keys for session " + body.session_id +
|
||||
@@ -3653,7 +3639,7 @@ export function fixBackupKey(key: string): string | null {
|
||||
* the relevant crypto algorithm implementation to share the keys for
|
||||
* this request.
|
||||
*/
|
||||
class IncomingRoomKeyRequest {
|
||||
export class IncomingRoomKeyRequest {
|
||||
public readonly userId: string;
|
||||
public readonly deviceId: string;
|
||||
public readonly requestId: string;
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface IKeyBackupRoomSessions {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IKeyBackupVersion {
|
||||
export interface IKeyBackupInfo {
|
||||
algorithm: string;
|
||||
auth_data: {
|
||||
public_key: string;
|
||||
@@ -41,10 +41,9 @@ export interface IKeyBackupVersion {
|
||||
private_key_iterations: number;
|
||||
private_key_bits?: number;
|
||||
};
|
||||
count: number;
|
||||
etag: string;
|
||||
version: string; // number contained within
|
||||
recovery_key: string;
|
||||
count?: number;
|
||||
etag?: string;
|
||||
version?: string; // number contained within
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 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.
|
||||
@@ -22,24 +20,42 @@ limitations under the License.
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import { Logger } from "loglevel";
|
||||
|
||||
import OlmDevice from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import anotherjson from "another-json";
|
||||
import { OneTimeKey } from "./dehydration";
|
||||
import { MatrixClient } from "../client";
|
||||
|
||||
enum Algorithm {
|
||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||
Megolm = "m.megolm.v1.aes-sha2",
|
||||
MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
}
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for olm
|
||||
*/
|
||||
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
export const OLM_ALGORITHM = Algorithm.Olm;
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm
|
||||
*/
|
||||
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
export const MEGOLM_ALGORITHM = Algorithm.Megolm;
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm backups
|
||||
*/
|
||||
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
|
||||
|
||||
export interface IOlmSessionResult {
|
||||
device: DeviceInfo;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for an Olm device
|
||||
@@ -58,9 +74,13 @@ export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
* has been encrypted into `resultsObject`
|
||||
*/
|
||||
export async function encryptMessageForDevice(
|
||||
resultsObject,
|
||||
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
|
||||
payloadFields,
|
||||
resultsObject: Record<string, string>,
|
||||
ourUserId: string,
|
||||
ourDeviceId: string,
|
||||
olmDevice: OlmDevice,
|
||||
recipientUserId: string,
|
||||
recipientDevice: DeviceInfo,
|
||||
payloadFields: Record<string, any>,
|
||||
) {
|
||||
const deviceKey = recipientDevice.getIdentityKey();
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
|
||||
@@ -77,6 +97,7 @@ export async function encryptMessageForDevice(
|
||||
|
||||
const payload = {
|
||||
sender: ourUserId,
|
||||
// TODO this appears to no longer be used whatsoever
|
||||
sender_device: ourDeviceId,
|
||||
|
||||
// Include the Ed25519 key so that the recipient knows what
|
||||
@@ -129,7 +150,9 @@ export async function encryptMessageForDevice(
|
||||
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function getExistingOlmSessions(
|
||||
olmDevice, baseApis, devicesByUser,
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
) {
|
||||
const devicesWithoutSession = {};
|
||||
const sessions = {};
|
||||
@@ -189,23 +212,30 @@ export async function getExistingOlmSessions(
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
|
||||
) {
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
force = false,
|
||||
otkTimeout?: number,
|
||||
failedServers?: string[],
|
||||
log: Logger = logger,
|
||||
): Promise<Record<string, Record<string, IOlmSessionResult>>> {
|
||||
if (typeof force === "number") {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - backwards compatibility
|
||||
log = failedServers;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - backwards compatibility
|
||||
failedServers = otkTimeout;
|
||||
otkTimeout = force;
|
||||
force = false;
|
||||
}
|
||||
if (!log) {
|
||||
log = logger;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const resolveSession = {};
|
||||
const resolveSession: Record<string, (sessionId?: string) => void> = {};
|
||||
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
// important to do this for all devices this task cares about in a single
|
||||
@@ -227,9 +257,9 @@ export async function ensureOlmSessionsForDevices(
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (...args) => {
|
||||
resolveSession[key] = (v: any) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
resolve(v);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -375,7 +405,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
return result;
|
||||
}
|
||||
|
||||
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
|
||||
async function _verifyKeyAndStartSession(
|
||||
olmDevice: OlmDevice,
|
||||
oneTimeKey: OneTimeKey,
|
||||
userId: string,
|
||||
deviceInfo: DeviceInfo,
|
||||
): Promise<string> {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
try {
|
||||
await verifySignature(
|
||||
@@ -407,6 +442,11 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
return sid;
|
||||
}
|
||||
|
||||
export interface IObject {
|
||||
unsigned?: object;
|
||||
signatures?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the signature on an object
|
||||
*
|
||||
@@ -424,7 +464,11 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
* or rejects with an Error if it is bad.
|
||||
*/
|
||||
export async function verifySignature(
|
||||
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
|
||||
olmDevice: OlmDevice,
|
||||
obj: OneTimeKey | IObject,
|
||||
signingUserId: string,
|
||||
signingDeviceId: string,
|
||||
signingKey: string,
|
||||
) {
|
||||
const signKeyId = "ed25519:" + signingDeviceId;
|
||||
const signatures = obj.signatures || {};
|
||||
@@ -434,10 +478,11 @@ export async function verifySignature(
|
||||
throw Error("No signature");
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||
// anotherjson
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson
|
||||
const mangledObj = Object.assign({}, obj);
|
||||
delete mangledObj.unsigned;
|
||||
if ("unsigned" in mangledObj) {
|
||||
delete mangledObj.unsigned;
|
||||
}
|
||||
delete mangledObj.signatures;
|
||||
const json = anotherjson.stringify(mangledObj);
|
||||
|
||||
@@ -453,14 +498,14 @@ export async function verifySignature(
|
||||
* @param {Olm.PkSigning|Uint8Array} key the signing object or the private key
|
||||
* seed
|
||||
* @param {string} userId The user ID who owns the signing key
|
||||
* @param {string} pubkey The public key (ignored if key is a seed)
|
||||
* @param {string} pubKey The public key (ignored if key is a seed)
|
||||
* @returns {string} the signature for the object
|
||||
*/
|
||||
export function pkSign(obj, key, userId, pubkey) {
|
||||
export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string {
|
||||
let createdKey = false;
|
||||
if (key instanceof Uint8Array) {
|
||||
const keyObj = new global.Olm.PkSigning();
|
||||
pubkey = keyObj.init_with_seed(key);
|
||||
pubKey = keyObj.init_with_seed(key);
|
||||
key = keyObj;
|
||||
createdKey = true;
|
||||
}
|
||||
@@ -472,7 +517,7 @@ export function pkSign(obj, key, userId, pubkey) {
|
||||
const mysigs = sigs[userId] || {};
|
||||
sigs[userId] = mysigs;
|
||||
|
||||
return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj));
|
||||
return mysigs['ed25519:' + pubKey] = key.sign(anotherjson.stringify(obj));
|
||||
} finally {
|
||||
obj.signatures = sigs;
|
||||
if (unsigned) obj.unsigned = unsigned;
|
||||
@@ -485,11 +530,11 @@ export function pkSign(obj, key, userId, pubkey) {
|
||||
/**
|
||||
* Verify a signed JSON object
|
||||
* @param {Object} obj Object to verify
|
||||
* @param {string} pubkey The public key to use to verify
|
||||
* @param {string} pubKey The public key to use to verify
|
||||
* @param {string} userId The user ID who signed the object
|
||||
*/
|
||||
export function pkVerify(obj, pubkey, userId) {
|
||||
const keyId = "ed25519:" + pubkey;
|
||||
export function pkVerify(obj: IObject, pubKey: string, userId: string) {
|
||||
const keyId = "ed25519:" + pubKey;
|
||||
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
|
||||
throw new Error("No signature");
|
||||
}
|
||||
@@ -500,7 +545,7 @@ export function pkVerify(obj, pubkey, userId) {
|
||||
const unsigned = obj.unsigned;
|
||||
if (obj.unsigned) delete obj.unsigned;
|
||||
try {
|
||||
util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature);
|
||||
util.ed25519_verify(pubKey, anotherjson.stringify(obj), signature);
|
||||
} finally {
|
||||
obj.signatures = sigs;
|
||||
if (unsigned) obj.unsigned = unsigned;
|
||||
@@ -513,7 +558,7 @@ export function pkVerify(obj, pubkey, userId) {
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
* @return {string} The base64.
|
||||
*/
|
||||
export function encodeBase64(uint8Array) {
|
||||
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return Buffer.from(uint8Array).toString("base64");
|
||||
}
|
||||
|
||||
@@ -522,7 +567,7 @@ export function encodeBase64(uint8Array) {
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
* @return {string} The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64(uint8Array) {
|
||||
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return encodeBase64(uint8Array).replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
@@ -531,6 +576,6 @@ export function encodeUnpaddedBase64(uint8Array) {
|
||||
* @param {string} base64 The base64 to decode.
|
||||
* @return {Uint8Array} The decoded data.
|
||||
*/
|
||||
export function decodeBase64(base64) {
|
||||
export function decodeBase64(base64: string): Uint8Array {
|
||||
return Buffer.from(base64, "base64");
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import bs58 from 'bs58';
|
||||
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
|
||||
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
|
||||
|
||||
export function encodeRecoveryKey(key) {
|
||||
export function encodeRecoveryKey(key: ArrayLike<number>): string {
|
||||
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
|
||||
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
|
||||
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
|
||||
@@ -35,8 +35,8 @@ export function encodeRecoveryKey(key) {
|
||||
return base58key.match(/.{1,4}/g).join(" ");
|
||||
}
|
||||
|
||||
export function decodeRecoveryKey(recoverykey) {
|
||||
const result = bs58.decode(recoverykey.replace(/ /g, ''));
|
||||
export function decodeRecoveryKey(recoveryKey: string): Uint8Array {
|
||||
const result = bs58.decode(recoveryKey.replace(/ /g, ''));
|
||||
|
||||
let parity = 0;
|
||||
for (const b of result) {
|
||||
@@ -10,6 +10,9 @@
|
||||
* @interface CryptoStore
|
||||
*/
|
||||
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*
|
||||
@@ -32,3 +35,11 @@
|
||||
* @property {Number} state current state of this request (states are defined
|
||||
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
|
||||
*/
|
||||
export interface OutgoingRoomKeyRequest {
|
||||
requestId: string;
|
||||
requestTxnId?: string;
|
||||
cancellationTxnId?: string;
|
||||
recipients: IRoomKeyRequestRecipient[];
|
||||
requestBody: IRoomKeyRequestBody;
|
||||
state: RoomKeyRequestState;
|
||||
}
|
||||
+1
-1
@@ -131,7 +131,7 @@ interface IDecryptionResult {
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface IClearEvent {
|
||||
export interface IClearEvent {
|
||||
type: string;
|
||||
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
|
||||
unsigned?: IUnsigned;
|
||||
|
||||
+4
-3
@@ -22,6 +22,7 @@ limitations under the License.
|
||||
|
||||
import unhomoglyph from "unhomoglyph";
|
||||
import promiseRetry from "p-retry";
|
||||
import type NodeCrypto from "crypto";
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
@@ -500,13 +501,13 @@ export function simpleRetryOperation<T>(promiseFn: (attempt: number) => Promise<
|
||||
// Matrix SDK without needing to `require("crypto")`, which will fail in
|
||||
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
|
||||
// it, we can call `getCrypto`.
|
||||
let crypto: Object;
|
||||
let crypto: typeof NodeCrypto;
|
||||
|
||||
export function setCrypto(c: Object) {
|
||||
export function setCrypto(c: typeof NodeCrypto) {
|
||||
crypto = c;
|
||||
}
|
||||
|
||||
export function getCrypto(): Object {
|
||||
export function getCrypto(): typeof NodeCrypto {
|
||||
return crypto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1232,6 +1232,13 @@
|
||||
dependencies:
|
||||
"@types/babel-types" "*"
|
||||
|
||||
"@types/bs58@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.1.tgz#3d51222aab067786d3bc3740a84a7f5a0effaa37"
|
||||
integrity sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==
|
||||
dependencies:
|
||||
base-x "^3.0.6"
|
||||
|
||||
"@types/caseless@*":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
@@ -1778,7 +1785,7 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base-x@^3.0.2:
|
||||
base-x@^3.0.2, base-x@^3.0.6:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d"
|
||||
integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==
|
||||
|
||||
Reference in New Issue
Block a user