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

Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups

This commit is contained in:
David Baker
2018-08-24 13:29:29 +01:00
7 changed files with 509 additions and 1 deletions

View File

@@ -0,0 +1,245 @@
/*
Copyright 2018 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.
*/
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm backup tests: libolm not available");
}
import expect from 'expect';
import Promise from 'bluebird';
import sdk from '../../..';
import algorithms from '../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../MockStorageApi';
import testUtils from '../../test-utils';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../lib/crypto/OlmDevice');
Crypto = require('../../../lib/crypto');
}
const MatrixClient = sdk.MatrixClient;
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
describe("MegolmBackup", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
let olmDevice;
let mockOlmLib;
let mockCrypto;
let mockStorage;
let sessionStore;
let cryptoStore;
let megolmDecryption;
beforeEach(function () {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new global.Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK"
);
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(sessionStore, cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
});
describe("backup", function() {
let mockBaseApis;
beforeEach(function() {
mockBaseApis = {};
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
});
it('automatically backs up keys', function() {
const 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",
};
mockCrypto.decryptEvent = function() {
return Promise.resolve(decryptedData);
};
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const msgevent = 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,
});
mockBaseApis.sendKeyBackup = expect.createSpy();
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockBaseApis.sendKeyBackup).toHaveBeenCalled();
});
});
});
describe("restore", function () {
let client;
beforeEach(function() {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
it('can restore from backup', 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: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc',
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs'
},
event_id: '$event1',
origin_server_ts: 1507753886000,
});
client._http.authedRequest = function () {
return Promise.resolve({
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
}
});
};
return client.restoreKeyBackups(
"qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD"
+ "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA",
ROOM_ID,
'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc'
).then(() => {
return megolmDecryption.decryptEvent(event);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
});
});

View File

@@ -732,6 +732,177 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
return this._crypto.importRoomKeys(keys); return this._crypto.importRoomKeys(keys);
}; };
/**
* Get information about the current key backup.
*/
MatrixClient.prototype.getKeyBackupVersion = function(callback) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._http.authedRequest(
undefined, "GET", "/room_keys/version",
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
callback(err);
return Promise.reject(err);
} else if (!(typeof res.auth_data === "object")
|| !res.auth_data.public_key) {
const err = "Invalid backup data returned";
callback(err);
return Promise.reject(err);
} else {
if (callback) {
callback(null, res);
}
return res;
}
});
}
/**
* Enable backing up of keys, using data previously returned from
* getKeyBackupVersion.
*/
MatrixClient.prototype.enableKeyBackup = function(info) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupKey = new global.Olm.PkEncryption();
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
}
/**
* Disable backing up of keys.
*/
MatrixClient.prototype.disableKeyBackup = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupKey = undefined;
}
/**
* Create a new key backup version and enable it.
*/
MatrixClient.prototype.createKeyBackupVersion = function(callback) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const decryption = new global.Olm.PkDecryption();
const public_key = decryption.generate_key();
const encryption = new global.Olm.PkEncryption();
encryption.set_recipient_key(public_key);
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
public_key: public_key,
}
};
this._crypto._signObject(data.auth_data);
return this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
).then((res) => {
this._crypto.backupKey = encryption;
// FIXME: pickle isn't the right thing to use, but we don't have
// anything else yet
const recovery_key = decryption.pickle("secret_key");
callback(null, recovery_key);
return recovery_key;
});
}
MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) {
let path;
if (sessionId !== undefined) {
path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
});
} else if (roomId !== undefined) {
path = utils.encodeUri("/room_keys/keys/$roomId", {
$roomId: roomId,
});
} else {
path = "/room_keys/keys";
}
const queryData = version === undefined ? undefined : {version : version};
return {
path: path,
queryData: queryData,
}
}
/**
* Back up session keys to the homeserver.
* @param {string} roomId ID of the room that the keys are for Optional.
* @param {string} sessionId ID of the session that the keys are for Optional.
* @param {integer} version backup version Optional.
* @param {object} key data
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} a promise that will resolve when the keys
* are uploaded
*/
MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data, callback) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
callback, "PUT", path.path, path.queryData, data,
);
};
MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
// FIXME: see the FIXME in createKeyBackupVersion
const decryption = new global.Olm.PkDecryption();
decryption.unpickle("secret_key", decryptionKey);
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
).then((res) => {
const keys = [];
// FIXME: for each room, session, if response has multiple
// decrypt response.data.session_data
const session_data = res.session_data;
const key = JSON.parse(decryption.decrypt(
session_data.ephemeral,
session_data.mac,
session_data.ciphertext
));
// set room_id and session_id
key.room_id = roomId;
key.session_id = sessionId;
keys.push(key);
return this.importRoomKeys(keys);
}).then(() => {
if (callback) {
callback();
}
})
};
MatrixClient.prototype.deleteKeyBackups = function(roomId, sessionId, version, callback) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
callback, "DELETE", path.path, path.queryData,
)
};
// Group ops // Group ops
// ========= // =========
// Operations on groups that come down the sync stream (ie. ones the // Operations on groups that come down the sync stream (ie. ones the
@@ -3695,6 +3866,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* }); * });
*/ */
/**
* Fires when we want to suggest to the user that they restore their megolm keys
* from backup or by cross-signing the device.
*
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
*/
// EventEmitter JSDocs // EventEmitter JSDocs

View File

@@ -91,6 +91,17 @@ function OlmDevice(sessionStore, cryptoStore) {
this.deviceEd25519Key = null; this.deviceEd25519Key = null;
this._maxOneTimeKeys = null; this._maxOneTimeKeys = null;
// track which of our other devices (if any) have cross-signed this device
// XXX: this should probably have a single source of truth in the /devices
// API store or whatever we use to track our self-signed devices.
this.crossSelfSigs = [];
// track whether we have already suggested to the user that they should
// restore their keys from backup or by cross-signing the device.
// We use this to avoid repeatedly emitting the suggestion event.
// XXX: persist this somewhere!
this.suggestedKeyRestore = false;
// we don't bother stashing outboundgroupsessions in the sessionstore - // we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here. // instead we keep them here.
this._outboundGroupSessionStore = {}; this._outboundGroupSessionStore = {};

View File

@@ -814,7 +814,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
} }
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession( return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId, content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed, content.session_key, keysClaimed,
exportFormat, exportFormat,
@@ -829,6 +829,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId); this._retryDecryption(senderKey, sessionId);
}).then(() => {
return this.backupGroupSession(
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
);
}).catch((e) => { }).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`); console.error(`Error handling m.room_key_event: ${e}`);
}); });
@@ -951,6 +957,54 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
}); });
}; };
MegolmDecryption.prototype.backupGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
exportFormat,
) {
// new session.
const session = new Olm.InboundGroupSession();
let first_known_index;
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
if (!exportFormat) {
sessionKey = session.export_session();
}
const first_known_index = session.first_known_index();
const sessionData = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: senderKey,
sender_claimed_keys: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
session_key: sessionKey
};
const encrypted = this._crypto.backupKey.encrypt(JSON.stringify(sessionData));
const data = {
first_message_index: first_known_index,
forwarded_count: forwardingCurve25519KeyChain.length,
is_verified: false, // FIXME: how do we determine this?
session_data: encrypted
};
return this._baseApis.sendKeyBackup(roomId, sessionId, data);
} catch (e) {
return Promise.reject(e);
} finally {
session.free();
}
}
/** /**
* Have another go at decrypting events after we receive a key * Have another go at decrypting events after we receive a key
* *

View File

@@ -72,6 +72,11 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._cryptoStore = cryptoStore; this._cryptoStore = cryptoStore;
this._roomList = roomList; this._roomList = roomList;
// track whether this device's megolm keys are being backed up incrementally
// to the server or not.
// XXX: this should probably have a single source of truth from OlmAccount
this.backupKey = null;
this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList( this._deviceList = new DeviceList(
baseApis, cryptoStore, sessionStore, this._olmDevice, baseApis, cryptoStore, sessionStore, this._olmDevice,

View File

@@ -35,6 +35,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
*/ */
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* matrix algorithm tag for megolm backups
*/
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1";
/** /**
* Encrypt an event payload for an Olm device * Encrypt an event payload for an Olm device

View File

@@ -1088,6 +1088,17 @@ SyncApi.prototype._processSyncResponse = async function(
async function processRoomEvent(e) { async function processRoomEvent(e) {
client.emit("event", e); client.emit("event", e);
if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) {
/*
// XXX: get device
if (!device.getSuggestedKeyRestore() &&
!device.backupKey && !device.selfCrossSigs.length)
{
client.emit("crypto.suggestKeyRestore");
device.setSuggestedKeyRestore(true);
}
*/
await self.opts.crypto.onCryptoEvent(e); await self.opts.crypto.onCryptoEvent(e);
} }
} }