diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt index 4004294d97..5e7744853a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt @@ -18,8 +18,6 @@ package org.matrix.android.sdk.internal.crypto.model import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity data class CryptoDeviceInfo( val deviceId: String, @@ -77,7 +75,3 @@ data class CryptoDeviceInfo( internal fun CryptoDeviceInfo.toRest(): DeviceKeys { return CryptoInfoMapper.map(this) } - -internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity { - return CryptoMapper.mapToEntity(this) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 9266f8fe7d..80d00c3dde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -51,7 +51,6 @@ import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.model.toEntity import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo @@ -280,24 +279,34 @@ internal class RealmCryptoStore @Inject constructor( override fun storeUserDevices(userId: String, devices: Map?) { doRealmTransaction(realmConfiguration) { realm -> if (devices == null) { + Timber.d("Remove user $userId") // Remove the user UserEntity.delete(realm, userId) } else { - UserEntity.getOrCreate(realm, userId) - .let { u -> - // Add the devices - val currentKnownDevices = u.devices.toList() - val new = devices.map { entry -> entry.value.toEntity() } - new.forEach { entity -> - // Maintain first time seen - val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey } - entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis() - realm.insertOrUpdate(entity) - } - // Ensure all other devices are deleted - u.devices.clearWith { it.deleteOnCascade() } - u.devices.addAll(new) - } + val userEntity = UserEntity.getOrCreate(realm, userId) + // First delete the removed devices + val deviceIds = devices.keys + userEntity.devices.iterator().forEach { deviceInfoEntity -> + 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 -> + val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } + if (existingDeviceInfoEntity == null) { + // Add the device + Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") + val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) + newEntity.firstTimeSeenLocalTs = System.currentTimeMillis() + userEntity.devices.add(newEntity) + } else { + // Update the device + Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") + CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) + } + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 8e77f5b823..2846be9932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -55,7 +55,7 @@ internal object RealmCryptoStoreMigration : RealmMigration { // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - const val CRYPTO_STORE_SCHEMA_VERSION = 12L + const val CRYPTO_STORE_SCHEMA_VERSION = 13L private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema { if (!hasField(fieldName)) { @@ -93,6 +93,7 @@ internal object RealmCryptoStoreMigration : RealmMigration { if (oldVersion <= 9) migrateTo10(realm) if (oldVersion <= 10) migrateTo11(realm) if (oldVersion <= 11) migrateTo12(realm) + if (oldVersion <= 12) migrateTo13(realm) } private fun migrateTo1Legacy(realm: DynamicRealm) { @@ -497,4 +498,60 @@ internal object RealmCryptoStoreMigration : RealmMigration { realm.schema.get("CryptoRoomEntity") ?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema) } + + // Version 13L delete unreferenced TrustLevelEntity + private fun migrateTo13(realm: DynamicRealm) { + Timber.d("Step 12 -> 13") + + // Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366 + val trustLevelEntitySchema = realm.schema.get("TrustLevelEntity") + + /* + Creating a new temp field called isLinked which is set to true for those which are + references by other objects. Rest of them are set to false. Then removing all + those which are false and hence duplicate and unnecessary. Then removing the temp field + isLinked + */ + var mainCounter = 0 + var deviceInfoCounter = 0 + var keyInfoCounter = 0 + val deleteCounter: Int + + trustLevelEntitySchema + ?.addField("isLinked", Boolean::class.java) + ?.transform { obj -> + // Setting to false for all by default + obj.set("isLinked", false) + mainCounter++ + } + + realm.schema.get("DeviceInfoEntity")?.transform { obj -> + // Setting to true for those which are referenced in DeviceInfoEntity + deviceInfoCounter++ + obj.getObject("trustLevelEntity")?.set("isLinked", true) + } + + realm.schema.get("KeyInfoEntity")?.transform { obj -> + // Setting to true for those which are referenced in KeyInfoEntity + keyInfoCounter++ + obj.getObject("trustLevelEntity")?.set("isLinked", true) + } + + // Removing all those which are set as false + realm.where("TrustLevelEntity") + .equalTo("isLinked", false) + .findAll() + .also { deleteCounter = it.size } + .deleteAllFromRealm() + + trustLevelEntitySchema?.removeField("isLinked") + + Timber.w("TrustLevelEntity cleanup: $mainCounter entities") + Timber.w("TrustLevelEntity cleanup: $deviceInfoCounter entities referenced in DeviceInfoEntities") + Timber.w("TrustLevelEntity cleanup: $keyInfoCounter entities referenced in KeyInfoEntity") + Timber.w("TrustLevelEntity cleanup: $deleteCounter entities deleted!") + if (mainCounter != deviceInfoCounter + keyInfoCounter + deleteCounter) { + Timber.e("TrustLevelEntity cleanup: Something is not correct...") + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt index 37d1441690..7ba986699a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt @@ -44,23 +44,32 @@ object CryptoMapper { )) internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity { - return DeviceInfoEntity( - primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId), - userId = deviceInfo.userId, - deviceId = deviceInfo.deviceId, - algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms), - keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys), - signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures), - isBlocked = deviceInfo.isBlocked, - trustLevelEntity = deviceInfo.trustLevel?.let { - TrustLevelEntity( - crossSignedVerified = it.crossSigningVerified, - locallyVerified = it.locallyVerified - ) - }, - // We store the device name if present now - unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName - ) + return DeviceInfoEntity(primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId)) + .also { updateDeviceInfoEntity(it, deviceInfo) } + } + + internal fun updateDeviceInfoEntity(entity: DeviceInfoEntity, deviceInfo: CryptoDeviceInfo) { + entity.userId = deviceInfo.userId + entity.deviceId = deviceInfo.deviceId + entity.algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms) + entity.keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys) + entity.signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures) + entity.isBlocked = deviceInfo.isBlocked + val deviceInfoTrustLevel = deviceInfo.trustLevel + if (deviceInfoTrustLevel == null) { + entity.trustLevelEntity?.deleteFromRealm() + entity.trustLevelEntity = null + } else { + if (entity.trustLevelEntity == null) { + // Create a new TrustLevelEntity object + entity.trustLevelEntity = TrustLevelEntity() + } + // Update the existing TrustLevelEntity object + entity.trustLevelEntity?.crossSignedVerified = deviceInfoTrustLevel.crossSigningVerified + entity.trustLevelEntity?.locallyVerified = deviceInfoTrustLevel.locallyVerified + } + // We store the device name if present now + entity.unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName } internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo {