From fc3943bcba42770ab52141ab53929f88d6977274 Mon Sep 17 00:00:00 2001 From: yostyle Date: Fri, 27 Sep 2024 08:44:11 +0200 Subject: [PATCH] Fix Element FS --- matrix-sdk-android/build.gradle | 3 + .../sdk/internal/crypto/RustCryptoService.kt | 24 ++++ .../model/MXInboundMegolmSessionWrapper.kt | 62 ++++++++++ .../crypto/store/IMXCommonCryptoStore.kt | 8 ++ .../internal/crypto/store/RustCryptoStore.kt | 14 +++ .../sdk/internal/crypto/store/db/Helper.kt | 40 +++++++ .../crypto/store/db/SafeObjectInputStream.kt | 44 +++++++ .../store/db/migration/MigrateCryptoTo024.kt | 1 - .../db/model/OlmInboundGroupSessionEntity.kt | 113 ++++++++++++++++++ 9 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 89f083f487..eae670eab6 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -206,6 +206,9 @@ dependencies { // Work implementation libs.androidx.work + // olm lib is now hosted in MavenCentral + implementation 'org.matrix.android:olm-sdk:3.2.12' + // DI implementation libs.dagger.dagger kapt libs.dagger.daggerCompiler diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index c998f104f4..1f4809cd00 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -84,6 +84,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId import org.matrix.android.sdk.internal.crypto.verification.RustVerificationService import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.StreamEventsManager @@ -693,6 +694,29 @@ internal class RustCryptoService @Inject constructor( return olmMachine.exportKeys(password, iterationCount) } + /** + * Migrate the crypto keys from realm to rust crypto database. + * + * @param password the password + * @param anIterationCount the encryption iteration count (0 means no encryption) + */ + private suspend fun migrateRoomKeys(password: String, anIterationCount: Int) { + return withContext(coroutineDispatchers.crypto) { + val iterationCount = max(0, anIterationCount) + + val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } + + val adapter = MoshiProvider.providesMoshi() + .adapter(List::class.java) + + // Export keys from realm + val roomKeys = MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) + + // Import keys to rust crypto database + importRoomKeys(roomKeysAsArray = roomKeys, password = password, progressListener = null) + } + } + override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt new file mode 100644 index 0000000000..2b67ab7376 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2022 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.model + +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber + +data class MXInboundMegolmSessionWrapper( + // olm object + val session: OlmInboundGroupSession, + // data about the session + val sessionData: InboundGroupSessionData +) { + // shortcut + val roomId = sessionData.roomId + val senderKey = sessionData.senderKey + val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() } + + /** + * Export the inbound group session keys. + * @param index the index to export. If null, the first known index will be used + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + internal fun exportKeys(index: Long? = null): MegolmSessionData? { + return try { + val keysClaimed = sessionData.keysClaimed ?: return null + val wantedIndex = index ?: session.firstKnownIndex + + MegolmSessionData( + senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"), + forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(), + sessionKey = session.export(wantedIndex), + senderClaimedKeys = keysClaimed, + roomId = sessionData.roomId, + sessionId = session.sessionIdentifier(), + senderKey = senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sharedHistory = sessionData.sharedHistory + ) + } catch (e: Exception) { + Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed") + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt index ca389c9b00..eb58de74c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator /** @@ -34,6 +35,13 @@ import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator */ interface IMXCommonCryptoStore { + /** + * Retrieve the known inbound group sessions. + * + * @return the list of all known group sessions, to export them. + */ + fun getInboundGroupSessions(): List + /** * Provides the algorithm used in a dedicated room. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt index 93d4963c91..f3d8545a31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventCo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransaction import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransactionAsync @@ -46,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.di.CryptoDatabase @@ -150,6 +152,18 @@ internal class RustCryptoStore @Inject constructor( } } + /** + * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, + * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management. + */ + override fun getInboundGroupSessions(): List { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .findAll() + .mapNotNull { it.toModel() } + } + } + override fun getRoomAlgorithm(roomId: String): String? { return doWithRealm(realmConfiguration) { CryptoRoomEntity.getById(it, roomId)?.algorithm diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt index fb10ecc999..2565b6c27e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -16,9 +16,14 @@ package org.matrix.android.sdk.internal.crypto.store.db +import android.util.Base64 import io.realm.Realm import io.realm.RealmConfiguration import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.io.ObjectOutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream import kotlin.system.measureTimeMillis /** @@ -46,3 +51,38 @@ internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, act realm.executeTransactionAsync { action.invoke(it) } } } + +/** + * Serialize any Serializable object, zip it and convert to Base64 String. + */ +internal fun serializeForRealm(o: Any?): String? { + if (o == null) { + return null + } + + val baos = ByteArrayOutputStream() + val gzis = GZIPOutputStream(baos) + val out = ObjectOutputStream(gzis) + out.use { + it.writeObject(o) + } + return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) +} + +/** + * Do the opposite of serializeForRealm. + */ +@Suppress("UNCHECKED_CAST") +internal fun deserializeFromRealm(string: String?): T? { + if (string == null) { + return null + } + val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) + + val bais = decodedB64.inputStream() + val gzis = GZIPInputStream(bais) + val ois = SafeObjectInputStream(gzis) + return ois.use { + it.readObject() as T + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt new file mode 100644 index 0000000000..5897869a97 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.db + +import java.io.IOException +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectStreamClass + +/** + * Package has been renamed from `im.vector.matrix.android` to `org.matrix.android.sdk` + * so ensure deserialization of previously stored objects still works + * + * Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes + */ +internal class SafeObjectInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) { + + init { + enableResolveObject(true) + } + + @Throws(IOException::class, ClassNotFoundException::class) + override fun readClassDescriptor(): ObjectStreamClass { + val read = super.readClassDescriptor() + if (read.name.startsWith("im.vector.matrix.android.")) { + return ObjectStreamClass.lookup(Class.forName(read.name.replace("im.vector.matrix.android.", "org.matrix.android.sdk."))) + } + return read + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo024.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo024.kt index 9a87eb4741..81fa3ed6c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo024.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo024.kt @@ -38,7 +38,6 @@ internal class MigrateCryptoTo024(realm: DynamicRealm) : RealmMigrator(realm, 24 remove("KeyInfoEntity") remove("TrustLevelEntity") remove("KeysBackupDataEntity") - remove("OlmInboundGroupSessionEntity") remove("OlmSessionEntity") remove("AuditTrailEntity") remove("OutgoingKeyRequestEntity") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt new file mode 100644 index 0000000000..62ab73e379 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2020 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.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber + +internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" + +internal open class OlmInboundGroupSessionEntity( + // Combined value to build a primary key + @PrimaryKey var primaryKey: String? = null, + + // denormalization for faster querying (these fields are in the inboundGroupSessionDataJson) + var sessionId: String? = null, + var senderKey: String? = null, + var roomId: String? = null, + + // Deprecated, used for migration / olmInboundGroupSessionData contains Json + // keep it in case of problem to have a chance to recover + var olmInboundGroupSessionData: String? = null, + + // Stores the session data in an extensible format + // to allow to store data not yet supported for later use + var inboundGroupSessionDataJson: String? = null, + + // The pickled session + var serializedOlmInboundGroupSession: String? = null, + + // Flag that indicates whether or not the current inboundSession will be shared to + // invited users to decrypt past messages + var sharedHistory: Boolean = false, + // Indicate if the key has been backed up to the homeserver + var backedUp: Boolean = false +) : + RealmObject() { + + fun store(wrapper: MXInboundMegolmSessionWrapper) { + this.serializedOlmInboundGroupSession = serializeForRealm(wrapper.session) + this.inboundGroupSessionDataJson = adapter.toJson(wrapper.sessionData) + this.roomId = wrapper.sessionData.roomId + this.senderKey = wrapper.sessionData.senderKey + this.sessionId = wrapper.session.sessionIdentifier() + this.sharedHistory = wrapper.sessionData.sharedHistory + } +// fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? { +// return try { +// deserializeFromRealm(olmInboundGroupSessionData) +// } catch (failure: Throwable) { +// Timber.e(failure, "## Deserialization failure") +// return null +// } +// } +// +// fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) { +// olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper) +// } + + fun getOlmGroupSession(): OlmInboundGroupSession? { + return try { + deserializeFromRealm(serializedOlmInboundGroupSession) + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } + } + + fun getData(): InboundGroupSessionData? { + return try { + inboundGroupSessionDataJson?.let { + adapter.fromJson(it) + } + } catch (failure: Throwable) { + Timber.e(failure, "## Deserialization failure") + return null + } + } + + fun toModel(): MXInboundMegolmSessionWrapper? { + val data = getData() ?: return null + val session = getOlmGroupSession() ?: return null + return MXInboundMegolmSessionWrapper( + session = session, + sessionData = data + ) + } + + companion object { + private val adapter = MoshiProvider.providesMoshi() + .adapter(InboundGroupSessionData::class.java) + } +}