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

Move Device Tracking Data to Crypto Store #594

Merged
merged 22 commits into from
Jan 29, 2018
Merged

Conversation

dbkr
Copy link
Member

@dbkr dbkr commented Jan 12, 2018

Moves the device tracking to the crypto store.

Most of the cross-tab safety here works by writing all the device data atomically, so it's all stored in one big object, much as it feels like an abuse of indexeddb.

This also changes the API a little (in particular there's an explicit save to avoid multiple writes when we mark a room full of devices as known)

 * 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
Don't end up with devices / device tracking status being null
Includes making saveIfDirty() return a promise in case you care
about when changes got saved (which the test does).
Allow localstorage store to take a localstorage impl, make TestClient
pass a cryptostore & fix True/true typo
Mostly making tests aware of new storage format or making them
force it to be written. Also some bugfixes like we didn't json
encode some things in the localstorage store and we didn't
correctly check the promise when requesting device data saves.
 * Check whether we share an e2e room with user IDs in the 'left'
   field of /keys/changes: there's no guarantee we no longer share
   any e2e rooms with these users
 * Reset everyone's tracking status on an initial sync - just
   re-fetching device lists for all users we're currently tracking
   isn't good enough since room memberships may have changed.
 * Fix typo in test
 * Change test for new storage layer
 * Always store device keys we download, even if we weren't
   tracking the user.
It may change whilst processing the sync
This is more normal, and the code doesn't expect to get empty
objects here which is reasonable since it never sets one.
@dbkr dbkr changed the title [WIP] Move Device Tracking Data to Crypto Store Move Device Tracking Data to Crypto Store Jan 18, 2018
}

/**
* Gets the current sync token
Copy link
Member

Choose a reason for hiding this comment

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

the current sync token? I'm not sure I even know what it means for a sync token to be current.

@@ -72,7 +92,7 @@ describe('DeviceList', function() {
queryDefer1.resolve(utils.deepCopy(signedDeviceList));

return prom1.then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
Copy link
Member Author

Choose a reason for hiding this comment

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

We can pull the stored key straight out of the DeviceList now rather than inspecting the backing storage, which I think makes it more of a unit test.

expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
Copy link
Member Author

Choose a reason for hiding this comment

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

Most of the test changes are the same pattern: add the explicit save calls and wait on them, and change the fetch from the store to the new callback based API.

* @return {Object} userId->deviceId->{object} devices, or null if
* there is no data for this user.
*/
getRawStoredDevicesForUser(userId) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This got added because crypto/index.js was rifling around in the session store itself to get data out, so rather than make it rifle around in indexeddb instead (which wouldn't necessarily be up to date anyway), I've given it a function to get the data it wants from the DeviceList class.

Copy link
Member

Choose a reason for hiding this comment

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

Why not just use getStoredDevice ?

Copy link
Member Author

Choose a reason for hiding this comment

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

It could do, but getStoredDevice returns a DeviceInfo so we'd have to adapt the code to manipulate the DeviceInfo class and save it back rather than just manipulating the data as stored.

@@ -326,10 +471,6 @@ export default class DeviceList {
}
}

// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();
Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to a saveIfDirty() (above, for no particular reason)

users.forEach((u) => {
this._devices[u] = newDevices[u];
this._dirty = true;

Copy link
Member Author

Choose a reason for hiding this comment

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

The serialiser used to save the device keys to localstorage as they arrived, but since it store now lives in the DeviceList class, it made more sense to pass all the changes back in the promise.

See the License for the specific language governing permissions and
limitations under the License.
*/

Copy link
Member Author

Choose a reason for hiding this comment

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

This is all just adding support to the crypto store, fairly obviously.

*/
storeEndToEndDevicesForUser: function(userId, devices) {
Copy link
Member Author

Choose a reason for hiding this comment

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

We never write to this store anymore :)

* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
Copy link
Member Author

Choose a reason for hiding this comment

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

...or need individual devices from it, only ever all of them at once for the migration.

if (this.opts.crypto && !isCachedResponse) {
// tell the crypto module we're about to process a sync
// response
await this.opts.crypto.onSyncWillProcess(syncEventData);
Copy link
Member Author

Choose a reason for hiding this comment

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

Adds the callback for before the sync is processed - this is where we stop tracking all the devices on an initial sync, so we mark the appropriate ones as tracked again during the processing of the sync.

oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this._catchingUp,
};
Copy link
Member Author

Choose a reason for hiding this comment

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

This got declared earlier so we can pass the same data structure into the various different hooks (lots of them need to know if this is an initial sync or not, etc). The catchingUp flag therefore needs to be updated once the sync is actually processed.

if (this._savePromise === null) {
this._savePromise = Promise.delay(500).then(() => {
console.log('Saving device tracking data at token ' + this._syncToken);
return this._cryptoStore.doTxn(
Copy link
Member

Choose a reason for hiding this comment

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

is there scope for this to fail and leave _savePromise non-null forever?

Copy link
Member Author

@dbkr dbkr Jan 22, 2018

Choose a reason for hiding this comment

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

mmm, maybe. Changed to clear this before we start the write to fix the below, which should fix this too.

);
}).then(() => {
this._dirty = false;
this._savePromise = null;
Copy link
Member

Choose a reason for hiding this comment

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

this looks racy: we are clearing _savePromise some time after reading the current state, and we might miss updates which happen in that window

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, true. Fixed now I think by clearing the promise before we start the write.

if (!this._dirty) return Promise.resolve(false);

if (this._savePromise === null) {
this._savePromise = Promise.delay(500).then(() => {
Copy link
Member

Choose a reason for hiding this comment

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

this code path doesn't return a value.

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, good spot

* Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
*
* @return {Promise<bool>} true is the data was saved, false if
Copy link
Member

Choose a reason for hiding this comment

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

might be worth noting that it may take 500ms to resolve...?

Copy link
Member Author

Choose a reason for hiding this comment

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

true, done

this._deviceTrackingStatus = {}; // 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
Copy link
Member

Choose a reason for hiding this comment

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

represtenting

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, done

*
* @param {string} userId the user to get data for
*
* @return {Object} userId->deviceId->{object} devices, or null if
Copy link
Member

Choose a reason for hiding this comment

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

surely just deviceId->{object} ?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, yep

* @return {Object} userId->deviceId->{object} devices, or null if
* there is no data for this user.
*/
getRawStoredDevicesForUser(userId) {
Copy link
Member

Choose a reason for hiding this comment

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

Why not just use getStoredDevice ?

this._deviceList.stopTrackingDeviceList(u);
});
Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLists) {
// No point processing device list changes for initial syncs: they'd be meaningless
Copy link
Member

Choose a reason for hiding this comment

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

initial syncs have empty change lists iirc

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, I suspected they probably did. updated comment to reflect


await this.handleDeviceListChanges(r);
deviceLists.left.forEach((u) => {
if (!e2eUserIds.has(u)) {
Copy link
Member

Choose a reason for hiding this comment

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

this looks O(N^2) ish

Copy link
Member Author

@dbkr dbkr Jan 22, 2018

Choose a reason for hiding this comment

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

The set membership test shouldn't be O(n) though?

Copy link
Member

Choose a reason for hiding this comment

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

er good point

}

storeEndToEndDeviceData(deviceData, txn) {
this._deviceData = deviceData;
Copy link
Member

Choose a reason for hiding this comment

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

wondering if there is any point in this at all, given DeviceList maintains its own copy

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it is a bit pointless but this is for the unit tests anyway so it basically just has to obey the interface.

@richvdh richvdh assigned dbkr and unassigned richvdh Jan 18, 2018
@@ -154,6 +159,11 @@ export default class DeviceList {
// in quick succession (eg. when a whole room's devices are marked as known)
this._savePromise = Promise.delay(500).then(() => {
console.log('Saving device tracking data at token ' + this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewsie for the dirty flag.
Copy link
Member

Choose a reason for hiding this comment

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

Likwsie

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, fixed

@@ -510,7 +665,7 @@ class DeviceListUpdateSerialiser {
console.log('Completed key download for ' + downloadUsers);

this._downloadInProgress = false;
deferred.resolve();
deferred.resolve(this._updatedDevices);
Copy link
Member

Choose a reason for hiding this comment

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

ok so the problem with passing the updated device list back to the caller and having the caller actually persist the updates is twofold:

firstly, note that this function calls itself recursively just below here and when that happens, the result is discarded. Any results from queued queries will therefore be lost (I'm surprised that the tests don't pick this up, but shrug)

Secondly I'm concerned that there may be a race wherein the caller hasn't yet updated the device list before _processQueryResponseForUser reads it again and builds a response based on the old value rather than the new one. I'm not actually sure if that's a problem, but we should think about it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Mmm, I see what you mean. I've made it manipulate the device list by passing the DeviceList object in and using the getter and a new setter, which shouldn't have that problem and doesn't seem too bad.


await this.handleDeviceListChanges(r);
deviceLists.left.forEach((u) => {
if (!e2eUserIds.has(u)) {
Copy link
Member

Choose a reason for hiding this comment

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

er good point

@richvdh richvdh assigned dbkr and unassigned richvdh Jan 24, 2018
@dbkr dbkr removed their assignment Jan 29, 2018
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

lgtm except for lying comment

@@ -446,18 +605,22 @@ class DeviceListUpdateSerialiser {
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
* users. Returns a fresh device list object for the users queried.
Copy link
Member

Choose a reason for hiding this comment

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

LIES

Copy link
Member Author

Choose a reason for hiding this comment

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

oops, fixed

this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
this._deviceList._setRawStoredDevicesForUser(userId, storage);
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is any more elegant than just manipulating the device list directly, but fine

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably not, but it seemed prudent to at least do it via setters & getters.

@richvdh richvdh assigned dbkr and unassigned richvdh Jan 29, 2018
@dbkr dbkr merged commit f1e874c into develop Jan 29, 2018
krombel added a commit to krombel/matrix-js-sdk that referenced this pull request Mar 21, 2018
[Full Changelog](matrix-org/matrix-js-sdk@v0.9.2...v0.10.0-rc.1)
* Fix duplicated state events in timeline from peek
[\matrix-org#630](matrix-org#630)
* Create indexeddb worker when starting the store
[\matrix-org#627](matrix-org#627)
* Fix indexeddb logging
[\matrix-org#626](matrix-org#626)
* Don't do /keys/changes on incremental sync
[\matrix-org#625](matrix-org#625)
* Don't mark devicelist dirty unnecessarily
[\matrix-org#623](matrix-org#623)
* Cache the joined member count for a room state
[\matrix-org#619](matrix-org#619)
* Fix JS doc
[\matrix-org#618](matrix-org#618)
* Precompute push actions for state events
[\matrix-org#617](matrix-org#617)
* Fix bug where global "Never send to unverified..." is ignored
[\matrix-org#616](matrix-org#616)
* Intern legacy top-level 'membership' field
[\matrix-org#615](matrix-org#615)
* Don't synthesize RR for m.room.redaction as causes the RR to go missing.
[\matrix-org#598](matrix-org#598)
* Make Events create Dates on demand
[\matrix-org#613](matrix-org#613)
* Stop cloning events when adding to state
[\matrix-org#612](matrix-org#612)
* De-dup code: use the initialiseState function
[\matrix-org#611](matrix-org#611)
* Create sentinel members on-demand
[\matrix-org#610](matrix-org#610)
* Some more doc on how sentinels work
[\matrix-org#609](matrix-org#609)
* Migrate room encryption store to crypto store
[\matrix-org#597](matrix-org#597)
* add parameter to getIdentityServerUrl to strip the protocol for invites
[\matrix-org#600](matrix-org#600)
* Move Device Tracking Data to Crypto Store
[\matrix-org#594](matrix-org#594)
* Optimise pushprocessor
[\matrix-org#591](matrix-org#591)
* Set event error before emitting
[\matrix-org#592](matrix-org#592)
* Add event type for stickers [WIP]
[\matrix-org#590](matrix-org#590)
* Migrate inbound sessions to cryptostore
[\matrix-org#587](matrix-org#587)
* Disambiguate names if they contain an mxid
[\matrix-org#588](matrix-org#588)
* Check for sessions in indexeddb before migrating
[\matrix-org#585](matrix-org#585)
* Emit an event for crypto store migration
[\matrix-org#586](matrix-org#586)
* Supporting fixes For making UnknownDeviceDialog not pop up automatically
[\matrix-org#575](matrix-org#575)
* Move sessions to the crypto store
[\matrix-org#584](matrix-org#584)
* Change crypto store transaction API
[\matrix-org#582](matrix-org#582)
* Add some missed copyright notices
[\matrix-org#581](matrix-org#581)
* Move Olm account to IndexedDB
[\matrix-org#579](matrix-org#579)
* Fix logging of DecryptionErrors to be more useful
[\matrix-org#580](matrix-org#580)
* [BREAKING] Change the behaviour of the unverfied devices blacklist flag
[\matrix-org#568](matrix-org#568)
* Support set_presence=offline for syncing
[\matrix-org#557](matrix-org#557)
* Consider cases where the sender may not redact their own event
[\matrix-org#556](matrix-org#556)
krombel added a commit to krombel/matrix-js-sdk that referenced this pull request Apr 18, 2018
[Full Changelog](matrix-org/matrix-js-sdk@v0.9.2...v0.10.0-rc.1)
* Fix duplicated state events in timeline from peek
[\matrix-org#630](matrix-org#630)
* Create indexeddb worker when starting the store
[\matrix-org#627](matrix-org#627)
* Fix indexeddb logging
[\matrix-org#626](matrix-org#626)
* Don't do /keys/changes on incremental sync
[\matrix-org#625](matrix-org#625)
* Don't mark devicelist dirty unnecessarily
[\matrix-org#623](matrix-org#623)
* Cache the joined member count for a room state
[\matrix-org#619](matrix-org#619)
* Fix JS doc
[\matrix-org#618](matrix-org#618)
* Precompute push actions for state events
[\matrix-org#617](matrix-org#617)
* Fix bug where global "Never send to unverified..." is ignored
[\matrix-org#616](matrix-org#616)
* Intern legacy top-level 'membership' field
[\matrix-org#615](matrix-org#615)
* Don't synthesize RR for m.room.redaction as causes the RR to go missing.
[\matrix-org#598](matrix-org#598)
* Make Events create Dates on demand
[\matrix-org#613](matrix-org#613)
* Stop cloning events when adding to state
[\matrix-org#612](matrix-org#612)
* De-dup code: use the initialiseState function
[\matrix-org#611](matrix-org#611)
* Create sentinel members on-demand
[\matrix-org#610](matrix-org#610)
* Some more doc on how sentinels work
[\matrix-org#609](matrix-org#609)
* Migrate room encryption store to crypto store
[\matrix-org#597](matrix-org#597)
* add parameter to getIdentityServerUrl to strip the protocol for invites
[\matrix-org#600](matrix-org#600)
* Move Device Tracking Data to Crypto Store
[\matrix-org#594](matrix-org#594)
* Optimise pushprocessor
[\matrix-org#591](matrix-org#591)
* Set event error before emitting
[\matrix-org#592](matrix-org#592)
* Add event type for stickers [WIP]
[\matrix-org#590](matrix-org#590)
* Migrate inbound sessions to cryptostore
[\matrix-org#587](matrix-org#587)
* Disambiguate names if they contain an mxid
[\matrix-org#588](matrix-org#588)
* Check for sessions in indexeddb before migrating
[\matrix-org#585](matrix-org#585)
* Emit an event for crypto store migration
[\matrix-org#586](matrix-org#586)
* Supporting fixes For making UnknownDeviceDialog not pop up automatically
[\matrix-org#575](matrix-org#575)
* Move sessions to the crypto store
[\matrix-org#584](matrix-org#584)
* Change crypto store transaction API
[\matrix-org#582](matrix-org#582)
* Add some missed copyright notices
[\matrix-org#581](matrix-org#581)
* Move Olm account to IndexedDB
[\matrix-org#579](matrix-org#579)
* Fix logging of DecryptionErrors to be more useful
[\matrix-org#580](matrix-org#580)
* [BREAKING] Change the behaviour of the unverfied devices blacklist flag
[\matrix-org#568](matrix-org#568)
* Support set_presence=offline for syncing
[\matrix-org#557](matrix-org#557)
* Consider cases where the sender may not redact their own event
[\matrix-org#556](matrix-org#556)
@t3chguy t3chguy deleted the dbkr/device_tracking_indexeddb branch May 10, 2022 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants