-
-
Notifications
You must be signed in to change notification settings - Fork 592
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
Conversation
* 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.
src/crypto/DeviceList.js
Outdated
} | ||
|
||
/** | ||
* Gets the current sync token |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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); | ||
}); |
There was a problem hiding this comment.
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.
src/crypto/DeviceList.js
Outdated
* @return {Object} userId->deviceId->{object} devices, or null if | ||
* there is no data for this user. | ||
*/ | ||
getRawStoredDevicesForUser(userId) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)
src/crypto/DeviceList.js
Outdated
users.forEach((u) => { | ||
this._devices[u] = newDevices[u]; | ||
this._dirty = true; | ||
|
There was a problem hiding this comment.
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. | ||
*/ | ||
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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, | ||
}; |
There was a problem hiding this comment.
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.
src/crypto/DeviceList.js
Outdated
if (this._savePromise === null) { | ||
this._savePromise = Promise.delay(500).then(() => { | ||
console.log('Saving device tracking data at token ' + this._syncToken); | ||
return this._cryptoStore.doTxn( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/crypto/DeviceList.js
Outdated
); | ||
}).then(() => { | ||
this._dirty = false; | ||
this._savePromise = null; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/crypto/DeviceList.js
Outdated
if (!this._dirty) return Promise.resolve(false); | ||
|
||
if (this._savePromise === null) { | ||
this._savePromise = Promise.delay(500).then(() => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, good spot
src/crypto/DeviceList.js
Outdated
* 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 |
There was a problem hiding this comment.
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...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, done
src/crypto/DeviceList.js
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
represtenting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, done
src/crypto/DeviceList.js
Outdated
* | ||
* @param {string} userId the user to get data for | ||
* | ||
* @return {Object} userId->deviceId->{object} devices, or null if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
surely just deviceId->{object} ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, yep
src/crypto/DeviceList.js
Outdated
* @return {Object} userId->deviceId->{object} devices, or null if | ||
* there is no data for this user. | ||
*/ | ||
getRawStoredDevicesForUser(userId) { |
There was a problem hiding this comment.
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
?
src/crypto/index.js
Outdated
this._deviceList.stopTrackingDeviceList(u); | ||
}); | ||
Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLists) { | ||
// No point processing device list changes for initial syncs: they'd be meaningless |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/crypto/DeviceList.js
Outdated
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likwsie
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, fixed
src/crypto/DeviceList.js
Outdated
@@ -510,7 +665,7 @@ class DeviceListUpdateSerialiser { | |||
console.log('Completed key download for ' + downloadUsers); | |||
|
|||
this._downloadInProgress = false; | |||
deferred.resolve(); | |||
deferred.resolve(this._updatedDevices); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
er good point
There was a problem hiding this 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
src/crypto/DeviceList.js
Outdated
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LIES
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
[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)
[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)
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)