You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-09 10:22:46 +03:00
The earlier commit, d3ce0cb82f
, has most of the juicy details on this. In addition to d3ce's changes, we also:
* Use `TestClient` in many integration tests due to subtle behaviour changes in imports when switching to ES6. Namely the behaviour where setting the request function is less reliable in the way we did it, but `TestClient` is very reliable.
* We now use the Olm loader more often to avoid having to maintain so much duplicate code. This makes the imports slightly easier to read.
344 lines
13 KiB
JavaScript
344 lines
13 KiB
JavaScript
import '../../../olm-loader';
|
|
import * as algorithms from "../../../../src/crypto/algorithms";
|
|
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
|
import {MockStorageApi} from "../../../MockStorageApi";
|
|
import * as testUtils from "../../../test-utils";
|
|
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
|
import {Crypto} from "../../../../src/crypto";
|
|
import {logger} from "../../../../src/logger";
|
|
import {MatrixEvent} from "../../../../src/models/event";
|
|
|
|
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
|
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
|
|
|
const ROOM_ID = '!ROOM:ID';
|
|
|
|
const Olm = global.Olm;
|
|
|
|
describe("MegolmDecryption", function() {
|
|
if (!global.Olm) {
|
|
logger.warn('Not running megolm unit tests: libolm not present');
|
|
return;
|
|
}
|
|
|
|
beforeAll(function() {
|
|
return Olm.init();
|
|
});
|
|
|
|
let megolmDecryption;
|
|
let mockOlmLib;
|
|
let mockCrypto;
|
|
let mockBaseApis;
|
|
|
|
beforeEach(async function() {
|
|
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
|
mockBaseApis = {};
|
|
|
|
const mockStorage = new MockStorageApi();
|
|
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
|
|
|
const olmDevice = new OlmDevice(cryptoStore);
|
|
|
|
megolmDecryption = new MegolmDecryption({
|
|
userId: '@user:id',
|
|
crypto: mockCrypto,
|
|
olmDevice: olmDevice,
|
|
baseApis: mockBaseApis,
|
|
roomId: ROOM_ID,
|
|
});
|
|
|
|
|
|
// we stub out the olm encryption bits
|
|
mockOlmLib = {};
|
|
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
|
mockOlmLib.encryptMessageForDevice =
|
|
jest.fn().mockResolvedValue(undefined);
|
|
megolmDecryption.olmlib = mockOlmLib;
|
|
});
|
|
|
|
describe('receives some keys:', function() {
|
|
let groupSession;
|
|
beforeEach(async function() {
|
|
groupSession = new global.Olm.OutboundGroupSession();
|
|
groupSession.create();
|
|
|
|
// 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',
|
|
room_id: ROOM_ID,
|
|
session_id: groupSession.session_id(),
|
|
session_key: groupSession.session_key(),
|
|
},
|
|
},
|
|
senderCurve25519Key: "SENDER_CURVE25519",
|
|
claimedEd25519Key: "SENDER_ED25519",
|
|
};
|
|
|
|
const mockCrypto = {
|
|
decryptEvent: function() {
|
|
return Promise.resolve(decryptedData);
|
|
},
|
|
};
|
|
|
|
await event.attemptDecryption(mockCrypto).then(() => {
|
|
megolmDecryption.onRoomKeyEvent(event);
|
|
});
|
|
});
|
|
|
|
it('can decrypt an event', function() {
|
|
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: groupSession.session_id(),
|
|
ciphertext: groupSession.encrypt(JSON.stringify({
|
|
room_id: ROOM_ID,
|
|
content: 'testytest',
|
|
})),
|
|
},
|
|
});
|
|
|
|
return megolmDecryption.decryptEvent(event).then((res) => {
|
|
expect(res.clearEvent.content).toEqual('testytest');
|
|
});
|
|
});
|
|
|
|
it('can respond to a key request event', function() {
|
|
const keyRequest = {
|
|
userId: '@alice:foo',
|
|
deviceId: 'alidevice',
|
|
requestBody: {
|
|
room_id: ROOM_ID,
|
|
sender_key: "SENDER_CURVE25519",
|
|
session_id: groupSession.session_id(),
|
|
},
|
|
};
|
|
|
|
return megolmDecryption.hasKeysForKeyRequest(
|
|
keyRequest,
|
|
).then((hasKeys) => {
|
|
expect(hasKeys).toBe(true);
|
|
|
|
// set up some pre-conditions for the share call
|
|
const deviceInfo = {};
|
|
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
|
|
|
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
|
'@alice:foo': {'alidevice': {
|
|
sessionId: 'alisession',
|
|
}},
|
|
});
|
|
|
|
const awaitEncryptForDevice = new Promise((res, rej) => {
|
|
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
|
res();
|
|
return Promise.resolve();
|
|
});
|
|
});
|
|
|
|
mockBaseApis.sendToDevice = jest.fn();
|
|
|
|
// do the share
|
|
megolmDecryption.shareKeysWithDevice(keyRequest);
|
|
|
|
// it's asynchronous, so we have to wait a bit
|
|
return awaitEncryptForDevice;
|
|
}).then(() => {
|
|
// check that it called encryptMessageForDevice with
|
|
// appropriate args.
|
|
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
|
|
|
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
|
const payload = call[6];
|
|
|
|
expect(payload.type).toEqual("m.forwarded_room_key");
|
|
expect(payload.content).toMatchObject({
|
|
sender_key: "SENDER_CURVE25519",
|
|
sender_claimed_ed25519_key: "SENDER_ED25519",
|
|
session_id: groupSession.session_id(),
|
|
chain_index: 0,
|
|
forwarding_curve25519_key_chain: [],
|
|
});
|
|
expect(payload.content.session_key).toBeDefined();
|
|
});
|
|
});
|
|
|
|
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 = jest.fn();
|
|
const failureHandler = jest.fn((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).not.toHaveBeenCalled();
|
|
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
|
|
});
|
|
});
|
|
|
|
it("re-uses sessions for sequential messages", async function() {
|
|
const mockStorage = new MockStorageApi();
|
|
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
|
|
|
const olmDevice = new OlmDevice(cryptoStore);
|
|
olmDevice.verifySignature = jest.fn();
|
|
await olmDevice.init();
|
|
|
|
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
|
one_time_keys: {
|
|
'@alice:home.server': {
|
|
aliceDevice: {
|
|
'signed_curve25519:flooble': {
|
|
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
|
signatures: {
|
|
'@alice:home.server': {
|
|
'ed25519:aliceDevice': 'totally valid',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
|
|
|
|
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
|
'@alice:home.server': {
|
|
aliceDevice: {
|
|
deviceId: 'aliceDevice',
|
|
isBlocked: jest.fn().mockReturnValue(false),
|
|
isUnverified: jest.fn().mockReturnValue(false),
|
|
getIdentityKey: jest.fn().mockReturnValue(
|
|
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
|
),
|
|
getFingerprint: jest.fn().mockReturnValue(''),
|
|
},
|
|
},
|
|
}));
|
|
|
|
const megolmEncryption = new MegolmEncryption({
|
|
userId: '@user:id',
|
|
crypto: mockCrypto,
|
|
olmDevice: olmDevice,
|
|
baseApis: mockBaseApis,
|
|
roomId: ROOM_ID,
|
|
config: {
|
|
rotation_period_ms: 9999999999999,
|
|
},
|
|
});
|
|
const mockRoom = {
|
|
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
|
[{userId: "@alice:home.server"}],
|
|
),
|
|
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
|
};
|
|
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
|
body: "Some text",
|
|
});
|
|
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
|
|
|
// this should have claimed a key for alice as it's starting a new session
|
|
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
|
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
|
);
|
|
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
|
['@alice:home.server'], false,
|
|
);
|
|
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
|
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
|
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
|
);
|
|
|
|
mockBaseApis.claimOneTimeKeys.mockReset();
|
|
|
|
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
|
body: "Some more text",
|
|
});
|
|
|
|
// this should *not* have claimed a key as it should be using the same session
|
|
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
|
|
|
// likewise they should show the same session ID
|
|
expect(ct2.session_id).toEqual(ct1.session_id);
|
|
});
|
|
});
|
|
});
|