You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Initial attempt at device tracking -> indexeddb
* Message sending works again, but * Marking multiple devices known (ie. 'send anyway') is very slow because it writes all device info out each time * Support for non-indexedb stores not written yet * No migration
This commit is contained in:
@@ -25,6 +25,7 @@ import Promise from 'bluebird';
|
|||||||
|
|
||||||
import DeviceInfo from './deviceinfo';
|
import DeviceInfo from './deviceinfo';
|
||||||
import olmlib from './olmlib';
|
import olmlib from './olmlib';
|
||||||
|
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||||
|
|
||||||
|
|
||||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||||
@@ -58,26 +59,95 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
|||||||
* @alias module:crypto/DeviceList
|
* @alias module:crypto/DeviceList
|
||||||
*/
|
*/
|
||||||
export default class DeviceList {
|
export default class DeviceList {
|
||||||
constructor(baseApis, sessionStore, olmDevice) {
|
constructor(baseApis, cryptoStore, olmDevice) {
|
||||||
this._sessionStore = sessionStore;
|
this._cryptoStore = cryptoStore;
|
||||||
this._serialiser = new DeviceListUpdateSerialiser(
|
|
||||||
baseApis, sessionStore, olmDevice,
|
// userId -> {
|
||||||
);
|
// deviceId -> {
|
||||||
|
// [device info]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
this._devices = null;
|
||||||
|
|
||||||
// which users we are tracking device status for.
|
// which users we are tracking device status for.
|
||||||
// userId -> TRACKING_STATUS_*
|
// userId -> TRACKING_STATUS_*
|
||||||
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
|
this._deviceTrackingStatus = null; // loaded from storage in load()
|
||||||
|
|
||||||
|
// The 'next_batch' sync token at the point the data was writen,
|
||||||
|
// ie. a token represtenting the point immediately after the
|
||||||
|
// moment represented by the snapshot in the db.
|
||||||
|
this._syncToken = null;
|
||||||
|
|
||||||
|
this._serialiser = new DeviceListUpdateSerialiser(
|
||||||
|
baseApis, olmDevice,
|
||||||
|
);
|
||||||
|
|
||||||
|
// userId -> promise
|
||||||
|
this._keyDownloadsInProgressByUser = {};
|
||||||
|
|
||||||
|
// Set whenever changes are made other than setting the sync token
|
||||||
|
this._dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the device tracking state from storage
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
await this._cryptoStore.doTxn(
|
||||||
|
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||||
|
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||||
|
this._devices = deviceData ? deviceData.devices : {},
|
||||||
|
this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {};
|
||||||
|
this._syncToken = deviceData ? deviceData.syncToken : null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
for (const u of Object.keys(this._deviceTrackingStatus)) {
|
for (const u of Object.keys(this._deviceTrackingStatus)) {
|
||||||
// if a download was in progress when we got shut down, it isn't any more.
|
// if a download was in progress when we got shut down, it isn't any more.
|
||||||
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// userId -> promise
|
/**
|
||||||
this._keyDownloadsInProgressByUser = {};
|
* Save the device tracking state to storage, if any changes are
|
||||||
|
* pending other than updating the sync token
|
||||||
|
* Before calling this, the caller must ensure that the state it
|
||||||
|
* has set this object to is consistent, ie. the appropriate sync
|
||||||
|
* token has been set with setSyncToken for any device updates that
|
||||||
|
* have occurred.
|
||||||
|
*/
|
||||||
|
async saveIfDirty() {
|
||||||
|
if (!this._dirty) return;
|
||||||
|
await this._cryptoStore.doTxn(
|
||||||
|
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||||
|
this._cryptoStore.storeEndToEndDeviceData({
|
||||||
|
devices: this._devices,
|
||||||
|
trackingStatus: this._deviceTrackingStatus,
|
||||||
|
syncToken: this._syncToken,
|
||||||
|
}, txn);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this._dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.lastKnownSyncToken = null;
|
/**
|
||||||
|
* Gets the current sync token
|
||||||
|
*
|
||||||
|
* @return {string} The sync token
|
||||||
|
*/
|
||||||
|
getSyncToken() {
|
||||||
|
return this._syncToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current sync token
|
||||||
|
*
|
||||||
|
* @param {string} st The sync token
|
||||||
|
*/
|
||||||
|
setSyncToken(st) {
|
||||||
|
this._syncToken = st;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,7 +222,7 @@ export default class DeviceList {
|
|||||||
* managed to get a list of devices for this user yet.
|
* managed to get a list of devices for this user yet.
|
||||||
*/
|
*/
|
||||||
getStoredDevicesForUser(userId) {
|
getStoredDevicesForUser(userId) {
|
||||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
const devs = this._devices[userId];
|
||||||
if (!devs) {
|
if (!devs) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -165,6 +235,22 @@ export default class DeviceList {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored device data for a user, in raw object form
|
||||||
|
*
|
||||||
|
* @param {string} userId the user to get data for
|
||||||
|
*
|
||||||
|
* @return {Object} userId->deviceId->{object} devices, or null if
|
||||||
|
* there is no data for this user.
|
||||||
|
*/
|
||||||
|
getRawStoredDevicesForUser(userId) {
|
||||||
|
const devs = this._devices[userId];
|
||||||
|
if (!devs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return devs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the stored keys for a single device
|
* Get the stored keys for a single device
|
||||||
*
|
*
|
||||||
@@ -175,7 +261,7 @@ export default class DeviceList {
|
|||||||
* if we don't know about this device
|
* if we don't know about this device
|
||||||
*/
|
*/
|
||||||
getStoredDevice(userId, deviceId) {
|
getStoredDevice(userId, deviceId) {
|
||||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
const devs = this._devices[userId];
|
||||||
if (!devs || !devs[deviceId]) {
|
if (!devs || !devs[deviceId]) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -200,7 +286,7 @@ export default class DeviceList {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
const devices = this._devices[userId];
|
||||||
if (!devices) {
|
if (!devices) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -229,6 +315,14 @@ export default class DeviceList {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the list of devices for a user with the given device list
|
||||||
|
*/
|
||||||
|
storeDevicesForUser(u, devs) {
|
||||||
|
this._devices[u] = devs;
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* flag the given user for device-list tracking, if they are not already.
|
* flag the given user for device-list tracking, if they are not already.
|
||||||
*
|
*
|
||||||
@@ -254,8 +348,8 @@ export default class DeviceList {
|
|||||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||||
}
|
}
|
||||||
// we don't yet persist the tracking status, since there may be a lot
|
// we don't yet persist the tracking status, since there may be a lot
|
||||||
// of calls; instead we wait for the forthcoming
|
// of calls; we save all data together once the sync is done
|
||||||
// refreshOutdatedDeviceLists.
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,8 +367,9 @@ export default class DeviceList {
|
|||||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
|
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
|
||||||
}
|
}
|
||||||
// we don't yet persist the tracking status, since there may be a lot
|
// we don't yet persist the tracking status, since there may be a lot
|
||||||
// of calls; instead we wait for the forthcoming
|
// of calls; we save all data together once the sync is done
|
||||||
// refreshOutdatedDeviceLists.
|
|
||||||
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -295,8 +390,9 @@ export default class DeviceList {
|
|||||||
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||||
}
|
}
|
||||||
// we don't yet persist the tracking status, since there may be a lot
|
// we don't yet persist the tracking status, since there may be a lot
|
||||||
// of calls; instead we wait for the forthcoming
|
// of calls; we save all data together once the sync is done
|
||||||
// refreshOutdatedDeviceLists.
|
|
||||||
|
this._dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,6 +414,8 @@ export default class DeviceList {
|
|||||||
* is no need to wait for this (it's mostly for the unit tests).
|
* is no need to wait for this (it's mostly for the unit tests).
|
||||||
*/
|
*/
|
||||||
refreshOutdatedDeviceLists() {
|
refreshOutdatedDeviceLists() {
|
||||||
|
this.saveIfDirty();
|
||||||
|
|
||||||
const usersToDownload = [];
|
const usersToDownload = [];
|
||||||
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
for (const userId of Object.keys(this._deviceTrackingStatus)) {
|
||||||
const stat = this._deviceTrackingStatus[userId];
|
const stat = this._deviceTrackingStatus[userId];
|
||||||
@@ -326,10 +424,6 @@ export default class DeviceList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we didn't persist the tracking status during
|
|
||||||
// invalidateUserDeviceList, so do it now.
|
|
||||||
this._persistDeviceTrackingStatus();
|
|
||||||
|
|
||||||
return this._doKeyDownload(usersToDownload);
|
return this._doKeyDownload(usersToDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,14 +446,14 @@ export default class DeviceList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prom = this._serialiser.updateDevicesForUsers(
|
const prom = this._serialiser.updateDevicesForUsers(
|
||||||
users, this.lastKnownSyncToken,
|
this._devices, users, this._syncToken,
|
||||||
).then(() => {
|
).then((newDevices) => {
|
||||||
finished(true);
|
finished(newDevices);
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
console.error(
|
console.error(
|
||||||
'Error downloading keys for ' + users + ":", e,
|
'Error downloading keys for ' + users + ":", e,
|
||||||
);
|
);
|
||||||
finished(false);
|
finished(null);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -371,7 +465,7 @@ export default class DeviceList {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const finished = (success) => {
|
const finished = (newDevices) => {
|
||||||
users.forEach((u) => {
|
users.forEach((u) => {
|
||||||
// we may have queued up another download request for this user
|
// we may have queued up another download request for this user
|
||||||
// since we started this request. If that happens, we should
|
// since we started this request. If that happens, we should
|
||||||
@@ -384,25 +478,23 @@ export default class DeviceList {
|
|||||||
delete this._keyDownloadsInProgressByUser[u];
|
delete this._keyDownloadsInProgressByUser[u];
|
||||||
const stat = this._deviceTrackingStatus[u];
|
const stat = this._deviceTrackingStatus[u];
|
||||||
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||||
if (success) {
|
if (newDevices) {
|
||||||
// we didn't get any new invalidations since this download started:
|
// we didn't get any new invalidations since this download started:
|
||||||
// this user's device list is now up to date.
|
// this user's device list is now up to date.
|
||||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
|
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
|
||||||
|
this._devices[u] = newDevices[u];
|
||||||
|
this._dirty = true;
|
||||||
console.log("Device list for", u, "now up to date");
|
console.log("Device list for", u, "now up to date");
|
||||||
} else {
|
} else {
|
||||||
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._persistDeviceTrackingStatus();
|
this.saveIfDirty();
|
||||||
};
|
};
|
||||||
|
|
||||||
return prom;
|
return prom;
|
||||||
}
|
}
|
||||||
|
|
||||||
_persistDeviceTrackingStatus() {
|
|
||||||
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -415,9 +507,8 @@ export default class DeviceList {
|
|||||||
* time (and queuing other requests up).
|
* time (and queuing other requests up).
|
||||||
*/
|
*/
|
||||||
class DeviceListUpdateSerialiser {
|
class DeviceListUpdateSerialiser {
|
||||||
constructor(baseApis, sessionStore, olmDevice) {
|
constructor(baseApis, olmDevice) {
|
||||||
this._baseApis = baseApis;
|
this._baseApis = baseApis;
|
||||||
this._sessionStore = sessionStore;
|
|
||||||
this._olmDevice = olmDevice;
|
this._olmDevice = olmDevice;
|
||||||
|
|
||||||
this._downloadInProgress = false;
|
this._downloadInProgress = false;
|
||||||
@@ -431,14 +522,15 @@ class DeviceListUpdateSerialiser {
|
|||||||
// non-null indicates that we have users queued for download.
|
// non-null indicates that we have users queued for download.
|
||||||
this._queuedQueryDeferred = null;
|
this._queuedQueryDeferred = null;
|
||||||
|
|
||||||
// sync token to be used for the next query: essentially the
|
this._devices = null; // the complete device list
|
||||||
// most recent one we know about
|
this._updatedDevices = null; // device list updates we've fetched
|
||||||
this._nextSyncToken = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a key query request for the given users
|
* Make a key query request for the given users
|
||||||
*
|
*
|
||||||
|
* @param {object} devices The current device list
|
||||||
|
*
|
||||||
* @param {String[]} users list of user ids
|
* @param {String[]} users list of user ids
|
||||||
*
|
*
|
||||||
* @param {String} syncToken sync token to pass in the query request, to
|
* @param {String} syncToken sync token to pass in the query request, to
|
||||||
@@ -446,13 +538,12 @@ class DeviceListUpdateSerialiser {
|
|||||||
*
|
*
|
||||||
* @return {module:client.Promise} resolves when all the users listed have
|
* @return {module:client.Promise} resolves when all the users listed have
|
||||||
* been updated. rejects if there was a problem updating any of the
|
* been updated. rejects if there was a problem updating any of the
|
||||||
* users.
|
* users. Returns a fresh device list object for the users queried.
|
||||||
*/
|
*/
|
||||||
updateDevicesForUsers(users, syncToken) {
|
updateDevicesForUsers(devices, users, syncToken) {
|
||||||
users.forEach((u) => {
|
users.forEach((u) => {
|
||||||
this._keyDownloadsQueuedByUser[u] = true;
|
this._keyDownloadsQueuedByUser[u] = true;
|
||||||
});
|
});
|
||||||
this._nextSyncToken = syncToken;
|
|
||||||
|
|
||||||
if (!this._queuedQueryDeferred) {
|
if (!this._queuedQueryDeferred) {
|
||||||
this._queuedQueryDeferred = Promise.defer();
|
this._queuedQueryDeferred = Promise.defer();
|
||||||
@@ -464,11 +555,13 @@ class DeviceListUpdateSerialiser {
|
|||||||
return this._queuedQueryDeferred.promise;
|
return this._queuedQueryDeferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._devices = devices;
|
||||||
|
this._updatedDevices = {};
|
||||||
// start a new download.
|
// start a new download.
|
||||||
return this._doQueuedQueries();
|
return this._doQueuedQueries(syncToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
_doQueuedQueries() {
|
_doQueuedQueries(syncToken) {
|
||||||
if (this._downloadInProgress) {
|
if (this._downloadInProgress) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
|
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
|
||||||
@@ -484,8 +577,8 @@ class DeviceListUpdateSerialiser {
|
|||||||
this._downloadInProgress = true;
|
this._downloadInProgress = true;
|
||||||
|
|
||||||
const opts = {};
|
const opts = {};
|
||||||
if (this._nextSyncToken) {
|
if (syncToken) {
|
||||||
opts.token = this._nextSyncToken;
|
opts.token = syncToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._baseApis.downloadKeysForUsers(
|
this._baseApis.downloadKeysForUsers(
|
||||||
@@ -510,11 +603,11 @@ class DeviceListUpdateSerialiser {
|
|||||||
console.log('Completed key download for ' + downloadUsers);
|
console.log('Completed key download for ' + downloadUsers);
|
||||||
|
|
||||||
this._downloadInProgress = false;
|
this._downloadInProgress = false;
|
||||||
deferred.resolve();
|
deferred.resolve(this._updatedDevices);
|
||||||
|
|
||||||
// if we have queued users, fire off another request.
|
// if we have queued users, fire off another request.
|
||||||
if (this._queuedQueryDeferred) {
|
if (this._queuedQueryDeferred) {
|
||||||
this._doQueuedQueries();
|
this._doQueuedQueries(syncToken);
|
||||||
}
|
}
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
|
||||||
@@ -530,7 +623,7 @@ class DeviceListUpdateSerialiser {
|
|||||||
|
|
||||||
// map from deviceid -> deviceinfo for this user
|
// map from deviceid -> deviceinfo for this user
|
||||||
const userStore = {};
|
const userStore = {};
|
||||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
const devs = this._devices[userId];
|
||||||
if (devs) {
|
if (devs) {
|
||||||
Object.keys(devs).forEach((deviceId) => {
|
Object.keys(devs).forEach((deviceId) => {
|
||||||
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||||
@@ -548,9 +641,7 @@ class DeviceListUpdateSerialiser {
|
|||||||
storage[deviceId] = userStore[deviceId].toStorage();
|
storage[deviceId] = userStore[deviceId].toStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._sessionStore.storeEndToEndDevicesForUser(
|
this._updatedDevices[userId] = storage;
|
||||||
userId, storage,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
|||||||
this._cryptoStore = cryptoStore;
|
this._cryptoStore = cryptoStore;
|
||||||
|
|
||||||
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||||
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
|
this._deviceList = new DeviceList(baseApis, cryptoStore, this._olmDevice);
|
||||||
|
|
||||||
// the last time we did a check for the number of one-time-keys on the
|
// the last time we did a check for the number of one-time-keys on the
|
||||||
// server.
|
// server.
|
||||||
@@ -128,6 +128,7 @@ Crypto.prototype.init = async function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this._olmDevice.init();
|
await this._olmDevice.init();
|
||||||
|
await this._deviceList.load();
|
||||||
|
|
||||||
// build our device keys: these will later be uploaded
|
// build our device keys: these will later be uploaded
|
||||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||||
@@ -135,7 +136,7 @@ Crypto.prototype.init = async function() {
|
|||||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||||
this._olmDevice.deviceCurve25519Key;
|
this._olmDevice.deviceCurve25519Key;
|
||||||
|
|
||||||
let myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
let myDevices = this._deviceList.getRawStoredDevicesForUser(
|
||||||
this._userId,
|
this._userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -153,9 +154,10 @@ Crypto.prototype.init = async function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
myDevices[this._deviceId] = deviceInfo;
|
myDevices[this._deviceId] = deviceInfo;
|
||||||
this._sessionStore.storeEndToEndDevicesForUser(
|
this._deviceList.storeDevicesForUser(
|
||||||
this._userId, myDevices,
|
this._userId, myDevices,
|
||||||
);
|
);
|
||||||
|
this._deviceList.saveIfDirty();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -456,7 +458,7 @@ Crypto.prototype.getStoredDevice = function(userId, deviceId) {
|
|||||||
Crypto.prototype.setDeviceVerification = async function(
|
Crypto.prototype.setDeviceVerification = async function(
|
||||||
userId, deviceId, verified, blocked, known,
|
userId, deviceId, verified, blocked, known,
|
||||||
) {
|
) {
|
||||||
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||||
if (!devices || !devices[deviceId]) {
|
if (!devices || !devices[deviceId]) {
|
||||||
throw new Error("Unknown device " + userId + ":" + deviceId);
|
throw new Error("Unknown device " + userId + ":" + deviceId);
|
||||||
}
|
}
|
||||||
@@ -484,7 +486,8 @@ Crypto.prototype.setDeviceVerification = async function(
|
|||||||
if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
|
if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
|
||||||
dev.verified = verificationStatus;
|
dev.verified = verificationStatus;
|
||||||
dev.known = knownStatus;
|
dev.known = knownStatus;
|
||||||
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
|
this._deviceList.storeDevicesForUser(userId, devices);
|
||||||
|
this._deviceList.saveIfDirty();
|
||||||
}
|
}
|
||||||
return DeviceInfo.fromStorage(dev, deviceId);
|
return DeviceInfo.fromStorage(dev, deviceId);
|
||||||
};
|
};
|
||||||
@@ -812,21 +815,31 @@ Crypto.prototype.decryptEvent = function(event) {
|
|||||||
* @param {Object} deviceLists device_lists field from /sync, or response from
|
* @param {Object} deviceLists device_lists field from /sync, or response from
|
||||||
* /keys/changes
|
* /keys/changes
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.handleDeviceListChanges = async function(deviceLists) {
|
Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLists) {
|
||||||
if (deviceLists.changed && Array.isArray(deviceLists.changed)) {
|
// No point processing device list changes for initial syncs: they'd be meaningless
|
||||||
deviceLists.changed.forEach((u) => {
|
// since the server doesn't know what point we were were at previously. We'll either
|
||||||
this._deviceList.invalidateUserDeviceList(u);
|
// get the complete list of changes for the interval or invalidate everything in
|
||||||
});
|
// onSyncComplete
|
||||||
}
|
if (!syncData.oldSyncToken) return;
|
||||||
|
|
||||||
if (deviceLists.left && Array.isArray(deviceLists.left)) {
|
if (syncData.oldSyncToken === this._deviceList.getSyncToken()) {
|
||||||
deviceLists.left.forEach((u) => {
|
// the point the db is at matches where the sync started from, so
|
||||||
this._deviceList.stopTrackingDeviceList(u);
|
// we can safely write the changes
|
||||||
});
|
this._evalDeviceListChanges(syncDeviceLists);
|
||||||
|
} else {
|
||||||
|
// the db is at a different point to where this sync started from, so
|
||||||
|
// additionally fetch the changes between where the db is and where the
|
||||||
|
// sync started
|
||||||
|
console.log(
|
||||||
|
"Device list sync gap detected - fetching key changes between " +
|
||||||
|
this._deviceList.getSyncToken() + " and " + syncData.oldSyncToken,
|
||||||
|
);
|
||||||
|
const gapDeviceLists = await this._baseApis.getKeyChanges(
|
||||||
|
this._deviceList.getSyncToken(), syncData.oldSyncToken,
|
||||||
|
);
|
||||||
|
this._evalDeviceListChanges(gapDeviceLists);
|
||||||
|
this._evalDeviceListChanges(syncDeviceLists);
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't flush the outdated device list yet - we do it once we finish
|
|
||||||
// processing the sync.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -890,36 +903,15 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
|||||||
const nextSyncToken = syncData.nextSyncToken;
|
const nextSyncToken = syncData.nextSyncToken;
|
||||||
|
|
||||||
if (!syncData.oldSyncToken) {
|
if (!syncData.oldSyncToken) {
|
||||||
console.log("Completed initial sync");
|
// If we have a stored device sync token, we could request the complete
|
||||||
|
// list of device changes from the server here to get our device list up
|
||||||
// if we have a deviceSyncToken, we can tell the deviceList to
|
// to date. This case should be relatively rare though (only when you hit
|
||||||
// invalidate devices which have changed since then.
|
// 'clear cache and reload' in practice) so we just invalidate everything.
|
||||||
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
|
console.log("invalidating all device list caches after inital sync");
|
||||||
if (oldSyncToken !== null) {
|
this._deviceList.invalidateAllDeviceLists();
|
||||||
try {
|
|
||||||
await this._invalidateDeviceListsSince(
|
|
||||||
oldSyncToken, nextSyncToken,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// if that failed, we fall back to invalidating everyone.
|
|
||||||
console.warn("Error fetching changed device list", e);
|
|
||||||
this._deviceList.invalidateAllDeviceLists();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise, we have to invalidate all devices for all users we
|
|
||||||
// are tracking.
|
|
||||||
console.log("Completed first initialsync; invalidating all " +
|
|
||||||
"device list caches");
|
|
||||||
this._deviceList.invalidateAllDeviceLists();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this._deviceList.setSyncToken(syncData.nextSyncToken);
|
||||||
// we can now store our sync token so that we can get an update on
|
this._deviceList.saveIfDirty();
|
||||||
// restart rather than having to invalidate everyone.
|
|
||||||
//
|
|
||||||
// (we don't really need to do this on every sync - we could just
|
|
||||||
// do it periodically)
|
|
||||||
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
|
|
||||||
|
|
||||||
// catch up on any new devices we got told about during the sync.
|
// catch up on any new devices we got told about during the sync.
|
||||||
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
this._deviceList.lastKnownSyncToken = nextSyncToken;
|
||||||
@@ -936,25 +928,24 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the server which users have new devices since a given token,
|
* Trigger the appropriate invalidations and removes for a given
|
||||||
* and invalidate them
|
* device list
|
||||||
*
|
*
|
||||||
* @param {String} oldSyncToken
|
* @param {Object} deviceLists device_lists field from /sync, or response from
|
||||||
* @param {String} lastKnownSyncToken
|
* /keys/changes
|
||||||
*
|
|
||||||
* Returns a Promise which resolves once the query is complete. Rejects if the
|
|
||||||
* keyChange query fails.
|
|
||||||
*/
|
*/
|
||||||
Crypto.prototype._invalidateDeviceListsSince = async function(
|
Crypto.prototype._evalDeviceListChanges = async function(deviceLists) {
|
||||||
oldSyncToken, lastKnownSyncToken,
|
if (deviceLists.changed && Array.isArray(deviceLists.changed)) {
|
||||||
) {
|
deviceLists.changed.forEach((u) => {
|
||||||
const r = await this._baseApis.getKeyChanges(
|
this._deviceList.invalidateUserDeviceList(u);
|
||||||
oldSyncToken, lastKnownSyncToken,
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
console.log("got key changes since", oldSyncToken, ":", r);
|
if (deviceLists.left && Array.isArray(deviceLists.left)) {
|
||||||
|
deviceLists.left.forEach((u) => {
|
||||||
await this.handleDeviceListChanges(r);
|
this._deviceList.stopTrackingDeviceList(u);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import utils from '../../utils';
|
import utils from '../../utils';
|
||||||
|
|
||||||
export const VERSION = 4;
|
export const VERSION = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of a CryptoStore which is backed by an existing
|
* Implementation of a CryptoStore which is backed by an existing
|
||||||
@@ -391,6 +391,23 @@ export class Backend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEndToEndDeviceData(txn, func) {
|
||||||
|
const objectStore = txn.objectStore("device_data");
|
||||||
|
const getReq = objectStore.get("-");
|
||||||
|
getReq.onsuccess = function() {
|
||||||
|
try {
|
||||||
|
func(getReq.result || null);
|
||||||
|
} catch (e) {
|
||||||
|
abortWithException(txn, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
storeEndToEndDeviceData(deviceData, txn) {
|
||||||
|
const objectStore = txn.objectStore("device_data");
|
||||||
|
objectStore.put(deviceData, "-");
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -423,6 +440,9 @@ export function upgradeDatabase(db, oldVersion) {
|
|||||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 5) {
|
||||||
|
db.createObjectStore("device_data");
|
||||||
|
}
|
||||||
// Expand as needed.
|
// Expand as needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,30 @@ export default class IndexedDBCryptoStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End-to-end device tracking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the state of all tracked devices
|
||||||
|
* This contains devices for each user, a tracking state for each user
|
||||||
|
* and a sync token matching the point in time the snapshot represents.
|
||||||
|
* These all need to be written out in full each time such that the snapshot
|
||||||
|
* is always consistent, so they are stored in one object.
|
||||||
|
*
|
||||||
|
* @param {Object} deviceData
|
||||||
|
*/
|
||||||
|
storeEndToEndDeviceData(deviceData, txn) {
|
||||||
|
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the state of all tracked devices
|
||||||
|
*
|
||||||
|
* @param {*} txn An active transaction. See doTxn().
|
||||||
|
*/
|
||||||
|
getEndToEndDeviceData(txn, func) {
|
||||||
|
return this._backendPromise.value().getEndToEndDeviceData(txn, func);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -389,3 +413,4 @@ export default class IndexedDBCryptoStore {
|
|||||||
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
|
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
|
||||||
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
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';
|
||||||
|
|||||||
22
src/sync.js
22
src/sync.js
@@ -621,8 +621,14 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
|||||||
await client.store.setSyncData(data);
|
await client.store.setSyncData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncEventData = {
|
||||||
|
oldSyncToken: syncToken,
|
||||||
|
nextSyncToken: data.next_batch,
|
||||||
|
catchingUp: this._catchingUp,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._processSyncResponse(syncToken, data, isCachedResponse);
|
await this._processSyncResponse(syncEventData, data, isCachedResponse);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// log the exception with stack if we have it, else fall back
|
// log the exception with stack if we have it, else fall back
|
||||||
// to the plain description
|
// to the plain description
|
||||||
@@ -630,12 +636,6 @@ SyncApi.prototype._sync = async function(syncOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// emit synced events
|
// emit synced events
|
||||||
const syncEventData = {
|
|
||||||
oldSyncToken: syncToken,
|
|
||||||
nextSyncToken: data.next_batch,
|
|
||||||
catchingUp: this._catchingUp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!syncOptions.hasSyncedBefore) {
|
if (!syncOptions.hasSyncedBefore) {
|
||||||
this._updateSyncState("PREPARED", syncEventData);
|
this._updateSyncState("PREPARED", syncEventData);
|
||||||
syncOptions.hasSyncedBefore = true;
|
syncOptions.hasSyncedBefore = true;
|
||||||
@@ -708,7 +708,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
|
|||||||
* @param {bool} isCachedResponse True if this response is from our local cache
|
* @param {bool} isCachedResponse True if this response is from our local cache
|
||||||
*/
|
*/
|
||||||
SyncApi.prototype._processSyncResponse = async function(
|
SyncApi.prototype._processSyncResponse = async function(
|
||||||
syncToken, data, isCachedResponse,
|
syncEventData, data, isCachedResponse,
|
||||||
) {
|
) {
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -950,7 +950,7 @@ SyncApi.prototype._processSyncResponse = async function(
|
|||||||
self._deregisterStateListeners(room);
|
self._deregisterStateListeners(room);
|
||||||
room.resetLiveTimeline(
|
room.resetLiveTimeline(
|
||||||
joinObj.timeline.prev_batch,
|
joinObj.timeline.prev_batch,
|
||||||
self.opts.canResetEntireTimeline(room.roomId) ? null : syncToken,
|
self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We have to assume any gap in any timeline is
|
// We have to assume any gap in any timeline is
|
||||||
@@ -1034,7 +1034,7 @@ SyncApi.prototype._processSyncResponse = async function(
|
|||||||
// in the timeline relative to ones paginated in by /notifications.
|
// in the timeline relative to ones paginated in by /notifications.
|
||||||
// XXX: we could fix this by making EventTimeline support chronological
|
// XXX: we could fix this by making EventTimeline support chronological
|
||||||
// ordering... but it doesn't, right now.
|
// ordering... but it doesn't, right now.
|
||||||
if (syncToken && this._notifEvents.length) {
|
if (syncEventData.oldSyncToken && this._notifEvents.length) {
|
||||||
this._notifEvents.sort(function(a, b) {
|
this._notifEvents.sort(function(a, b) {
|
||||||
return a.getTs() - b.getTs();
|
return a.getTs() - b.getTs();
|
||||||
});
|
});
|
||||||
@@ -1046,7 +1046,7 @@ SyncApi.prototype._processSyncResponse = async function(
|
|||||||
// Handle device list updates
|
// Handle device list updates
|
||||||
if (data.device_lists) {
|
if (data.device_lists) {
|
||||||
if (this.opts.crypto) {
|
if (this.opts.crypto) {
|
||||||
await this.opts.crypto.handleDeviceListChanges(data.device_lists);
|
await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists);
|
||||||
} else {
|
} else {
|
||||||
// FIXME if we *don't* have a crypto module, we still need to
|
// FIXME if we *don't* have a crypto module, we still need to
|
||||||
// invalidate the device lists. But that would require a
|
// invalidate the device lists. But that would require a
|
||||||
|
|||||||
Reference in New Issue
Block a user