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

e2e key backups #684

Merged
merged 47 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fb1b554
initial pseudocode WIP for e2e online backups
ara4n Jan 15, 2018
e0c9b99
blindly move crypto.suggestKeyRestore over to /sync
ara4n Jan 18, 2018
69204d4
Merge branch 'develop' into matthew/e2e_backups
ara4n May 27, 2018
d556189
initial implementation of e2e key backup and restore
uhoreg Aug 8, 2018
1faf477
fix formatting and fix authedRequest usage
uhoreg Aug 23, 2018
fb8efe3
initial draft of API for working with backup versions
uhoreg Aug 23, 2018
75107f9
pass in key rather than decryption object to restoreKeyBackups
uhoreg Aug 23, 2018
e5ec479
check that crypto is enabled
uhoreg Aug 23, 2018
73e294b
add copyright header to backup.spec
uhoreg Aug 23, 2018
ec5fff2
Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk i…
dbkr Aug 24, 2018
017f81e
fix some bugs
uhoreg Aug 24, 2018
bf873bd
split the backup version creation into two different methods
uhoreg Aug 25, 2018
29db856
Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk i…
dbkr Sep 11, 2018
72bd51f
Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups
dbkr Sep 11, 2018
3838fab
WIP e2e key backup support
dbkr Sep 13, 2018
e789747
Check sigs on e2e backup & enable it if we can
dbkr Sep 14, 2018
073fb73
Make multi-room key restore work
dbkr Sep 17, 2018
009430e
Add isValidRecoveryKey
dbkr Sep 17, 2018
f75d188
Soe progress on linting
dbkr Sep 17, 2018
3af9af9
More linting
dbkr Sep 17, 2018
54c443a
Make tests pass
dbkr Sep 18, 2018
e4bb37b
Fix lint mostly
dbkr Sep 18, 2018
0bad7b2
Fix lint
dbkr Sep 18, 2018
a78825e
Bump to Olm 2.3.0 for PkEncryption
dbkr Sep 18, 2018
1b62a21
Free PkEncryption/Decryption objects
dbkr Sep 18, 2018
2f4c1df
Test all 3 code paths on backup restore
dbkr Sep 18, 2018
c556ca4
Support Olm with WebAssembly
dbkr Sep 25, 2018
63cc3fd
lint
dbkr Sep 25, 2018
33ad65a
Don't assume Olm will be available from start
dbkr Sep 26, 2018
e9b0aca
Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups
dbkr Oct 2, 2018
ce2058a
Merge branch 'dbkr/wasm' into dbkr/e2e_backups
dbkr Oct 2, 2018
7cd101d
Fix recovery key format
dbkr Oct 2, 2018
262ace1
commit the recovery key util file
dbkr Oct 3, 2018
258adda
retry key backups when they fail
uhoreg Oct 4, 2018
89c3f6f
Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups
dbkr Oct 5, 2018
b3fe05e
Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups
dbkr Oct 9, 2018
59e6066
Replace base58check with a simple parity check
dbkr Oct 9, 2018
ada4b6e
Lint
dbkr Oct 9, 2018
da65f43
wrap backup sending in a try, and add delays
uhoreg Oct 10, 2018
fc59bc2
add localstorage support for key backups
uhoreg Oct 10, 2018
3957006
Merge remote-tracking branch 'upstream/dbkr/e2e_backups' into e2e_bac…
uhoreg Oct 11, 2018
9b12c22
de-lint plus some minor fixes
uhoreg Oct 12, 2018
91fb7b0
fix unit tests for backup recovery
uhoreg Oct 12, 2018
d49c0a1
more de-linting and fixing
uhoreg Oct 12, 2018
40d0a82
remove accidental change to eslintrc
uhoreg Oct 12, 2018
434ac86
properly fill out the is_verified and first_message_index fields
uhoreg Oct 19, 2018
322ef1f
update backup algorithm name to agree with the proposal
uhoreg Oct 22, 2018
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
218 changes: 218 additions & 0 deletions spec/unit/crypto/backup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
try {
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
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 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/NCiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBlmkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs'
},
event_id: '$event1',
origin_server_ts: 1507753886000,
});
client._http.authedRequest = function () {
return Promise.resolve({
data: {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZSlne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOySyw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGFru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxvC+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpeUg5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3NfQHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPyiie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
}
},
headers: {},
code: 200
});
};
const decryption = new Olm.PkDecryption();
decryption.unpickle("secret_key", "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZDQWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA");
return client.restoreKeyBackups(decryption, ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc')
.then(() => {
return megolmDecryption.decryptEvent(event);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
});
});
90 changes: 90 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,90 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
return this._crypto.importRoomKeys(keys);
};

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");
}

const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
).then((response) => {
if (response.code === 200) {
const keys = [];
// FIXME: for each room, session, if response has multiple
// decrypt response.data.session_data
const data = response.data;
const key = JSON.parse(decryptionKey.decrypt(data.session_data.ephemeral, data.session_data.mac, data.session_data.ciphertext));
// set room_id and session_id
key.room_id = roomId;
key.session_id = sessionId;
keys.push(key);
return this.importRoomKeys(keys);
} else {
callback("aargh!");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need to do better than aargh ;)

return Promise.reject("aaargh!");
}
}).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
// =========
// Operations on groups that come down the sync stream (ie. ones the
Expand Down Expand Up @@ -3625,6 +3709,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

Expand Down
11 changes: 11 additions & 0 deletions src/crypto/OlmDevice.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ function OlmDevice(sessionStore, cryptoStore) {
this.deviceEd25519Key = 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 -
// instead we keep them here.
this._outboundGroupSessionStore = {};
Expand Down
Loading