1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Type the rest of algorithms because tests are unhappy otherwise

This commit is contained in:
Michael Telatynski
2021-06-25 10:18:46 +01:00
parent 48ad9ba3d7
commit 835aafcb17
9 changed files with 2225 additions and 2202 deletions

View File

@@ -59,7 +59,7 @@ enum TrackingStatus {
UpToDate, UpToDate,
} }
type DeviceInfoMap = Record<string, Record<string, IDevice>>; export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
/** /**
* @alias module:crypto/DeviceList * @alias module:crypto/DeviceList
@@ -70,7 +70,7 @@ export class DeviceList extends EventEmitter {
// [device info] // [device info]
// } // }
// } // }
private devices: DeviceInfoMap = {}; private devices: Record<string, Record<string, IDevice>> = {};
// userId -> { // userId -> {
// [key info] // [key info]
@@ -315,7 +315,7 @@ export class DeviceList extends EventEmitter {
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
*/ */
private getDevicesFromStore(userIds: string[]): DeviceInfoMap { private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
const stored = {}; const stored: DeviceInfoMap = {};
userIds.map((u) => { userIds.map((u) => {
stored[u] = {}; stored[u] = {};
const devices = this.getStoredDevicesForUser(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 * Replaces the list of devices for a user with the given device list
* *
* @param {string} u The user ID * @param {string} userId The user ID
* @param {Object} devs New device info for user * @param {Object} devices New device info for user
*/ */
public storeDevicesForUser(u: string, devs: Record<string, IDevice>): void { public storeDevicesForUser(userId: string, devices: Record<string, IDevice>): void {
// remove previous devices from userByIdentityKey this.setRawStoredDevicesForUser(userId, devices);
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;
}
this.dirty = true; this.dirty = true;
} }
@@ -859,7 +843,7 @@ class DeviceListUpdateSerialiser {
); );
// put the updates into the object that will be returned as our results // 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) => { Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage(); storage[deviceId] = userStore[deviceId].toStorage();
}); });

View File

@@ -24,7 +24,7 @@ import { MatrixClient } from "../../client";
import { Room } from "../../models/room"; import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice"; import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../.."; import { MatrixEvent, RoomMember } from "../..";
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
/** /**
@@ -33,7 +33,9 @@ import { DeviceInfo } from "../deviceinfo";
* *
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>} * @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/ */
export const ENCRYPTION_CLASSES: Record<string, EncryptionAlgorithm> = {}; 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 * map of registered encryption algorithm classes. Map from string to {@link
@@ -41,7 +43,7 @@ export const ENCRYPTION_CLASSES: Record<string, EncryptionAlgorithm> = {};
* *
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>} * @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/ */
export const DECRYPTION_CLASSES: Record<string, DecryptionAlgorithm> = {}; export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
interface IParams { interface IParams {
userId: string; userId: string;
@@ -50,6 +52,7 @@ interface IParams {
olmDevice: OlmDevice; olmDevice: OlmDevice;
baseApis: MatrixClient; baseApis: MatrixClient;
roomId: string; roomId: string;
config: object;
} }
/** /**
@@ -89,7 +92,7 @@ export abstract class EncryptionAlgorithm {
* *
* @param {module:models/room} room the room the event is in * @param {module:models/room} room the room the event is in
*/ */
public abstract prepareToEncrypt(room: Room): void; public prepareToEncrypt(room: Room): void {}
/** /**
* Encrypt a message event * Encrypt a message event
@@ -115,16 +118,16 @@ export abstract class EncryptionAlgorithm {
* @public * @public
* @abstract * @abstract
*/ */
public abstract onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string); public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {}
public abstract reshareKeyWithDevice( public reshareKeyWithDevice?(
senderKey: string, senderKey: string,
sessionId: string, sessionId: string,
userId: string, userId: string,
device: DeviceInfo, device: DeviceInfo,
): Promise<void>; ): Promise<void>;
public forceDiscardSession?: () => void; public forceDiscardSession?(): void;
} }
/** /**
@@ -140,13 +143,13 @@ export abstract class EncryptionAlgorithm {
* from. Null for to-device events. * from. Null for to-device events.
*/ */
export abstract class DecryptionAlgorithm { export abstract class DecryptionAlgorithm {
private readonly userId: string; protected readonly userId: string;
private readonly crypto: Crypto; protected readonly crypto: Crypto;
private readonly olmDevice: OlmDevice; protected readonly olmDevice: OlmDevice;
private readonly baseApis: MatrixClient; protected readonly baseApis: MatrixClient;
private readonly roomId: string; protected readonly roomId: string;
constructor(params: Omit<IParams, "deviceId">) { constructor(params: DecryptionClassParams) {
this.userId = params.userId; this.userId = params.userId;
this.crypto = params.crypto; this.crypto = params.crypto;
this.olmDevice = params.olmDevice; this.olmDevice = params.olmDevice;
@@ -205,7 +208,7 @@ export abstract class DecryptionAlgorithm {
* *
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest * @param {module:crypto~IncomingRoomKeyRequest} keyRequest
*/ */
public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest) { public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
} }
@@ -215,12 +218,13 @@ export abstract class DecryptionAlgorithm {
* *
* @param {string} senderKey the sender's key * @param {string} senderKey the sender's key
*/ */
public async retryDecryptionFromSender(senderKey: string): Promise<void> { public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
// ignore by default // ignore by default
return false;
} }
public onRoomKeyWithheldEvent?: (event: MatrixEvent) => Promise<void>; public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
public sendSharedHistoryInboundSessions?: (devicesByUser: Record<string, DeviceInfo[]>) => Promise<void>; public sendSharedHistoryInboundSessions?(devicesByUser: Record<string, DeviceInfo[]>): Promise<void>;
} }
/** /**
@@ -237,7 +241,7 @@ export abstract class DecryptionAlgorithm {
export class DecryptionError extends Error { export class DecryptionError extends Error {
public readonly detailedString: string; public readonly detailedString: string;
constructor(public readonly code: string, msg: string, details: Record<string, string>) { constructor(public readonly code: string, msg: string, details?: Record<string, string>) {
super(msg); super(msg);
this.code = code; this.code = code;
this.name = 'DecryptionError'; this.name = 'DecryptionError';
@@ -245,7 +249,7 @@ export class DecryptionError extends Error {
} }
} }
function detailedStringForDecryptionError(err: DecryptionError, details: Record<string, string>): string { function detailedStringForDecryptionError(err: DecryptionError, details?: Record<string, string>): string {
let result = err.name + '[msg: ' + err.message; let result = err.name + '[msg: ' + err.message;
if (details) { if (details) {
@@ -289,8 +293,8 @@ export class UnknownDeviceError extends Error {
*/ */
export function registerAlgorithm( export function registerAlgorithm(
algorithm: string, algorithm: string,
encryptor: EncryptionAlgorithm, encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: DecryptionAlgorithm, decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
): void { ): void {
ENCRYPTION_CLASSES[algorithm] = encryptor; ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor; DECRYPTION_CLASSES[algorithm] = decryptor;

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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);

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);

View File

@@ -28,7 +28,7 @@ import { ReEmitter } from '../ReEmitter';
import { logger } from '../logger'; import { logger } from '../logger';
import { OlmDevice } from "./OlmDevice"; import { OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib"; import * as olmlib from "./olmlib";
import { DeviceList } from "./DeviceList"; import {DeviceInfoMap, DeviceList } from "./DeviceList";
import { DeviceInfo, IDevice } from "./deviceinfo"; import { DeviceInfo, IDevice } from "./deviceinfo";
import * as algorithms from "./algorithms"; import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning'; import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
@@ -1923,10 +1923,7 @@ export class Crypto extends EventEmitter {
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link * @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}. * module:crypto/deviceinfo|DeviceInfo}.
*/ */
public downloadKeys( public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
userIds: string[],
forceDownload?: boolean,
): Promise<Record<string, Record<string, IDevice>>> {
return this.deviceList.downloadKeys(userIds, forceDownload); return this.deviceList.downloadKeys(userIds, forceDownload);
} }

View File

@@ -131,7 +131,7 @@ interface IDecryptionResult {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
interface IClearEvent { export interface IClearEvent {
type: string; type: string;
content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">; content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
unsigned?: IUnsigned; unsigned?: IUnsigned;