1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Make crypto.decryptMessage return decryption results

... instead of having it call event.setClearData.

The main advantage of this is that it fixes a race condition, wherein apps
could see `event.isDecrypting()` to be true, but in fact the event had been
decrypted (and there was no `Event.decrypted` event on its way).

We're also fixing another race, wherein if the first attempt to decrypt failed,
a call to `attemptDecryption` would race against the first call and a second
attempt to decrypt would never happen.

This also gives a cleaner interface to MatrixEvent, at the expense of making
the `megolm` unit test a bit more hoop-jumpy.
This commit is contained in:
Richard van der Hoff
2017-08-09 17:34:45 +01:00
parent 9550bca099
commit 6613ee6b0d
7 changed files with 227 additions and 83 deletions

View File

@@ -71,9 +71,13 @@ describe("MegolmDecryption", function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
const event = new MatrixEvent({});
event.setClearData(
{
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
@@ -82,12 +86,20 @@ describe("MegolmDecryption", function() {
session_key: groupSession.session_key(),
},
},
"SENDER_CURVE25519",
"SENDER_ED25519",
);
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
const mockCrypto = {
decryptEvent: function() {
return Promise.resolve(decryptedData);
},
};
return event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
it('can decrypt an event', function() {
const event = new MatrixEvent({
@@ -104,8 +116,8 @@ describe("MegolmDecryption", function() {
},
});
return megolmDecryption.decryptEvent(event).then(() => {
expect(event.getContent()).toEqual('testytest');
return megolmDecryption.decryptEvent(event).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});

74
spec/unit/event.spec.js Normal file
View File

@@ -0,0 +1,74 @@
/*
Copyright 2017 New Vector 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.
*/
import sdk from '../..';
const MatrixEvent = sdk.MatrixEvent;
import testUtils from '../test-utils';
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe(".attemptDecryption", () => {
let encryptedEvent;
beforeEach(() => {
encryptedEvent = new MatrixEvent({
id: 'test_encrypted_event',
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', () => {
let callCount = 0;
const crypto = {
decryptEvent: function() {
++callCount;
console.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
} else {
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}
},
};
return encryptedEvent.attemptDecryption(crypto).then(() => {
expect(callCount).toEqual(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
});
});
});
});

View File

@@ -117,7 +117,8 @@ class DecryptionAlgorithm {
*
* @param {MatrixEvent} event undecrypted event
*
* @return {Promise} resolves once we have finished decrypting. Rejects with an
* @return {Promise<module:crypto~EventDecryptionResult>} promise which
* resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
*/

View File

@@ -535,9 +535,10 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* returns a promise which resolves once we have finished decrypting, or
* rejects with an `algorithms.DecryptionError` if there is a problem
* decrypting the event.
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting, or rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
MegolmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
@@ -588,8 +589,12 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
);
}
event.setClearData(payload, res.senderKey, res.keysClaimed.ed25519,
res.forwardingCurve25519KeyChain);
return {
clearEvent: payload,
senderCurve25519Key: res.senderKey,
claimedEd25519Key: res.keysClaimed.ed25519,
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
};
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {

View File

@@ -157,8 +157,10 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* returns a promise which resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the 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();
@@ -227,9 +229,13 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
}
const claimedKeys = payload.keys || {};
event.setClearData(payload, deviceKey, claimedKeys.ed25519 || null);
};
return {
clearEvent: payload,
senderCurve25519Key: deviceKey,
claimedEd25519Key: claimedKeys.ed25519 || null,
};
};
/**
* Attempt to decrypt an Olm message

View File

@@ -771,8 +771,9 @@ Crypto.prototype.encryptEvent = function(event, room) {
*
* @param {MatrixEvent} event
*
* @return {Promise} resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
* @return {Promise<module:crypto~EventDecryptionResult>} resolves once we have
* finished decrypting. Rejects with an `algorithms.DecryptionError` if there
* is a problem decrypting the event.
*/
Crypto.prototype.decryptEvent = function(event) {
const content = event.getWireContent();
@@ -1294,6 +1295,25 @@ class IncomingRoomKeyRequestCancellation {
}
}
/**
* The result of a (successful) call to decryptEvent.
*
* @typedef {Object} EventDecryptionResult
*
* @property {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @property {string?} senderCurve25519Key Key owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getSenderKey}.
*
* @property {string?} claimedEd25519Key ed25519 key claimed by the sender of
* this event. See {@link module:models/event.MatrixEvent#getClaimedEd25519Key}.
*
* @property {Array<string>?} forwardingCurve25519KeyChain list of curve25519 keys
* involved in telling us about the senderCurve25519Key and claimedEd25519Key.
* See {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}.
*/
/**
* Fires when we receive a room key request
*

View File

@@ -25,8 +25,6 @@ const EventEmitter = require("events").EventEmitter;
const utils = require('../utils.js');
import Promise from 'bluebird';
/**
* Enum for event statuses.
* @readonly
@@ -326,13 +324,12 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @internal
*
* @param {module:crypto} crypto crypto module
*
* returns a promise which resolves (to undefined) when the decryption
* attempt is completed.
*/
attemptDecryption: function(crypto) {
if (!crypto) {
this._badEncryptedMessage("Encryption not enabled");
return;
}
attemptDecryption: async function(crypto) {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
}
@@ -347,6 +344,12 @@ utils.extend(module.exports.MatrixEvent.prototype, {
);
}
// if we already have a decryption attempt in progress, then it may
// fail because it was using outdated info. We now have reason to
// succeed where it failed before, but we don't want to have multiple
// attempts going at the same time, so just set a flag that says we have
// new info.
//
if (this._decrypting) {
console.log(
`Event ${this.getId()} already being decrypted; queueing a retry`,
@@ -356,51 +359,86 @@ utils.extend(module.exports.MatrixEvent.prototype, {
}
this._decrypting = true;
this._doDecryption(crypto).finally(() => {
this._decrypting = false;
while (true) {
this._retryDecryption = false;
});
},
_doDecryption: function(crypto) {
return Promise.try(() => {
return crypto.decryptEvent(this);
}).catch((e) => {
let res;
try {
if (!crypto) {
res = this._badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
}
} catch (e) {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error.
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
console.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
);
return null;
} else if (this._retryDecryption) {
this._decrypting = false;
this._retryDecryption = false;
return;
}
// see if we have a retry queued.
//
// NB: make sure to keep this check in the same tick of the
// event loop as `_decrypting = false` below - otherwise we
// risk a race:
//
// * A: we check _retryDecryption here and see that it is
// false
// * B: we get a second call to attemptDecryption, which sees
// that _decrypting is true so sets _retryDecryption
// * A: we continue below, clear _decrypting, and never do
// the retry.
//
if (this._retryDecryption) {
// decryption error, but we have a retry queued.
console.log(
`Got error decrypting event (id=${this.getId()}), but retrying`,
`Got error decrypting event (id=${this.getId()}: ` +
`${e.message}), but retrying`,
);
this._retryDecryption = false;
return this._doDecryption(crypto);
} else {
continue;
}
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
console.warn(
`Error decrypting event (id=${this.getId()}): ${e}`,
);
this._badEncryptedMessage(e.message);
return null;
res = this._badEncryptedMessage(e.message);
}
// at this point, we've either successfully decrypted the event, or have given up
// (and set res to a 'badEncryptedMessage'). Either way, we can now set the
// cleartext of the event and raise Event.decrypted.
//
// make sure we clear '_decrypting' before sending the 'Event.decrypted' event,
// otherwise the app will be confused to see `isBeingDecrypted` still set when
// there isn't an `Event.decrypted` on the way.
//
// see also notes on _retryDecryption above.
//
this._decrypting = false;
this._retryDecryption = false;
this._setClearData(res);
return;
}
});
},
_badEncryptedMessage: function(reason) {
this.setClearData({
return {
clearEvent: {
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
});
},
};
},
/**
@@ -412,29 +450,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {string=} senderCurve25519Key Key owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getSenderKey}.
*
* @param {string=} claimedEd25519Key ed25519 key claimed by the sender of
* this event. See {@link module:models/event.MatrixEvent#getClaimedEd25519Key}.
*
* @param {Array<string>=} forwardingCurve25519KeyChain list of curve25519 keys
* involved in telling us about the senderCurve25519Key and claimedEd25519Key.
* See {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}.
* @param {module:crypto~EventDecryptionResult} decryptionResult
* the decryption result, including the plaintext and some key info
*/
setClearData: function(
clearEvent,
senderCurve25519Key,
claimedEd25519Key,
forwardingCurve25519KeyChain,
) {
this._clearEvent = clearEvent;
this._senderCurve25519Key = senderCurve25519Key || null;
this._claimedEd25519Key = claimedEd25519Key || null;
this._forwardingCurve25519KeyChain = forwardingCurve25519KeyChain || [];
_setClearData: function(decryptionResult) {
this._clearEvent = decryptionResult.clearEvent;
this._senderCurve25519Key =
decryptionResult.senderCurve25519Key || null;
this._claimedEd25519Key =
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.emit("Event.decrypted", this);
},