1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-01 04:43:29 +03:00

retry key backups when they fail

This commit is contained in:
Hubert Chathi
2018-10-04 15:19:20 -04:00
parent 2f4c1dfcc4
commit 258adda67c
6 changed files with 399 additions and 59 deletions

View File

@@ -98,7 +98,7 @@ describe("MegolmBackup", function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new global.Olm.PkEncryption(); mockCrypto.backupKey = new global.Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key( mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK", "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
); );
mockCrypto.backupInfo = { mockCrypto.backupInfo = {
version: 1, version: 1,
@@ -134,7 +134,7 @@ describe("MegolmBackup", function() {
megolmDecryption.olmlib = mockOlmLib; megolmDecryption.olmlib = mockOlmLib;
}); });
it('automatically backs up keys', function() { it('automatically calls the key back up', function() {
const groupSession = new global.Olm.OutboundGroupSession(); const groupSession = new global.Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -169,6 +169,194 @@ describe("MegolmBackup", function() {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
}); });
}); });
it('sends backups to the server', function () {
const groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new global.Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
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));
const 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()
.then(() => {
return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK"
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(callback, method, path, queryParams, data, opts) {
expect(++numCalls <= 1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id());
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key());
})
.then(() => {
expect(numCalls).toBe(1);
});
});
});
it('retries when a backup fails', function () {
const groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new global.Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
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));
const 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()
.then(() => {
return cryptoStore.doTxn("readwrite", [cryptoStore.STORE_SESSION], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK"
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(callback, method, path, queryParams, data, opts) {
expect(++numCalls <= 2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(groupSession.session_id());
if (numCalls > 1) {
resolve();
return Promise.resolve({});
} else {
return Promise.reject(new Error("this is an expected failure"));
}
};
client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key());
})
.then(() => {
expect(numCalls).toBe(2);
});
});
});
}); });
describe("restore", function() { describe("restore", function() {

View File

@@ -848,6 +848,8 @@ MatrixClient.prototype.enableKeyBackup = function(info) {
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
this.emit('keyBackupStatus', true); this.emit('keyBackupStatus', true);
this._crypto._maybeSendKeyBackup();
}; };
/** /**

View File

@@ -83,6 +83,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this.backupInfo = null; // The info dict from /room_keys/version this.backupInfo = null; // The info dict from /room_keys/version
this.backupKey = null; // The encryption key object this.backupKey = null; // The encryption key object
this._checkedForBackup = false; // Have we checked the server for a backup we can use? this._checkedForBackup = false; // Have we checked the server for a backup we can use?
this._sendingBackups = false; // Are we currently sending backups?
this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList( this._deviceList = new DeviceList(
@@ -965,51 +966,62 @@ Crypto.prototype.importRoomKeys = function(keys) {
); );
}; };
Crypto.prototype._backupPayloadForSession = function( Crypto.prototype._maybeSendKeyBackup = async function() {
senderKey, forwardingCurve25519KeyChain, if (!this._sendingBackups) {
sessionId, sessionKey, keysClaimed, this._sendingBackups = true;
exportFormat, while (1) {
) { if (!this.backupKey) {
// new session. this._sendingBackups = false;
const session = new Olm.InboundGroupSession(); return;
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
} }
if (sessionId != session.session_id()) { // FIXME: figure out what limit is reasonable
throw new Error( const sessions = await this._cryptoStore.getSessionsNeedingBackup(10);
"Mismatched group session ID from senderKey: " + if (!sessions.length) {
senderKey, this._sendingBackups = false;
); return;
} }
const data = {};
for (const session of sessions) {
const room_id = session.sessionData.room_id;
if (data[room_id] === undefined)
data[room_id] = {sessions: {}};
if (!exportFormat) { const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData);
sessionKey = session.export_session(); sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
} delete sessionData.session_id;
const firstKnownIndex = session.first_known_index(); delete sessionData.room_id;
const sessionData = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: senderKey,
sender_claimed_keys: keysClaimed,
session_key: sessionKey,
forwarding_curve25519_key_chain: forwardingCurve25519KeyChain,
};
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
return {
first_message_index: firstKnownIndex, data[room_id]['sessions'][session.sessionId] = {
forwarded_count: forwardingCurve25519KeyChain.length, first_message_index: 1, // FIXME
forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length,
is_verified: false, // FIXME: how do we determine this? is_verified: false, // FIXME: how do we determine this?
session_data: encrypted, session_data: encrypted,
}; };
} finally {
session.free();
} }
};
Crypto.prototype.backupGroupSession = function( let successful = false;
do {
if (!this.backupKey) {
this._sendingBackups = false;
return;
}
try {
await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data});
successful = true;
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
}
catch (e) {
console.log("send failed", e);
// FIXME: pause
}
} while (!successful);
// FIXME: pause between iterations?
}
}
}
Crypto.prototype.backupGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain, roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed, sessionId, sessionKey, keysClaimed,
exportFormat, exportFormat,
@@ -1018,26 +1030,26 @@ Crypto.prototype.backupGroupSession = function(
throw new Error("Key backups are not enabled"); throw new Error("Key backups are not enabled");
} }
const data = this._backupPayloadForSession( await this._cryptoStore.markSessionsNeedingBackup([{
senderKey, forwardingCurve25519KeyChain, senderKey: senderKey,
sessionId, sessionKey, keysClaimed, sessionId: sessionId,
exportFormat, }]);
);
return this._baseApis.sendKeyBackup(roomId, sessionId, this.backupInfo.version, data); this._maybeSendKeyBackup();
}; };
Crypto.prototype.backupAllGroupSessions = async function(version) { Crypto.prototype.backupAllGroupSessions = async function(version) {
const keys = await this.exportRoomKeys(); await this._cryptoStore.doTxn(
const data = {}; 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => {
for (const key of keys) { this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (data[key.room_id] === undefined) data[key.room_id] = {sessions: {}}; if (session !== null) {
this._cryptoStore.markSessionsNeedingBackup([session], txn);
data[key.room_id]['sessions'][key.session_id] = this._backupPayloadForSession(
key.sender_key, key.forwarding_curve25519_key_chain,
key.session_id, key.session_key, key.sender_claimed_keys, true,
);
} }
return this._baseApis.sendKeyBackup(undefined, undefined, version, {rooms: data}); });
}
);
this._maybeSendKeyBackup();
}; };
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307

View File

@@ -460,6 +460,71 @@ export class Backend {
}; };
} }
// session backups
getSessionsNeedingBackup(limit) {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly");
txn.onerror = reject;
txn.oncomplete = function() {
resolve(sessions);
}
const objectStore = txn.objectStore("sessions_needing_backup");
const sessionStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
const sessionGetReq = sessionStore.get(cursor.key)
sessionGetReq.onsuccess = function() {
sessions.push({
senderKey: sessionGetReq.result.senderCurve25519Key,
sessionId: sessionGetReq.result.sessionId,
sessionData: sessionGetReq.result.session
});
}
//sessions.push(cursor.value);
if (!limit || sessions.length < limit) {
cursor.continue();
}
}
}
});
}
unmarkSessionsNeedingBackup(sessions) {
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
console.log(session);
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
markSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
sessionId: session.sessionId
});
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode); const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn); const promise = promiseifyTxn(txn);
@@ -498,6 +563,11 @@ export function upgradeDatabase(db, oldVersion) {
if (oldVersion < 6) { if (oldVersion < 6) {
db.createObjectStore("rooms"); db.createObjectStore("rooms");
} }
if (oldVersion < 7) {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed. // Expand as needed.
} }

View File

@@ -407,6 +407,38 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().getEndToEndRooms(txn, func); this._backendPromise.value().getEndToEndRooms(txn, func);
} }
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* for no limit.
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
}
/**
* Unmark sessions as needing to be backed up.
* @param {[object]} The sessions that need to be backed up.
*/
unmarkSessionsNeedingBackup(sessions) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions);
});
}
/**
* Mark sessions as needing to be backed up.
* @param {[object]} The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may
@@ -440,3 +472,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';

View File

@@ -41,6 +41,8 @@ export default class MemoryCryptoStore {
this._deviceData = null; this._deviceData = null;
// roomId -> Opaque roomInfo object // roomId -> Opaque roomInfo object
this._rooms = {}; this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
} }
/** /**
@@ -295,6 +297,39 @@ export default class MemoryCryptoStore {
func(this._rooms); func(this._rooms);
} }
getSessionsNeedingBackup(limit) {
const sessions = [];
for (const session in this._sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
unmarkSessionsNeedingBackup(sessions) {
for(const session of sessions) {
delete this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
}
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
for(const session of sessions) {
this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
}
return Promise.resolve();
}
// Session key backups
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
return Promise.resolve(func(null)); return Promise.resolve(func(null));
} }