Batch insertion of user data after downloading keys.

This commit is contained in:
Benoit Marty 2023-01-03 15:57:39 +01:00
parent f26178fc21
commit 4c4ef0d73e
4 changed files with 165 additions and 112 deletions

View File

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
@ -371,6 +372,8 @@ internal class DeviceListManager @Inject constructor(
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
} }
val userDataToStore = UserDataToStore()
for (userId in filteredUsers) { for (userId in filteredUsers) {
// al devices = // al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
@ -404,7 +407,7 @@ internal class DeviceListManager @Inject constructor(
} }
// Update the store // Update the store
// Note that devices which aren't in the response will be removed from the stores // Note that devices which aren't in the response will be removed from the stores
cryptoStore.storeUserDevices(userId, workingCopy) userDataToStore.userDevices[userId] = workingCopy
} }
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
@ -416,14 +419,11 @@ internal class DeviceListManager @Inject constructor(
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
} }
cryptoStore.storeUserCrossSigningKeys( userDataToStore.userCrossSigningKeys[userId] = Triple(masterKey, selfSigningKey, userSigningKey)
userId,
masterKey,
selfSigningKey,
userSigningKey
)
} }
cryptoStore.storeUserDataToStore(userDataToStore)
// Update devices trust for these users // Update devices trust for these users
// dispatchDeviceChange(downloadUsers) // dispatchDeviceChange(downloadUsers)

View File

@ -583,4 +583,6 @@ internal interface IMXCryptoStore {
fun areDeviceKeysUploaded(): Boolean fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase() fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
fun storeUserDataToStore(userDataToStore: UserDataToStore)
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
internal data class UserDataToStore(
val userDevices: MutableMap<String, Map<String, CryptoDeviceInfo>> = mutableMapOf(),
val userCrossSigningKeys: MutableMap<String, Triple<CryptoCrossSigningKey?, CryptoCrossSigningKey?, CryptoCrossSigningKey?>> = mutableMapOf(),
)

View File

@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrappe
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
@ -289,37 +290,41 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) { override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
if (devices == null) { storeUserDevices(realm, userId, devices)
Timber.d("Remove user $userId") }
// Remove the user }
UserEntity.delete(realm, userId)
} else { private fun storeUserDevices(realm: Realm, userId: String, devices: Map<String, CryptoDeviceInfo>?) {
val userEntity = UserEntity.getOrCreate(realm, userId) if (devices == null) {
// First delete the removed devices Timber.d("Remove user $userId")
val deviceIds = devices.keys // Remove the user
userEntity.devices.toTypedArray().iterator().let { UserEntity.delete(realm, userId)
while (it.hasNext()) { } else {
val deviceInfoEntity = it.next() val userEntity = UserEntity.getOrCreate(realm, userId)
if (deviceInfoEntity.deviceId !in deviceIds) { // First delete the removed devices
Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") val deviceIds = devices.keys
deviceInfoEntity.deleteOnCascade() userEntity.devices.toTypedArray().iterator().let {
} while (it.hasNext()) {
val deviceInfoEntity = it.next()
if (deviceInfoEntity.deviceId !in deviceIds) {
Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
deviceInfoEntity.deleteOnCascade()
} }
} }
// Then update existing devices or add new one }
devices.values.forEach { cryptoDeviceInfo -> // Then update existing devices or add new one
val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } devices.values.forEach { cryptoDeviceInfo ->
if (existingDeviceInfoEntity == null) { val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
// Add the device if (existingDeviceInfoEntity == null) {
Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") // Add the device
val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
newEntity.firstTimeSeenLocalTs = clock.epochMillis() val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
userEntity.devices.add(newEntity) newEntity.firstTimeSeenLocalTs = clock.epochMillis()
} else { userEntity.devices.add(newEntity)
// Update the device } else {
Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") // Update the device
CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
} CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
} }
} }
} }
@ -332,85 +337,95 @@ internal class RealmCryptoStore @Inject constructor(
userSigningKey: CryptoCrossSigningKey? userSigningKey: CryptoCrossSigningKey?
) { ) {
doRealmTransaction("storeUserCrossSigningKeys", realmConfiguration) { realm -> doRealmTransaction("storeUserCrossSigningKeys", realmConfiguration) { realm ->
UserEntity.getOrCreate(realm, userId) storeUserCrossSigningKeys(realm, userId, masterKey, selfSigningKey, userSigningKey)
.let { userEntity -> }
if (masterKey == null || selfSigningKey == null) { }
// The user has disabled cross signing?
userEntity.crossSigningInfoEntity?.deleteOnCascade() private fun storeUserCrossSigningKeys(
userEntity.crossSigningInfoEntity = null realm: Realm,
} else { userId: String,
var shouldResetMyDevicesLocalTrust = false masterKey: CryptoCrossSigningKey?,
CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> selfSigningKey: CryptoCrossSigningKey?,
// What should we do if we detect a change of the keys? userSigningKey: CryptoCrossSigningKey?
val existingMaster = signingInfo.getMasterKey() ) {
if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { UserEntity.getOrCreate(realm, userId)
crossSigningKeysMapper.update(existingMaster, masterKey) .let { userEntity ->
} else { if (masterKey == null || selfSigningKey == null) {
Timber.d("## CrossSigning MSK change for $userId") // The user has disabled cross signing?
val keyEntity = crossSigningKeysMapper.map(masterKey) userEntity.crossSigningInfoEntity?.deleteOnCascade()
signingInfo.setMasterKey(keyEntity) userEntity.crossSigningInfoEntity = null
if (userId == this.userId) { } else {
shouldResetMyDevicesLocalTrust = true var shouldResetMyDevicesLocalTrust = false
// my msk has changed! clear my private key CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo ->
// Could we have some race here? e.g I am the one that did change the keys // What should we do if we detect a change of the keys?
// could i get this update to early and clear the private keys? val existingMaster = signingInfo.getMasterKey()
// -> initializeCrossSigning is guarding for that by storing all at once if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) {
realm.where<CryptoMetadataEntity>().findFirst()?.apply { crossSigningKeysMapper.update(existingMaster, masterKey)
xSignMasterPrivateKey = null } else {
} Timber.d("## CrossSigning MSK change for $userId")
val keyEntity = crossSigningKeysMapper.map(masterKey)
signingInfo.setMasterKey(keyEntity)
if (userId == this.userId) {
shouldResetMyDevicesLocalTrust = true
// my msk has changed! clear my private key
// Could we have some race here? e.g I am the one that did change the keys
// could i get this update to early and clear the private keys?
// -> initializeCrossSigning is guarding for that by storing all at once
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = null
} }
} }
val existingSelfSigned = signingInfo.getSelfSignedKey()
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
} else {
Timber.d("## CrossSigning SSK change for $userId")
val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
signingInfo.setSelfSignedKey(keyEntity)
if (userId == this.userId) {
shouldResetMyDevicesLocalTrust = true
// my ssk has changed! clear my private key
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignSelfSignedPrivateKey = null
}
}
}
// Only for me
if (userSigningKey != null) {
val existingUSK = signingInfo.getUserSigningKey()
if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
crossSigningKeysMapper.update(existingUSK, userSigningKey)
} else {
Timber.d("## CrossSigning USK change for $userId")
val keyEntity = crossSigningKeysMapper.map(userSigningKey)
signingInfo.setUserSignedKey(keyEntity)
if (userId == this.userId) {
shouldResetMyDevicesLocalTrust = true
// my usk has changed! clear my private key
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = null
}
}
}
}
// When my cross signing keys are reset, we consider clearing all existing device trust
if (shouldResetMyDevicesLocalTrust) {
realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, this.userId)
.findFirst()
?.devices?.forEach {
it?.trustLevelEntity?.crossSignedVerified = false
it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
}
}
userEntity.crossSigningInfoEntity = signingInfo
} }
val existingSelfSigned = signingInfo.getSelfSignedKey()
if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) {
crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey)
} else {
Timber.d("## CrossSigning SSK change for $userId")
val keyEntity = crossSigningKeysMapper.map(selfSigningKey)
signingInfo.setSelfSignedKey(keyEntity)
if (userId == this.userId) {
shouldResetMyDevicesLocalTrust = true
// my ssk has changed! clear my private key
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignSelfSignedPrivateKey = null
}
}
}
// Only for me
if (userSigningKey != null) {
val existingUSK = signingInfo.getUserSigningKey()
if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) {
crossSigningKeysMapper.update(existingUSK, userSigningKey)
} else {
Timber.d("## CrossSigning USK change for $userId")
val keyEntity = crossSigningKeysMapper.map(userSigningKey)
signingInfo.setUserSignedKey(keyEntity)
if (userId == this.userId) {
shouldResetMyDevicesLocalTrust = true
// my usk has changed! clear my private key
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = null
}
}
}
}
// When my cross signing keys are reset, we consider clearing all existing device trust
if (shouldResetMyDevicesLocalTrust) {
realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, this.userId)
.findFirst()
?.devices?.forEach {
it?.trustLevelEntity?.crossSignedVerified = false
it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId
}
}
userEntity.crossSigningInfoEntity = signingInfo
} }
} }
} }
} }
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
@ -1831,13 +1846,24 @@ internal class RealmCryptoStore @Inject constructor(
} }
doRealmTransaction("onSyncCompleted", realmConfiguration) { realm -> doRealmTransaction("onSyncCompleted", realmConfiguration) { realm ->
// setShouldShareHistory // setShouldShareHistory
aggregator.setShouldShareHistoryData.map { aggregator.setShouldShareHistoryData.forEach {
CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value
} }
// setShouldEncryptForInvitedMembers // setShouldEncryptForInvitedMembers
aggregator.setShouldEncryptForInvitedMembersData.map { aggregator.setShouldEncryptForInvitedMembersData.forEach {
CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value
} }
} }
} }
override fun storeUserDataToStore(userDataToStore: UserDataToStore) {
doRealmTransaction("storeUserDataToStore", realmConfiguration) { realm ->
userDataToStore.userDevices.forEach {
storeUserDevices(realm, it.key, it.value)
}
userDataToStore.userCrossSigningKeys.forEach {
storeUserCrossSigningKeys(realm, it.key, it.value.first, it.value.second, it.value.third)
}
}
}
} }