Merge pull request #1508 from matrix-org/t3chguy/ts/4

This commit is contained in:
Michael Telatynski
2021-06-29 22:24:18 +01:00
committed by GitHub
27 changed files with 2854 additions and 2661 deletions
+1
View File
@@ -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);
});
});
+5 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
},
);
},
+8 -24
View File
@@ -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);
}
}
@@ -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);
}
}
+36 -24
View File
@@ -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
-361
View File
@@ -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);
+355
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+1 -1
View File
@@ -44,7 +44,7 @@ interface DeviceKeys {
signatures?: Signatures;
}
interface OneTimeKey {
export interface OneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
+4 -2
View File
@@ -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
View File
@@ -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;
+4 -5
View File
@@ -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 */
+80 -35
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+8 -1
View File
@@ -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==