index : matrix-js-sdk

My fork of matrix-js-sdk

diff options
context:
space:
mode:
authorHubert Chathi <[email protected]>2020-06-23 15:37:27 -0400
committerGitHub <[email protected]>2020-06-23 15:37:27 -0400
commit69dc518c2ca6d559352a346f19ea73a3b3dd791c (patch)
treeaebfe8680fb25f3af00b479f15f76e199edc9e99
parentf683f4544aa5da150836b01c754062809119fa97 (diff)
parentfa1dddf06c334743e31955d11fe9fc43013e6adf (diff)
downloadmatrix-js-sdk-69dc518c2ca6d559352a346f19ea73a3b3dd791c.tar.gz
Merge pull request #1406 from matrix-org/uhoreg/distrust_backup
Don't trust keys megolm received from backup for verifying the sender
-rw-r--r--spec/unit/crypto.spec.js61
-rw-r--r--spec/unit/crypto/backup.spec.js1
-rw-r--r--src/client.js32
-rw-r--r--src/crypto/OlmDevice.js8
-rw-r--r--src/crypto/algorithms/megolm.js9
-rw-r--r--src/crypto/index.js79
-rw-r--r--src/models/event.js15
7 files changed, 186 insertions, 19 deletions
diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js
index 18cc930b..8f7fff10 100644
--- a/spec/unit/crypto.spec.js
+++ b/spec/unit/crypto.spec.js
@@ -10,6 +10,7 @@ import * as olmlib from "../../src/crypto/olmlib";
import {sleep} from "../../src/utils";
import {EventEmitter} from "events";
import {CRYPTO_ENABLED} from "../../src/client";
+import {DeviceInfo} from "../../src/crypto/deviceinfo";
const Olm = global.Olm;
@@ -26,6 +27,66 @@ describe("Crypto", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
+ describe("encrypted events", function() {
+ it("provides encryption information", async function() {
+ const client = (new TestClient(
+ "@alice:example.com", "deviceid",
+ )).client;
+ await client.initCrypto();
+
+ // unencrypted event
+ const event = {
+ getId: () => "$event_id",
+ getSenderKey: () => null,
+ getWireContent: () => {return {};},
+ };
+
+ let encryptionInfo = client.getEventEncryptionInfo(event);
+ expect(encryptionInfo.encrypted).toBeFalsy();
+
+ // unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
+ event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
+ event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
+ event.getForwardingCurve25519KeyChain = () => ["not empty"];
+ event.isKeySourceUntrusted = () => false;
+ event.getClaimedEd25519Key =
+ () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
+
+ encryptionInfo = client.getEventEncryptionInfo(event);
+ expect(encryptionInfo.encrypted).toBeTruthy();
+ expect(encryptionInfo.authenticated).toBeFalsy();
+ expect(encryptionInfo.sender).toBeFalsy();
+
+ // known sender, megolm key from backup
+ event.getForwardingCurve25519KeyChain = () => [];
+ event.isKeySourceUntrusted = () => true;
+ const device = new DeviceInfo("FLIBBLE");
+ device.keys["curve25519:FLIBBLE"] =
+ 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
+ device.keys["ed25519:FLIBBLE"] =
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
+ client._crypto._deviceList.getDeviceByIdentityKey = () => device;
+
+ encryptionInfo = client.getEventEncryptionInfo(event);
+ expect(encryptionInfo.encrypted).toBeTruthy();
+ expect(encryptionInfo.authenticated).toBeFalsy();
+ expect(encryptionInfo.sender).toBeTruthy();
+ expect(encryptionInfo.mismatchedSender).toBeFalsy();
+
+ // known sender, trusted megolm key, but bad ed25519key
+ event.isKeySourceUntrusted = () => false;
+ device.keys["ed25519:FLIBBLE"] =
+ 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
+
+ encryptionInfo = client.getEventEncryptionInfo(event);
+ expect(encryptionInfo.encrypted).toBeTruthy();
+ expect(encryptionInfo.authenticated).toBeTruthy();
+ expect(encryptionInfo.sender).toBeTruthy();
+ expect(encryptionInfo.mismatchedSender).toBeTruthy();
+
+ client.stopClient();
+ });
+ });
describe('Session management', function() {
const otkResponse = {
diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js
index 34766b87..96623dda 100644
--- a/spec/unit/crypto/backup.spec.js
+++ b/spec/unit/crypto/backup.spec.js
@@ -518,6 +518,7 @@ describe("MegolmBackup", function() {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
+ expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
});
});
diff --git a/src/client.js b/src/client.js
index a646500a..54aa7311 100644
--- a/src/client.js
+++ b/src/client.js
@@ -1172,20 +1172,23 @@ wrapCryptoFuncs(MatrixClient, [
]);
/**
- * Check if the sender of an event is verified
- * The cross-signing API is currently UNSTABLE and may change without notice.
+ * Get information about the encryption of an event
*
- * @param {MatrixEvent} event event to be checked
+ * @function module:client~MatrixClient#getEventEncryptionInfo
*
- * @returns {DeviceTrustLevel}
+ * @param {module:models/event.MatrixEvent} event event to be checked
+ *
+ * @return {object} An object with the fields:
+ * - encrypted: whether the event is encrypted (if not encrypted, some of the
+ * other properties may not be set)
+ * - senderKey: the sender's key
+ * - algorithm: the algorithm used to encrypt the event
+ * - authenticated: whether we can be sure that the owner of the senderKey
+ * sent the event
+ * - sender: the sender's device information, if available
+ * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
+ * (only meaningful if `sender` is set)
*/
-MatrixClient.prototype.checkEventSenderTrust = async function(event) {
- const device = await this.getEventSenderDeviceInfo(event);
- if (!device) {
- return 0;
- }
- return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId);
-};
/**
* Create a recovery key from a user-supplied passphrase.
@@ -1315,6 +1318,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
*/
wrapCryptoFuncs(MatrixClient, [
+ "getEventEncryptionInfo",
"createRecoveryKeyFromPassphrase",
"bootstrapSecretStorage",
"addSecretStorageKey",
@@ -1980,7 +1984,11 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
}
- return this.importRoomKeys(keys, { progressCallback });
+ return this.importRoomKeys(keys, {
+ progressCallback,
+ untrusted: true,
+ source: "backup",
+ });
}).then(() => {
return this._crypto.setTrustedBackupPubKey(backupPubKey);
}).then(() => {
diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js
index 6300609a..774d696b 100644
--- a/src/crypto/OlmDevice.js
+++ b/src/crypto/OlmDevice.js
@@ -992,11 +992,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
* @param {boolean} exportFormat true if the megolm keys are in export format
* (ie, they lack an ed25519 signature)
+ * @param {Object} [extraSessionData={}] any other data to be include with the session
*/
OlmDevice.prototype.addInboundGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
- exportFormat,
+ exportFormat, extraSessionData = {},
) {
await this._cryptoStore.doTxn(
'readwrite', [
@@ -1043,12 +1044,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
" with first index " + session.first_known_index(),
);
- const sessionData = {
+ const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
- };
+ });
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
@@ -1224,6 +1225,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
forwardingCurve25519KeyChain: (
sessionData.forwardingCurve25519KeyChain || []
),
+ untrusted: sessionData.untrusted,
};
},
);
diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js
index 0976302a..f3e27611 100644
--- a/src/crypto/algorithms/megolm.js
+++ b/src/crypto/algorithms/megolm.js
@@ -1201,6 +1201,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
senderCurve25519Key: res.senderKey,
claimedEd25519Key: res.keysClaimed.ed25519,
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
+ untrusted: res.untrusted,
};
};
@@ -1548,8 +1549,11 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @inheritdoc
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
+ * @param {object} [opts={}] options for the import
+ * @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
+ * @param {string} [opts.source] where the key came from
*/
-MegolmDecryption.prototype.importRoomKey = function(session) {
+MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
@@ -1558,8 +1562,9 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
session.session_key,
session.sender_claimed_keys,
true,
+ opts.untrusted ? { untrusted: opts.untrusted } : {},
).then(() => {
- if (this._crypto.backupInfo) {
+ if (this._crypto.backupInfo && opts.source !== "backup") {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
diff --git a/src/crypto/index.js b/src/crypto/index.js
index f2265374..1c92b4fe 100644
--- a/src/crypto/index.js
+++ b/src/crypto/index.js
@@ -2184,11 +2184,16 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
const forwardingChain = event.getForwardingCurve25519KeyChain();
if (forwardingChain.length > 0) {
- // we got this event from somewhere else
+ // we got the key this event from somewhere else
// TODO: check if we can trust the forwarders.
return null;
}
+ if (event.isKeySourceUntrusted()) {
+ // we got the key for this event from a source that we consider untrusted
+ return null;
+ }
+
// senderKey is the Curve25519 identity key of the device which the event
// was sent from. In the case of Megolm, it's actually the Curve25519
// identity key of the device which set up the Megolm session.
@@ -2228,6 +2233,76 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
};
/**
+ * Get information about the encryption of an event
+ *
+ * @param {module:models/event.MatrixEvent} event event to be checked
+ *
+ * @return {object} An object with the fields:
+ * - encrypted: whether the event is encrypted (if not encrypted, some of the
+ * other properties may not be set)
+ * - senderKey: the sender's key
+ * - algorithm: the algorithm used to encrypt the event
+ * - authenticated: whether we can be sure that the owner of the senderKey
+ * sent the event
+ * - sender: the sender's device information, if available
+ * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
+ * (only meaningful if `sender` is set)
+ */
+Crypto.prototype.getEventEncryptionInfo = function(event) {
+ const ret = {};
+
+ ret.senderKey = event.getSenderKey();
+ ret.algorithm = event.getWireContent().algorithm;
+
+ if (!ret.senderKey || !ret.algorithm) {
+ ret.encrypted = false;
+ return ret;
+ }
+ ret.encrypted = true;
+
+ const forwardingChain = event.getForwardingCurve25519KeyChain();
+ if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
+ // we got the key this event from somewhere else
+ // TODO: check if we can trust the forwarders.
+ ret.authenticated = false;
+ } else {
+ ret.authenticated = true;
+ }
+
+ // senderKey is the Curve25519 identity key of the device which the event
+ // was sent from. In the case of Megolm, it's actually the Curve25519
+ // identity key of the device which set up the Megolm session.
+
+ ret.sender = this._deviceList.getDeviceByIdentityKey(
+ ret.algorithm, ret.senderKey,
+ );
+
+ // so far so good, but now we need to check that the sender of this event
+ // hadn't advertised someone else's Curve25519 key as their own. We do that
+ // by checking the Ed25519 claimed by the event (or, in the case of megolm,
+ // the event which set up the megolm session), to check that it matches the
+ // fingerprint of the purported sending device.
+ //
+ // (see https://github.com/vector-im/vector-web/issues/2215)
+
+ const claimedKey = event.getClaimedEd25519Key();
+ if (!claimedKey) {
+ logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
+ "cannot verify sending device");
+ ret.mismatchedSender = true;
+ }
+
+ if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
+ logger.warn(
+ "Event " + event.getId() + " claims ed25519 key " + claimedKey +
+ "but sender device has key " + ret.sender.getFingerprint());
+ ret.mismatchedSender = true;
+ }
+
+ return ret;
+};
+
+/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
*
@@ -2471,7 +2546,7 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
}
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
- return alg.importRoomKey(key).finally((r) => {
+ return alg.importRoomKey(key, opts).finally((r) => {
successes++;
if (opts.progressCallback) { updateProgress(); }
});
diff --git a/src/models/event.js b/src/models/event.js
index 90365432..73ea735d 100644
--- a/src/models/event.js
+++ b/src/models/event.js
@@ -144,6 +144,10 @@ export const MatrixEvent = function(
*/
this._forwardingCurve25519KeyChain = [];
+ /* where the decryption key is untrusted
+ */
+ this._untrusted = null;
+
/* if we have a process decrypting this event, a Promise which resolves
* when it is finished. Normally null.
*/
@@ -599,6 +603,7 @@ utils.extend(MatrixEvent.prototype, {
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
+ this._untrusted = decryptionResult.untrusted || false;
},
/**
@@ -689,6 +694,16 @@ utils.extend(MatrixEvent.prototype, {
return this._forwardingCurve25519KeyChain;
},
+ /**
+ * Whether the decryption key was obtained from an untrusted source. If so,
+ * we cannot verify the authenticity of the message.
+ *
+ * @return {boolean}
+ */
+ isKeySourceUntrusted: function() {
+ return this._untrusted;
+ },
+
getUnsigned: function() {
return this.event.unsigned || {};
},