Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

keep track of event ID and timestamp of decrypted messages #555

Merged
merged 2 commits into from
Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions spec/unit/crypto/algorithms/megolm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,88 @@ describe("MegolmDecryption", function() {
expect(payload.content.session_key).toExist();
});
});

it("can detect replay attacks", function() {
// trying to decrypt two different messages (marked by different
// event IDs or timestamps) using the same (sender key, session id,
// message index) triple should result in an exception being thrown
// as it should be detected as a replay attack.
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event1 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});

const successHandler = expect.createSpy();
const failureHandler = expect.createSpy()
.andCall((err) => {
expect(err.toString()).toMatch(
/Duplicate message index, possible replay attack/,
);
});

return megolmDecryption.decryptEvent(event1).then((res) => {
const event2 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event2",
origin_server_ts: 1507754149000,
});

return megolmDecryption.decryptEvent(event2);
}).then(
successHandler,
failureHandler,
).then(() => {
expect(successHandler).toNotHaveBeenCalled();
expect(failureHandler).toHaveBeenCalled();
});
});

it("allows re-decryption of the same event", function() {
// in contrast with the previous test, if the event ID and
// timestamp are the same, then it should not be considered a
// replay attack
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});

return megolmDecryption.decryptEvent(event).then((res) => {
return megolmDecryption.decryptEvent(event);
// test is successful if no exception is thrown
});
});
});
});
37 changes: 27 additions & 10 deletions src/crypto/OlmDevice.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,18 @@ function OlmDevice(sessionStore) {
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// TODO: If we ever remove an event from memory we will also need to remove
// it from this map. Otherwise if we download the event from the server we
// will think that it is a duplicate.
// When we decrypt a message and the message index matches a previously
// decrypted message, one possible cause of that is that we are decrypting
// the same event, and may not indicate an actual replay attack. For
// example, this could happen if we receive events, forget about them, and
// then re-fetch them when we backfill. So we store the event ID and
// timestamp corresponding to each message index when we first decrypt it,
// and compare these against the event ID and timestamp every time we use
// that same index. If they match, then we're probably decrypting the same
// event and we don't consider it a replay attack.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
this._inboundGroupSessionMessageIndexes = {};
}

Expand Down Expand Up @@ -794,6 +800,8 @@ OlmDevice.prototype.importInboundGroupSession = async function(data) {
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
* @param {string} eventId ID of the event being decrypted
* @param {Number} timestamp timestamp of the event being decrypted
*
* @return {null} the sessionId is unknown
*
Expand All @@ -802,7 +810,7 @@ OlmDevice.prototype.importInboundGroupSession = async function(data) {
* keysClaimed: Object<string, string>}>}
*/
OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body,
roomId, senderKey, sessionId, body, eventId, timestamp,
) {
const self = this;

Expand All @@ -815,14 +823,23 @@ OlmDevice.prototype.decryptGroupMessage = async function(
plaintext = res;
} else {
// Check if we have seen this message index before to detect replay attacks.
// If the event ID and timestamp are specified, and the match the event ID
// and timestamp from the last time we used this message index, then we
// don't consider it a replay attack.
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
const msgInfo = self._inboundGroupSessionMessageIndexes[messageIndexKey];
if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
}
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
self._inboundGroupSessionMessageIndexes[messageIndexKey] = {
id: eventId,
timestamp: timestamp,
};
}

sessionData.session = session.pickle(self._pickleKey);
Expand Down
1 change: 1 addition & 0 deletions src/crypto/algorithms/megolm.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
try {
res = await this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
event.getId(), event.getTs(),
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
Expand Down