diff --git a/matrix-sdk-android/proguard-rules.pro b/matrix-sdk-android/proguard-rules.pro index 3cb49420b8..08a20cbf0a 100644 --- a/matrix-sdk-android/proguard-rules.pro +++ b/matrix-sdk-android/proguard-rules.pro @@ -60,4 +60,7 @@ -keep interface okhttp3.Interceptor.* { *; } ### OLM JNI ### --keep class org.matrix.olm.** { *; } \ No newline at end of file +-keep class org.matrix.olm.** { *; } + +### Webrtc +-keep class org.webrtc.** { *; } diff --git a/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/database/RealmDebugTools.kt b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/database/RealmDebugTools.kt new file mode 100644 index 0000000000..2d9dcda1d4 --- /dev/null +++ b/matrix-sdk-android/src/debug/java/im/vector/matrix/android/internal/database/RealmDebugTools.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.database + +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import timber.log.Timber + +object RealmDebugTools { + /** + * Log info about the crypto DB + */ + fun dumpCryptoDb(realmConfiguration: RealmConfiguration) { + Realm.getInstance(realmConfiguration).use { + Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}") + + val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) } + Timber.d("Realm encryption key : $key") + + // Check if we have data + Timber.e("Realm is empty: ${it.isEmpty}") + + Timber.d("Realm has CryptoMetadataEntity: ${it.where().count()}") + Timber.d("Realm has CryptoRoomEntity: ${it.where().count()}") + Timber.d("Realm has DeviceInfoEntity: ${it.where().count()}") + Timber.d("Realm has KeysBackupDataEntity: ${it.where().count()}") + Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where().count()}") + Timber.d("Realm has OlmSessionEntity: ${it.where().count()}") + Timber.d("Realm has UserEntity: ${it.where().count()}") + Timber.d("Realm has KeyInfoEntity: ${it.where().count()}") + Timber.d("Realm has CrossSigningInfoEntity: ${it.where().count()}") + Timber.d("Realm has TrustLevelEntity: ${it.where().count()}") + Timber.d("Realm has GossipingEventEntity: ${it.where().count()}") + Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where().count()}") + Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where().count()}") + Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where().count()}") + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 1a4c4aceee..12bfb0bb8e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.legacy.LegacySessionImporter import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments @@ -41,6 +42,7 @@ import javax.inject.Inject */ class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) { + @Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var userAgentHolder: UserAgentHolder @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @@ -62,6 +64,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo return authenticationService } + fun legacySessionImporter(): LegacySessionImporter { + return legacySessionImporter + } + companion object { private lateinit var instance: Matrix diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt index 125a7dfc1b..ef4c492bf8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt @@ -34,10 +34,10 @@ data class HomeServerConnectionConfig( val homeServerUri: Uri, val identityServerUri: Uri? = null, val antiVirusServerUri: Uri? = null, - val allowedFingerprints: MutableList = ArrayList(), + val allowedFingerprints: List = emptyList(), val shouldPin: Boolean = false, - val tlsVersions: MutableList? = null, - val tlsCipherSuites: MutableList? = null, + val tlsVersions: List? = null, + val tlsCipherSuites: List? = null, val shouldAcceptTlsExtensions: Boolean = true, val allowHttpExtension: Boolean = false, val forceUsageTlsVersions: Boolean = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/legacy/LegacySessionImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/legacy/LegacySessionImporter.kt new file mode 100644 index 0000000000..533f387ec8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/legacy/LegacySessionImporter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.api.legacy + +interface LegacySessionImporter { + + /** + * Will eventually import a session created by the legacy app. + */ + fun process() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/CallHangupContent.kt index 1e50bc247e..1cb379b508 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/CallHangupContent.kt @@ -40,6 +40,7 @@ data class CallHangupContent( */ @Json(name = "reason") val reason: Reason? = null ) { + @JsonClass(generateAdapter = false) enum class Reason { @Json(name = "ice_failed") ICE_FAILED, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/SdpType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/SdpType.kt index 17c6d9a89f..799792c8a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/SdpType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/call/SdpType.kt @@ -17,7 +17,9 @@ package im.vector.matrix.android.api.session.room.model.call import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class SdpType { @Json(name = "offer") OFFER, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 68f404cb71..688023b55e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -21,6 +21,7 @@ import dagger.Binds import dagger.Module import dagger.Provides import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.legacy.LegacySessionImporter import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore @@ -29,6 +30,7 @@ import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase +import im.vector.matrix.android.internal.legacy.DefaultLegacySessionImporter import im.vector.matrix.android.internal.wellknown.WellknownModule import io.realm.RealmConfiguration import java.io.File @@ -61,6 +63,9 @@ internal abstract class AuthModule { } } + @Binds + abstract fun bindLegacySessionImporter(importer: DefaultLegacySessionImporter): LegacySessionImporter + @Binds abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 66ee0c58f5..6a21355743 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -26,44 +26,182 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.di.SerializeNulls import io.realm.DynamicRealm import io.realm.RealmMigration +import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 import timber.log.Timber import javax.inject.Inject +import org.matrix.androidsdk.crypto.data.MXDeviceInfo as LegacyMXDeviceInfo internal class RealmCryptoStoreMigration @Inject constructor(private val crossSigningKeysMapper: CrossSigningKeysMapper) : RealmMigration { - // Version 1L added Cross Signing info persistence companion object { - const val CRYPTO_STORE_SCHEMA_VERSION = 6L + // 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 = 9L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") - if (oldVersion <= 0) migrateTo1(realm) - if (oldVersion <= 1) migrateTo2(realm) - if (oldVersion <= 2) migrateTo3(realm) + if (oldVersion <= 0) migrateTo1Legacy(realm) + if (oldVersion <= 1) migrateTo2Legacy(realm) + if (oldVersion <= 2) migrateTo3RiotX(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 5) migrateTo6(realm) + if (oldVersion <= 6) migrateTo7(realm) + if (oldVersion <= 7) migrateTo8(realm) + if (oldVersion <= 8) migrateTo9(realm) } - private fun migrateTo1(realm: DynamicRealm) { + private fun migrateTo1Legacy(realm: DynamicRealm) { Timber.d("Step 0 -> 1") + Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0") + + realm.schema.get("OlmSessionEntity") + ?.addField(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Long::class.java) + ?.transform { + it.setLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, 0) + } + } + + private fun migrateTo2Legacy(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("IncomingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") + + realm.schema.get("OutgoingRoomKeyRequestEntity") + ?.addField("requestBodyAlgorithm", String::class.java) + ?.addField("requestBodyRoomId", String::class.java) + ?.addField("requestBodySenderKey", String::class.java) + ?.addField("requestBodySessionId", String::class.java) + ?.transform { dynamicObject -> + val requestBodyString = dynamicObject.getString("requestBodyString") + try { + // It was a map before + val map: Map? = deserializeFromRealm(requestBodyString) + + map?.let { + dynamicObject.setString("requestBodyAlgorithm", it["algorithm"]) + dynamicObject.setString("requestBodyRoomId", it["room_id"]) + dynamicObject.setString("requestBodySenderKey", it["sender_key"]) + dynamicObject.setString("requestBodySessionId", it["session_id"]) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + ?.removeField("requestBodyString") + + Timber.d("Create KeysBackupDataEntity") + + realm.schema.create("KeysBackupDataEntity") + .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) + .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) + .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) + } + + private fun migrateTo3RiotX(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Migrate to RiotX model") + + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) + ?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) + + // Convert format of MXDeviceInfo, package has to be the same. + realm.schema.get("DeviceInfoEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm(oldSerializedData)?.let { legacyMxDeviceInfo -> + val newMxDeviceInfo = MXDeviceInfo( + deviceId = legacyMxDeviceInfo.deviceId, + userId = legacyMxDeviceInfo.userId, + algorithms = legacyMxDeviceInfo.algorithms, + keys = legacyMxDeviceInfo.keys, + signatures = legacyMxDeviceInfo.signatures, + unsigned = legacyMxDeviceInfo.unsigned, + verified = legacyMxDeviceInfo.mVerified + ) + + obj.setString("deviceInfoData", serializeForRealm(newMxDeviceInfo)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + + // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper2 + realm.schema.get("OlmInboundGroupSessionEntity") + ?.transform { obj -> + try { + val oldSerializedData = obj.getString("olmInboundGroupSessionData") + deserializeFromRealm(oldSerializedData)?.let { mxOlmInboundGroupSession2 -> + val newOlmInboundGroupSessionWrapper2 = OlmInboundGroupSessionWrapper2() + .apply { + olmInboundGroupSession = mxOlmInboundGroupSession2.mSession + roomId = mxOlmInboundGroupSession2.mRoomId + senderKey = mxOlmInboundGroupSession2.mSenderKey + keysClaimed = mxOlmInboundGroupSession2.mKeysClaimed + forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain + } + + obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper2)) + } + } catch (e: Exception) { + Timber.e(e, "Error") + } + } + } + + // Version 4L added Cross Signing info persistence + private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") Timber.d("Create KeyInfoEntity") - val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") + val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity") .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) @@ -73,7 +211,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) - .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) Timber.d("Create CrossSigningInfoEntity") @@ -112,7 +250,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) - ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelEntityEntitySchema) ?.transform { obj -> try { @@ -158,8 +296,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi ?.removeField("deviceInfoData") } - private fun migrateTo2(realm: DynamicRealm) { - Timber.d("Step 1 -> 2") + private fun migrateTo5(realm: DynamicRealm) { + Timber.d("Step 4 -> 5") realm.schema.remove("OutgoingRoomKeyRequestEntity") realm.schema.remove("IncomingRoomKeyRequestEntity") @@ -199,16 +337,16 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) } - private fun migrateTo3(realm: DynamicRealm) { - Timber.d("Step 2 -> 3") + private fun migrateTo6(realm: DynamicRealm) { + Timber.d("Step 5 -> 6") Timber.d("Updating CryptoMetadataEntity table") realm.schema.get("CryptoMetadataEntity") ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java) ?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY_VERSION, String::class.java) } - private fun migrateTo4(realm: DynamicRealm) { - Timber.d("Step 3 -> 4") + private fun migrateTo7(realm: DynamicRealm) { + Timber.d("Step 6 -> 7") Timber.d("Updating KeyInfoEntity table") val keyInfoEntities = realm.where("KeyInfoEntity").findAll() try { @@ -239,8 +377,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } - private fun migrateTo5(realm: DynamicRealm) { - Timber.d("Step 4 -> 5") + private fun migrateTo8(realm: DynamicRealm) { + Timber.d("Step 7 -> 8") realm.schema.create("MyDeviceLastSeenInfoEntity") .addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java) .addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID) @@ -261,7 +399,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } // Fixes duplicate devices in UserEntity#devices - private fun migrateTo6(realm: DynamicRealm) { + private fun migrateTo9(realm: DynamicRealm) { + Timber.d("Step 8 -> 9") val userEntities = realm.where("UserEntity").findAll() userEntities.forEach { try { @@ -273,7 +412,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi deviceList.addAll(distinct) } } catch (failure: Throwable) { - Timber.w(failure, "Crypto Data base migration error for migrateTo6") + Timber.w(failure, "Crypto Data base migration error for migrateTo9") } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index 71973b1193..5dac5d9f86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -86,6 +86,13 @@ internal class RealmKeysUtils @Inject constructor(context: Context, } fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) { + val key = getRealmEncryptionKey(alias) + + realmConfigurationBuilder.encryptionKey(key) + } + + // Expose to handle Realm migration to riotX + fun getRealmEncryptionKey(alias: String) : ByteArray { val key = if (hasKeyForDatabase(alias)) { Timber.i("Found key for alias:$alias") extractKeyForDatabase(alias) @@ -99,7 +106,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context, Timber.w("Database key for alias `$alias`: $log") } - realmConfigurationBuilder.encryptionKey(key) + return key } // Delete elements related to the alias diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/DefaultLegacySessionImporter.kt new file mode 100644 index 0000000000..568f9a521a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/DefaultLegacySessionImporter.kt @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy + +import android.content.Context +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.DiscoveryInformation +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.auth.data.WellKnownBaseConfig +import im.vector.matrix.android.api.legacy.LegacySessionImporter +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule +import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.legacy.riot.LoginStorage +import im.vector.matrix.android.internal.network.ssl.Fingerprint +import im.vector.matrix.android.internal.util.md5 +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import im.vector.matrix.android.internal.legacy.riot.Fingerprint as LegacyFingerprint +import im.vector.matrix.android.internal.legacy.riot.HomeServerConnectionConfig as LegacyHomeServerConnectionConfig + +internal class DefaultLegacySessionImporter @Inject constructor( + private val context: Context, + private val sessionParamsStore: SessionParamsStore, + private val realmCryptoStoreMigration: RealmCryptoStoreMigration, + private val realmKeysUtils: RealmKeysUtils +) : LegacySessionImporter { + + private val loginStorage = LoginStorage(context) + + companion object { + // During development, set to false to play several times the migration + private var DELETE_PREVIOUS_DATA = true + } + + override fun process() { + Timber.d("Migration: Importing legacy session") + + val list = loginStorage.credentialsList + + Timber.d("Migration: found ${list.size} session(s).") + + val legacyConfig = list.firstOrNull() ?: return + + runBlocking { + Timber.d("Migration: importing a session") + try { + importCredentials(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing credential") + } + + Timber.d("Migration: importing crypto DB") + try { + importCryptoDb(legacyConfig) + } catch (t: Throwable) { + // It can happen in case of partial migration. To test, do not return + Timber.e(t, "Migration: Error importing crypto DB") + } + + if (DELETE_PREVIOUS_DATA) { + try { + Timber.d("Migration: clear file system") + clearFileSystem(legacyConfig) + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing filesystem") + } + try { + Timber.d("Migration: clear shared prefs") + clearSharedPrefs() + } catch (t: Throwable) { + Timber.e(t, "Migration: Error clearing shared prefs") + } + } else { + Timber.d("Migration: clear file system - DEACTIVATED") + Timber.d("Migration: clear shared prefs - DEACTIVATED") + } + } + } + + private suspend fun importCredentials(legacyConfig: LegacyHomeServerConnectionConfig) { + @Suppress("DEPRECATION") + val sessionParams = SessionParams( + credentials = Credentials( + userId = legacyConfig.credentials.userId, + accessToken = legacyConfig.credentials.accessToken, + refreshToken = legacyConfig.credentials.refreshToken, + homeServer = legacyConfig.credentials.homeServer, + deviceId = legacyConfig.credentials.deviceId, + discoveryInformation = legacyConfig.credentials.wellKnown?.let { wellKnown -> + // Note credentials.wellKnown is not serialized in the LoginStorage, so this code is a bit useless... + if (wellKnown.homeServer?.baseURL != null || wellKnown.identityServer?.baseURL != null) { + DiscoveryInformation( + homeServer = wellKnown.homeServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) }, + identityServer = wellKnown.identityServer?.baseURL?.let { WellKnownBaseConfig(baseURL = it) } + ) + } else { + null + } + } + ), + homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = legacyConfig.homeserverUri, + identityServerUri = legacyConfig.identityServerUri, + antiVirusServerUri = legacyConfig.antiVirusServerUri, + allowedFingerprints = legacyConfig.allowedFingerprints.map { + Fingerprint( + bytes = it.bytes, + hashType = when (it.type) { + LegacyFingerprint.HashType.SHA1, + null -> Fingerprint.HashType.SHA1 + LegacyFingerprint.HashType.SHA256 -> Fingerprint.HashType.SHA256 + } + ) + }, + shouldPin = legacyConfig.shouldPin(), + tlsVersions = legacyConfig.acceptedTlsVersions, + tlsCipherSuites = legacyConfig.acceptedTlsCipherSuites, + shouldAcceptTlsExtensions = legacyConfig.shouldAcceptTlsExtensions(), + allowHttpExtension = false, // TODO + forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions() + ), + // If token is not valid, this boolean will be updated later + isTokenValid = true + ) + + Timber.d("Migration: save session") + sessionParamsStore.save(sessionParams) + } + + private fun importCryptoDb(legacyConfig: LegacyHomeServerConnectionConfig) { + // Here we migrate the DB, we copy the crypto DB to the location specific to RiotX, and we encrypt it. + val userMd5 = legacyConfig.credentials.userId.md5() + + val sessionId = legacyConfig.credentials.let { (if (it.deviceId.isNullOrBlank()) it.userId else "${it.userId}|${it.deviceId}").md5() } + val newLocation = File(context.filesDir, sessionId) + + val keyAlias = "crypto_module_$userMd5" + + // Ensure newLocation does not exist (can happen in case of partial migration) + newLocation.deleteRecursively() + newLocation.mkdirs() + + Timber.d("Migration: create legacy realm configuration") + + val realmConfiguration = RealmConfiguration.Builder() + .directory(File(context.filesDir, userMd5)) + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(realmCryptoStoreMigration) + .build() + + Timber.d("Migration: copy DB to encrypted DB") + Realm.getInstance(realmConfiguration).use { + // Move the DB to the new location, handled by RiotX + it.writeEncryptedCopyTo(File(newLocation, realmConfiguration.realmFileName), realmKeysUtils.getRealmEncryptionKey(keyAlias)) + } + } + + // Delete all the files created by Riot Android which will not be used anymore by RiotX + private fun clearFileSystem(legacyConfig: LegacyHomeServerConnectionConfig) { + val cryptoFolder = legacyConfig.credentials.userId.md5() + + listOf( + // Where session store was saved (we do not care about migrating that, an initial sync will be performed) + File(context.filesDir, "MXFileStore"), + // Previous (and very old) file crypto store + File(context.filesDir, "MXFileCryptoStore"), + // Draft. They will be lost, this is sad but we assume it + File(context.filesDir, "MXLatestMessagesStore"), + // Media storage + File(context.filesDir, "MXMediaStore"), + File(context.filesDir, "MXMediaStore2"), + File(context.filesDir, "MXMediaStore3"), + // Ext folder + File(context.filesDir, "ext_share"), + // Crypto store + File(context.filesDir, cryptoFolder) + ).forEach { file -> + try { + file.deleteRecursively() + } catch (t: Throwable) { + Timber.e(t, "Migration: unable to delete $file") + } + } + } + + private fun clearSharedPrefs() { + // Shared Pref. Note that we do not delete the default preferences, as it should be nearly the same (TODO check that) + listOf( + "Vector.LoginStorage", + "GcmRegistrationManager", + "IntegrationManager.Storage" + ).forEach { prefName -> + context.getSharedPreferences(prefName, Context.MODE_PRIVATE) + .edit() + .clear() + .apply() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java new file mode 100644 index 0000000000..6c48eddad8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import android.util.Pair; + +import androidx.annotation.NonNull; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.CipherSuite; +import okhttp3.ConnectionSpec; +import okhttp3.TlsVersion; +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Various utility classes for dealing with X509Certificates + */ +public class CertUtil { + /** + * Generates the SHA-256 fingerprint of the given certificate + * + * @param cert the certificate. + * @return the finger print + * @throws CertificateException the certificate exception + */ + public static byte[] generateSha256Fingerprint(X509Certificate cert) throws CertificateException { + return generateFingerprint(cert, "SHA-256"); + } + + /** + * Generates the SHA-1 fingerprint of the given certificate + * + * @param cert the certificated + * @return the SHA1 fingerprint + * @throws CertificateException the certificate exception + */ + public static byte[] generateSha1Fingerprint(X509Certificate cert) throws CertificateException { + return generateFingerprint(cert, "SHA-1"); + } + + /** + * Generate the fingerprint for a dedicated type. + * + * @param cert the certificate + * @param type the type + * @return the fingerprint + * @throws CertificateException certificate exception + */ + private static byte[] generateFingerprint(X509Certificate cert, String type) throws CertificateException { + final byte[] fingerprint; + final MessageDigest md; + try { + md = MessageDigest.getInstance(type); + } catch (Exception e) { + // This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl. + throw new CertificateException(e); + } + + fingerprint = md.digest(cert.getEncoded()); + + return fingerprint; + } + + final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + /** + * Convert the fingerprint to an hexa string. + * + * @param fingerprint the fingerprint + * @return the hexa string. + */ + public static String fingerprintToHexString(byte[] fingerprint) { + return fingerprintToHexString(fingerprint, ' '); + } + + public static String fingerprintToHexString(byte[] fingerprint, char sep) { + char[] hexChars = new char[fingerprint.length * 3]; + for (int j = 0; j < fingerprint.length; j++) { + int v = fingerprint[j] & 0xFF; + hexChars[j * 3] = hexArray[v >>> 4]; + hexChars[j * 3 + 1] = hexArray[v & 0x0F]; + hexChars[j * 3 + 2] = sep; + } + return new String(hexChars, 0, hexChars.length - 1); + } + + /** + * Recursively checks the exception to see if it was caused by an + * UnrecognizedCertificateException + * + * @param e the throwable. + * @return The UnrecognizedCertificateException if exists, else null. + */ + public static UnrecognizedCertificateException getCertificateException(Throwable e) { + int i = 0; // Just in case there is a getCause loop + while (e != null && i < 10) { + if (e instanceof UnrecognizedCertificateException) { + return (UnrecognizedCertificateException) e; + } + e = e.getCause(); + i++; + } + + return null; + } + + /** + * Create a SSLSocket factory for a HS config. + * + * @param hsConfig the HS config. + * @return SSLSocket factory + */ + public static Pair newPinnedSSLSocketFactory(HomeServerConnectionConfig hsConfig) { + X509TrustManager defaultTrustManager = null; + + // If we haven't specified that we wanted to pin the certs, fallback to standard + // X509 checks if fingerprints don't match. + if (!hsConfig.shouldPin()) { + TrustManagerFactory trustManagerFactory = null; + + // get the PKIX instance + try { + trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); + } catch (NoSuchAlgorithmException e) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed"); + } + + // it doesn't exist, use the default one. + if (trustManagerFactory == null) { + try { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (NoSuchAlgorithmException e) { + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance with default algorithm failed"); + } + } + + if (trustManagerFactory != null) { + try { + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + + for (int i = 0; i < trustManagers.length; i++) { + if (trustManagers[i] instanceof X509TrustManager) { + defaultTrustManager = (X509TrustManager) trustManagers[i]; + break; + } + } + } catch (KeyStoreException e) { + Timber.e(e, "## newPinnedSSLSocketFactory()"); + } + } + } + + X509TrustManager trustManager = new PinnedTrustManager(hsConfig.getAllowedFingerprints(), defaultTrustManager); + + TrustManager[] trustManagers = new TrustManager[]{ + trustManager + }; + + SSLSocketFactory sslSocketFactory; + + try { + if (hsConfig.forceUsageOfTlsVersions() && hsConfig.getAcceptedTlsVersions() != null) { + // Force usage of accepted Tls Versions for Android < 20 + sslSocketFactory = new TLSSocketFactory(trustManagers, hsConfig.getAcceptedTlsVersions()); + } else { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + sslSocketFactory = sslContext.getSocketFactory(); + } + } catch (Exception e) { + // This is too fatal + throw new RuntimeException(e); + } + + return new Pair<>(sslSocketFactory, trustManager); + } + + /** + * Create a Host name verifier for a hs config. + * + * @param hsConfig the hs config. + * @return a new HostnameVerifier. + */ + public static HostnameVerifier newHostnameVerifier(HomeServerConnectionConfig hsConfig) { + final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + final List trusted_fingerprints = hsConfig.getAllowedFingerprints(); + + return new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + if (defaultVerifier.verify(hostname, session)) return true; + if (trusted_fingerprints == null || trusted_fingerprints.size() == 0) return false; + + // If remote cert matches an allowed fingerprint, just accept it. + try { + for (Certificate cert : session.getPeerCertificates()) { + for (Fingerprint allowedFingerprint : trusted_fingerprints) { + if (allowedFingerprint != null && cert instanceof X509Certificate && allowedFingerprint.matchesCert((X509Certificate) cert)) { + return true; + } + } + } + } catch (SSLPeerUnverifiedException e) { + return false; + } catch (CertificateException e) { + return false; + } + + return false; + } + }; + } + + /** + * Create a list of accepted TLS specifications for a hs config. + * + * @param hsConfig the hs config. + * @param url the url of the end point, used to check if we have to enable CLEARTEXT communication. + * @return a list of accepted TLS specifications. + */ + public static List newConnectionSpecs(@NonNull HomeServerConnectionConfig hsConfig, @NonNull String url) { + final ConnectionSpec.Builder builder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS); + + final List tlsVersions = hsConfig.getAcceptedTlsVersions(); + if (null != tlsVersions) { + builder.tlsVersions(tlsVersions.toArray(new TlsVersion[0])); + } + + final List tlsCipherSuites = hsConfig.getAcceptedTlsCipherSuites(); + if (null != tlsCipherSuites) { + builder.cipherSuites(tlsCipherSuites.toArray(new CipherSuite[0])); + } + + builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions()); + + List list = new ArrayList<>(); + + list.add(builder.build()); + + if (url.startsWith("http://")) { + list.add(ConnectionSpec.CLEARTEXT); + } + + return list; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Credentials.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Credentials.java new file mode 100644 index 0000000000..f11f3291e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Credentials.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import android.text.TextUtils; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * The user's credentials. + */ +public class Credentials { + public String userId; + + // This is the server name and not a URI, e.g. "matrix.org". Spec says it's now deprecated + @Deprecated + public String homeServer; + + public String accessToken; + + public String refreshToken; + + public String deviceId; + + // Optional data that may contain info to override home server and/or identity server + public WellKnown wellKnown; + + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("user_id", userId); + json.put("home_server", homeServer); + json.put("access_token", accessToken); + json.put("refresh_token", TextUtils.isEmpty(refreshToken) ? JSONObject.NULL : refreshToken); + json.put("device_id", deviceId); + + return json; + } + + public static Credentials fromJson(JSONObject obj) throws JSONException { + Credentials creds = new Credentials(); + creds.userId = obj.getString("user_id"); + creds.homeServer = obj.getString("home_server"); + creds.accessToken = obj.getString("access_token"); + + if (obj.has("device_id")) { + creds.deviceId = obj.getString("device_id"); + } + + // refresh_token is mandatory + if (obj.has("refresh_token")) { + try { + creds.refreshToken = obj.getString("refresh_token"); + } catch (Exception e) { + creds.refreshToken = null; + } + } else { + throw new RuntimeException("refresh_token is required."); + } + + return creds; + } + + @Override + public String toString() { + return "Credentials{" + + "userId='" + userId + '\'' + + ", homeServer='" + homeServer + '\'' + + ", refreshToken.length='" + (refreshToken != null ? refreshToken.length() : "null") + '\'' + + ", accessToken.length='" + (accessToken != null ? accessToken.length() : "null") + '\'' + + '}'; + } + + @Nullable + public String getUserId() { + return userId; + } + + @Nullable + public String getHomeServer() { + return homeServer; + } + + @Nullable + public String getAccessToken() { + return accessToken; + } + + @Nullable + public String getDeviceId() { + return deviceId; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java new file mode 100644 index 0000000000..2bfe7d6e32 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents a X509 Certificate fingerprint. + */ +public class Fingerprint { + public enum HashType { + SHA1, + SHA256 + } + + private final HashType mHashType; + private final byte[] mBytes; + private String mDisplayableHexRepr; + + public Fingerprint(HashType hashType, byte[] bytes) { + mHashType = hashType; + mBytes = bytes; + mDisplayableHexRepr = null; + } + + public static Fingerprint newSha256Fingerprint(X509Certificate cert) throws CertificateException { + return new Fingerprint(HashType.SHA256, CertUtil.generateSha256Fingerprint(cert)); + } + + public static Fingerprint newSha1Fingerprint(X509Certificate cert) throws CertificateException { + return new Fingerprint(HashType.SHA1, CertUtil.generateSha1Fingerprint(cert)); + } + + public HashType getType() { + return mHashType; + } + + public byte[] getBytes() { + return mBytes; + } + + public String getBytesAsHexString() { + if (mDisplayableHexRepr == null) { + mDisplayableHexRepr = CertUtil.fingerprintToHexString(mBytes); + } + + return mDisplayableHexRepr; + } + + public JSONObject toJson() throws JSONException { + JSONObject obj = new JSONObject(); + obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT)); + obj.put("hash_type", mHashType.toString()); + return obj; + } + + public static Fingerprint fromJson(JSONObject obj) throws JSONException { + String hashTypeStr = obj.getString("hash_type"); + byte[] fingerprintBytes = Base64.decode(obj.getString("bytes"), Base64.DEFAULT); + + final HashType hashType; + if ("SHA256".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA256; + } else if ("SHA1".equalsIgnoreCase(hashTypeStr)) { + hashType = HashType.SHA1; + } else { + throw new JSONException("Unrecognized hash type: " + hashTypeStr); + } + + return new Fingerprint(hashType, fingerprintBytes); + } + + public boolean matchesCert(X509Certificate cert) throws CertificateException { + Fingerprint o = null; + switch (mHashType) { + case SHA256: + o = Fingerprint.newSha256Fingerprint(cert); + break; + case SHA1: + o = Fingerprint.newSha1Fingerprint(cert); + break; + } + + return equals(o); + } + + public String toString() { + return String.format("Fingerprint{type: '%s', fingeprint: '%s'}", mHashType.toString(), getBytesAsHexString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Fingerprint that = (Fingerprint) o; + + if (!Arrays.equals(mBytes, that.mBytes)) return false; + return mHashType == that.mHashType; + + } + + @Override + public int hashCode() { + int result = mBytes != null ? Arrays.hashCode(mBytes) : 0; + result = 31 * result + (mHashType != null ? mHashType.hashCode() : 0); + return result; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/HomeServerConnectionConfig.java new file mode 100644 index 0000000000..9a84b2a25c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/HomeServerConnectionConfig.java @@ -0,0 +1,676 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.CipherSuite; +import okhttp3.TlsVersion; +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Represents how to connect to a specific Homeserver, may include credentials to use. + */ +public class HomeServerConnectionConfig { + + // the home server URI + private Uri mHomeServerUri; + // the jitsi server URI. Can be null + @Nullable + private Uri mJitsiServerUri; + // the identity server URI. Can be null + @Nullable + private Uri mIdentityServerUri; + // the anti-virus server URI + private Uri mAntiVirusServerUri; + // allowed fingerprints + private List mAllowedFingerprints = new ArrayList<>(); + // the credentials + private Credentials mCredentials; + // tell whether we should reject X509 certs that were issued by trusts CAs and only trustcerts with matching fingerprints. + private boolean mPin; + // the accepted TLS versions + private List mTlsVersions; + // the accepted TLS cipher suites + private List mTlsCipherSuites; + // should accept TLS extensions + private boolean mShouldAcceptTlsExtensions = true; + // Force usage of TLS versions + private boolean mForceUsageTlsVersions; + // the proxy hostname + private String mProxyHostname; + // the proxy port + private int mProxyPort = -1; + + + /** + * Private constructor. Please use the Builder + */ + private HomeServerConnectionConfig() { + // Private constructor + } + + /** + * Update the home server URI. + * + * @param uri the new HS uri + */ + public void setHomeserverUri(Uri uri) { + mHomeServerUri = uri; + } + + /** + * @return the home server uri + */ + public Uri getHomeserverUri() { + return mHomeServerUri; + } + + /** + * @return the jitsi server uri + */ + public Uri getJitsiServerUri() { + return mJitsiServerUri; + } + + /** + * @return the identity server uri, or null if not defined + */ + @Nullable + public Uri getIdentityServerUri() { + return mIdentityServerUri; + } + + /** + * @return the anti-virus server uri + */ + public Uri getAntiVirusServerUri() { + if (null != mAntiVirusServerUri) { + return mAntiVirusServerUri; + } + // Else consider the HS uri by default. + return mHomeServerUri; + } + + /** + * @return the allowed fingerprints. + */ + public List getAllowedFingerprints() { + return mAllowedFingerprints; + } + + /** + * @return the credentials + */ + public Credentials getCredentials() { + return mCredentials; + } + + /** + * Update the credentials. + * + * @param credentials the new credentials + */ + public void setCredentials(Credentials credentials) { + mCredentials = credentials; + + // Override home server url and/or identity server url if provided + if (credentials.wellKnown != null) { + if (credentials.wellKnown.homeServer != null) { + String homeServerUrl = credentials.wellKnown.homeServer.baseURL; + + if (!TextUtils.isEmpty(homeServerUrl)) { + // remove trailing "/" + if (homeServerUrl.endsWith("/")) { + homeServerUrl = homeServerUrl.substring(0, homeServerUrl.length() - 1); + } + + Timber.d("Overriding homeserver url to " + homeServerUrl); + mHomeServerUri = Uri.parse(homeServerUrl); + } + } + + if (credentials.wellKnown.identityServer != null) { + String identityServerUrl = credentials.wellKnown.identityServer.baseURL; + + if (!TextUtils.isEmpty(identityServerUrl)) { + // remove trailing "/" + if (identityServerUrl.endsWith("/")) { + identityServerUrl = identityServerUrl.substring(0, identityServerUrl.length() - 1); + } + + Timber.d("Overriding identity server url to " + identityServerUrl); + mIdentityServerUri = Uri.parse(identityServerUrl); + } + } + + if (credentials.wellKnown.jitsiServer != null) { + String jitsiServerUrl = credentials.wellKnown.jitsiServer.preferredDomain; + + if (!TextUtils.isEmpty(jitsiServerUrl)) { + // add trailing "/" + if (!jitsiServerUrl.endsWith("/")) { + jitsiServerUrl =jitsiServerUrl + "/"; + } + + Timber.d("Overriding jitsi server url to " + jitsiServerUrl); + mJitsiServerUri = Uri.parse(jitsiServerUrl); + } + } + } + } + + /** + * @return whether we should reject X509 certs that were issued by trusts CAs and only trust + * certs with matching fingerprints. + */ + public boolean shouldPin() { + return mPin; + } + + /** + * TLS versions accepted for TLS connections with the home server. + */ + @Nullable + public List getAcceptedTlsVersions() { + return mTlsVersions; + } + + /** + * TLS cipher suites accepted for TLS connections with the home server. + */ + @Nullable + public List getAcceptedTlsCipherSuites() { + return mTlsCipherSuites; + } + + /** + * @return whether we should accept TLS extensions. + */ + public boolean shouldAcceptTlsExtensions() { + return mShouldAcceptTlsExtensions; + } + + /** + * @return true if the usage of TlsVersions has to be forced + */ + public boolean forceUsageOfTlsVersions() { + return mForceUsageTlsVersions; + } + + + /** + * @return proxy config if available + */ + @Nullable + public Proxy getProxyConfig() { + if (mProxyHostname == null || mProxyHostname.length() == 0 || mProxyPort == -1) { + return null; + } + + return new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(mProxyHostname, mProxyPort)); + } + + + @Override + public String toString() { + return "HomeserverConnectionConfig{" + + "mHomeServerUri=" + mHomeServerUri + + ", mJitsiServerUri=" + mJitsiServerUri + + ", mIdentityServerUri=" + mIdentityServerUri + + ", mAntiVirusServerUri=" + mAntiVirusServerUri + + ", mAllowedFingerprints size=" + mAllowedFingerprints.size() + + ", mCredentials=" + mCredentials + + ", mPin=" + mPin + + ", mShouldAcceptTlsExtensions=" + mShouldAcceptTlsExtensions + + ", mProxyHostname=" + (null == mProxyHostname ? "" : mProxyHostname) + + ", mProxyPort=" + (-1 == mProxyPort ? "" : mProxyPort) + + ", mTlsVersions=" + (null == mTlsVersions ? "" : mTlsVersions.size()) + + ", mTlsCipherSuites=" + (null == mTlsCipherSuites ? "" : mTlsCipherSuites.size()) + + '}'; + } + + /** + * Convert the object instance into a JSon object + * + * @return the JSon representation + * @throws JSONException the JSON conversion failure reason + */ + public JSONObject toJson() throws JSONException { + JSONObject json = new JSONObject(); + + json.put("home_server_url", mHomeServerUri.toString()); + Uri jitsiServerUri = getJitsiServerUri(); + if (jitsiServerUri != null) { + json.put("jitsi_server_url", jitsiServerUri.toString()); + } + Uri identityServerUri = getIdentityServerUri(); + if (identityServerUri != null) { + json.put("identity_server_url", identityServerUri.toString()); + } + + if (mAntiVirusServerUri != null) { + json.put("antivirus_server_url", mAntiVirusServerUri.toString()); + } + + json.put("pin", mPin); + + if (mCredentials != null) json.put("credentials", mCredentials.toJson()); + if (mAllowedFingerprints != null) { + List fingerprints = new ArrayList<>(mAllowedFingerprints.size()); + + for (Fingerprint fingerprint : mAllowedFingerprints) { + fingerprints.add(fingerprint.toJson()); + } + + json.put("fingerprints", new JSONArray(fingerprints)); + } + + json.put("tls_extensions", mShouldAcceptTlsExtensions); + + if (mTlsVersions != null) { + List tlsVersions = new ArrayList<>(mTlsVersions.size()); + + for (TlsVersion tlsVersion : mTlsVersions) { + tlsVersions.add(tlsVersion.javaName()); + } + + json.put("tls_versions", new JSONArray(tlsVersions)); + } + + json.put("force_usage_of_tls_versions", mForceUsageTlsVersions); + + if (mTlsCipherSuites != null) { + List tlsCipherSuites = new ArrayList<>(mTlsCipherSuites.size()); + + for (CipherSuite tlsCipherSuite : mTlsCipherSuites) { + tlsCipherSuites.add(tlsCipherSuite.javaName()); + } + + json.put("tls_cipher_suites", new JSONArray(tlsCipherSuites)); + } + + if (mProxyPort != -1) { + json.put("proxy_port", mProxyPort); + } + + if (mProxyHostname != null && mProxyHostname.length() > 0) { + json.put("proxy_hostname", mProxyHostname); + } + + return json; + } + + /** + * Create an object instance from the json object. + * + * @param jsonObject the json object + * @return a HomeServerConnectionConfig instance + * @throws JSONException the conversion failure reason + */ + public static HomeServerConnectionConfig fromJson(JSONObject jsonObject) throws JSONException { + JSONObject credentialsObj = jsonObject.optJSONObject("credentials"); + Credentials creds = credentialsObj != null ? Credentials.fromJson(credentialsObj) : null; + + Builder builder = new Builder() + .withHomeServerUri(Uri.parse(jsonObject.getString("home_server_url"))) + .withJitsiServerUri(jsonObject.has("jitsi_server_url") ? Uri.parse(jsonObject.getString("jitsi_server_url")) : null) + .withIdentityServerUri(jsonObject.has("identity_server_url") ? Uri.parse(jsonObject.getString("identity_server_url")) : null) + .withCredentials(creds) + .withPin(jsonObject.optBoolean("pin", false)); + + JSONArray fingerprintArray = jsonObject.optJSONArray("fingerprints"); + if (fingerprintArray != null) { + for (int i = 0; i < fingerprintArray.length(); i++) { + builder.addAllowedFingerPrint(Fingerprint.fromJson(fingerprintArray.getJSONObject(i))); + } + } + + // Set the anti-virus server uri if any + if (jsonObject.has("antivirus_server_url")) { + builder.withAntiVirusServerUri(Uri.parse(jsonObject.getString("antivirus_server_url"))); + } + + builder.withShouldAcceptTlsExtensions(jsonObject.optBoolean("tls_extensions", true)); + + // Set the TLS versions if any + if (jsonObject.has("tls_versions")) { + JSONArray tlsVersionsArray = jsonObject.optJSONArray("tls_versions"); + if (tlsVersionsArray != null) { + for (int i = 0; i < tlsVersionsArray.length(); i++) { + builder.addAcceptedTlsVersion(TlsVersion.forJavaName(tlsVersionsArray.getString(i))); + } + } + } + + builder.forceUsageOfTlsVersions(jsonObject.optBoolean("force_usage_of_tls_versions", false)); + + // Set the TLS cipher suites if any + if (jsonObject.has("tls_cipher_suites")) { + JSONArray tlsCipherSuitesArray = jsonObject.optJSONArray("tls_cipher_suites"); + if (tlsCipherSuitesArray != null) { + for (int i = 0; i < tlsCipherSuitesArray.length(); i++) { + builder.addAcceptedTlsCipherSuite(CipherSuite.forJavaName(tlsCipherSuitesArray.getString(i))); + } + } + } + + // Set the proxy options right if any + if (jsonObject.has("proxy_hostname") && jsonObject.has("proxy_port")) { + builder.withProxy(jsonObject.getString("proxy_hostname"), jsonObject.getInt("proxy_port")); + } + + return builder.build(); + } + + /** + * Builder + */ + public static class Builder { + private HomeServerConnectionConfig mHomeServerConnectionConfig; + + /** + * Builder constructor + */ + public Builder() { + mHomeServerConnectionConfig = new HomeServerConnectionConfig(); + } + + /** + * create a Builder from an existing HomeServerConnectionConfig + */ + public Builder(HomeServerConnectionConfig from) { + try { + mHomeServerConnectionConfig = HomeServerConnectionConfig.fromJson(from.toJson()); + } catch (JSONException e) { + // Should not happen + throw new RuntimeException("Unable to create a HomeServerConnectionConfig", e); + } + } + + /** + * @param homeServerUri The URI to use to connect to the homeserver. Cannot be null + * @return this builder + */ + public Builder withHomeServerUri(final Uri homeServerUri) { + if (homeServerUri == null || (!"http".equals(homeServerUri.getScheme()) && !"https".equals(homeServerUri.getScheme()))) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + + // remove trailing / + if (homeServerUri.toString().endsWith("/")) { + try { + String url = homeServerUri.toString(); + mHomeServerConnectionConfig.mHomeServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid home server URI: " + homeServerUri); + } + } else { + mHomeServerConnectionConfig.mHomeServerUri = homeServerUri; + } + + return this; + } + + /** + * @param jitsiServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withJitsiServerUri(@Nullable final Uri jitsiServerUri) { + if (jitsiServerUri != null + && !jitsiServerUri.toString().isEmpty() + && !"http".equals(jitsiServerUri.getScheme()) + && !"https".equals(jitsiServerUri.getScheme())) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + + // add trailing / + if ((null != jitsiServerUri) && !jitsiServerUri.toString().endsWith("/")) { + try { + String url = jitsiServerUri.toString(); + mHomeServerConnectionConfig.mJitsiServerUri = Uri.parse(url + "/"); + } catch (Exception e) { + throw new RuntimeException("Invalid jitsi server URI: " + jitsiServerUri); + } + } else { + if (jitsiServerUri != null && jitsiServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mJitsiServerUri = null; + } else { + mHomeServerConnectionConfig.mJitsiServerUri = jitsiServerUri; + } + } + + return this; + } + + /** + * @param identityServerUri The URI to use to manage identity. Can be null + * @return this builder + */ + public Builder withIdentityServerUri(@Nullable final Uri identityServerUri) { + if (identityServerUri != null + && !identityServerUri.toString().isEmpty() + && !"http".equals(identityServerUri.getScheme()) + && !"https".equals(identityServerUri.getScheme())) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + + // remove trailing / + if ((null != identityServerUri) && identityServerUri.toString().endsWith("/")) { + try { + String url = identityServerUri.toString(); + mHomeServerConnectionConfig.mIdentityServerUri = Uri.parse(url.substring(0, url.length() - 1)); + } catch (Exception e) { + throw new RuntimeException("Invalid identity server URI: " + identityServerUri); + } + } else { + if (identityServerUri != null && identityServerUri.toString().isEmpty()) { + mHomeServerConnectionConfig.mIdentityServerUri = null; + } else { + mHomeServerConnectionConfig.mIdentityServerUri = identityServerUri; + } + } + + return this; + } + + /** + * @param credentials The credentials to use, if needed. Can be null. + * @return this builder + */ + public Builder withCredentials(@Nullable Credentials credentials) { + mHomeServerConnectionConfig.mCredentials = credentials; + return this; + } + + /** + * @param allowedFingerprint If using SSL, allow server certs that match this fingerprint. + * @return this builder + */ + public Builder addAllowedFingerPrint(@Nullable Fingerprint allowedFingerprint) { + if (allowedFingerprint != null) { + mHomeServerConnectionConfig.mAllowedFingerprints.add(allowedFingerprint); + } + + return this; + } + + /** + * @param pin If true only allow certs matching given fingerprints, otherwise fallback to + * standard X509 checks. + * @return this builder + */ + public Builder withPin(boolean pin) { + mHomeServerConnectionConfig.mPin = pin; + + return this; + } + + /** + * @param shouldAcceptTlsExtension + * @return this builder + */ + public Builder withShouldAcceptTlsExtensions(boolean shouldAcceptTlsExtension) { + mHomeServerConnectionConfig.mShouldAcceptTlsExtensions = shouldAcceptTlsExtension; + + return this; + } + + /** + * Add an accepted TLS version for TLS connections with the home server. + * + * @param tlsVersion the tls version to add to the set of TLS versions accepted. + * @return this builder + */ + public Builder addAcceptedTlsVersion(@NonNull TlsVersion tlsVersion) { + if (mHomeServerConnectionConfig.mTlsVersions == null) { + mHomeServerConnectionConfig.mTlsVersions = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsVersions.add(tlsVersion); + + return this; + } + + /** + * Force the usage of TlsVersion. This can be usefull for device on Android version < 20 + * + * @param forceUsageOfTlsVersions set to true to force the usage of specified TlsVersions (with {@link #addAcceptedTlsVersion(TlsVersion)} + * @return this builder + */ + public Builder forceUsageOfTlsVersions(boolean forceUsageOfTlsVersions) { + mHomeServerConnectionConfig.mForceUsageTlsVersions = forceUsageOfTlsVersions; + + return this; + } + + /** + * Add a TLS cipher suite to the list of accepted TLS connections with the home server. + * + * @param tlsCipherSuite the tls cipher suite to add. + * @return this builder + */ + public Builder addAcceptedTlsCipherSuite(@NonNull CipherSuite tlsCipherSuite) { + if (mHomeServerConnectionConfig.mTlsCipherSuites == null) { + mHomeServerConnectionConfig.mTlsCipherSuites = new ArrayList<>(); + } + + mHomeServerConnectionConfig.mTlsCipherSuites.add(tlsCipherSuite); + + return this; + } + + /** + * Update the anti-virus server URI. + * + * @param antivirusServerUri the new anti-virus uri. Can be null + * @return this builder + */ + public Builder withAntiVirusServerUri(@Nullable Uri antivirusServerUri) { + if ((null != antivirusServerUri) && (!"http".equals(antivirusServerUri.getScheme()) && !"https".equals(antivirusServerUri.getScheme()))) { + throw new RuntimeException("Invalid antivirus server URI: " + antivirusServerUri); + } + + mHomeServerConnectionConfig.mAntiVirusServerUri = antivirusServerUri; + + return this; + } + + /** + * Convenient method to limit the TLS versions and cipher suites for this Builder + * Ref: + * - https://www.ssi.gouv.fr/uploads/2017/02/security-recommendations-for-tls_v1.1.pdf + * - https://developer.android.com/reference/javax/net/ssl/SSLEngine + * + * @param tlsLimitations true to use Tls limitations + * @param enableCompatibilityMode set to true for Android < 20 + * @return this builder + */ + public Builder withTlsLimitations(boolean tlsLimitations, boolean enableCompatibilityMode) { + if (tlsLimitations) { + withShouldAcceptTlsExtensions(false); + + // Tls versions + addAcceptedTlsVersion(TlsVersion.TLS_1_2); + addAcceptedTlsVersion(TlsVersion.TLS_1_3); + + forceUsageOfTlsVersions(enableCompatibilityMode); + + // Cipher suites + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256); + + if (enableCompatibilityMode) { + // Adopt some preceding cipher suites for Android < 20 to be able to negotiate + // a TLS session. + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA); + addAcceptedTlsCipherSuite(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); + } + } + + return this; + } + + /** + * @param proxyHostname Proxy Hostname + * @param proxyPort Proxy Port + * @return this builder + */ + public Builder withProxy(@Nullable String proxyHostname, int proxyPort) { + mHomeServerConnectionConfig.mProxyHostname = proxyHostname; + mHomeServerConnectionConfig.mProxyPort = proxyPort; + return this; + } + + /** + * @return the {@link HomeServerConnectionConfig} + */ + public HomeServerConnectionConfig build() { + // Check mandatory parameters + if (mHomeServerConnectionConfig.mHomeServerUri == null) { + throw new RuntimeException("Home server URI not set"); + } + + return mHomeServerConnectionConfig; + } + + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/LoginStorage.java new file mode 100755 index 0000000000..325d8b3bc6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/LoginStorage.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Stores login credentials in SharedPreferences. + */ +public class LoginStorage { + private static final String PREFS_LOGIN = "Vector.LoginStorage"; + + // multi accounts + home server config + private static final String PREFS_KEY_CONNECTION_CONFIGS = "PREFS_KEY_CONNECTION_CONFIGS"; + + private final Context mContext; + + public LoginStorage(Context appContext) { + mContext = appContext.getApplicationContext(); + + } + + /** + * @return the list of home server configurations. + */ + public List getCredentialsList() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + + String connectionConfigsString = prefs.getString(PREFS_KEY_CONNECTION_CONFIGS, null); + + Timber.d("Got connection json: "); + + if (connectionConfigsString == null) { + return new ArrayList<>(); + } + + try { + JSONArray connectionConfigsStrings = new JSONArray(connectionConfigsString); + + List configList = new ArrayList<>( + connectionConfigsStrings.length() + ); + + for (int i = 0; i < connectionConfigsStrings.length(); i++) { + configList.add( + HomeServerConnectionConfig.fromJson(connectionConfigsStrings.getJSONObject(i)) + ); + } + + return configList; + } catch (JSONException e) { + Timber.e(e, "Failed to deserialize accounts"); + throw new RuntimeException("Failed to deserialize accounts"); + } + } + + /** + * Add a credentials to the credentials list + * + * @param config the home server config to add. + */ + public void addCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + + configs.add(config); + + List serialized = new ArrayList<>(configs.size()); + + try { + for (HomeServerConnectionConfig c : configs) { + serialized.add(c.toJson()); + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Remove the credentials from credentials list + * + * @param config the credentials to remove + */ + public void removeCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + Timber.d("Removing account: " + config.getCredentials().userId); + + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + List serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Replace the credential from credentials list, based on credentials.userId. + * If it does not match an existing credential it does *not* insert the new credentials. + * + * @param config the credentials to insert + */ + public void replaceCredentials(HomeServerConnectionConfig config) { + if (null != config && config.getCredentials() != null) { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + List configs = getCredentialsList(); + List serialized = new ArrayList<>(configs.size()); + + boolean found = false; + try { + for (HomeServerConnectionConfig c : configs) { + if (c.getCredentials().userId.equals(config.getCredentials().userId)) { + serialized.add(config.toJson()); + found = true; + } else { + serialized.add(c.toJson()); + } + } + } catch (JSONException e) { + throw new RuntimeException("Failed to serialize connection config"); + } + + if (!found) return; + + String ser = new JSONArray(serialized).toString(); + + Timber.d("Storing " + serialized.size() + " credentials"); + + editor.putString(PREFS_KEY_CONNECTION_CONFIGS, ser); + editor.apply(); + } + } + + /** + * Clear the stored values + */ + public void clear() { + SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(PREFS_KEY_CONNECTION_CONFIGS); + //Need to commit now because called before forcing an app restart + editor.commit(); + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java new file mode 100644 index 0000000000..e914bfb724 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.annotation.Nullable; +import javax.net.ssl.X509TrustManager; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Implements a TrustManager that checks Certificates against an explicit list of known + * fingerprints. + */ +public class PinnedTrustManager implements X509TrustManager { + private final List mFingerprints; + @Nullable + private final X509TrustManager mDefaultTrustManager; + + /** + * @param fingerprints An array of SHA256 cert fingerprints + * @param defaultTrustManager Optional trust manager to fall back on if cert does not match + * any of the fingerprints. Can be null. + */ + public PinnedTrustManager(List fingerprints, @Nullable X509TrustManager defaultTrustManager) { + mFingerprints = fingerprints; + mDefaultTrustManager = defaultTrustManager; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException { + try { + if (mDefaultTrustManager != null) { + mDefaultTrustManager.checkClientTrusted( + chain, s + ); + return; + } + } catch (CertificateException e) { + // If there is an exception we fall back to checking fingerprints + if (mFingerprints == null || mFingerprints.size() == 0) { + throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); + } + } + checkTrusted("client", chain); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException { + try { + if (mDefaultTrustManager != null) { + mDefaultTrustManager.checkServerTrusted( + chain, s + ); + return; + } + } catch (CertificateException e) { + // If there is an exception we fall back to checking fingerprints + if (mFingerprints == null || mFingerprints.isEmpty()) { + throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); + } + } + checkTrusted("server", chain); + } + + private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException { + X509Certificate cert = chain[0]; + + boolean found = false; + if (mFingerprints != null) { + for (Fingerprint allowedFingerprint : mFingerprints) { + if (allowedFingerprint != null && allowedFingerprint.matchesCert(cert)) { + found = true; + break; + } + } + } + + if (!found) { + throw new UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java new file mode 100644 index 0000000000..6a5921a82d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +import okhttp3.TlsVersion; +import timber.log.Timber; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Force the usage of Tls versions on every created socket + * Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ + */ +/*package*/ class TLSSocketFactory extends SSLSocketFactory { + private SSLSocketFactory internalSSLSocketFactory; + + private String[] enabledProtocols; + + /** + * Constructor + * + * @param trustPinned + * @param acceptedTlsVersions + * @throws KeyManagementException + * @throws NoSuchAlgorithmException + */ + /*package*/ TLSSocketFactory(TrustManager[] trustPinned, List acceptedTlsVersions) throws KeyManagementException, NoSuchAlgorithmException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustPinned, new SecureRandom()); + internalSSLSocketFactory = context.getSocketFactory(); + + enabledProtocols = new String[acceptedTlsVersions.size()]; + int i = 0; + for (TlsVersion tlsVersion : acceptedTlsVersions) { + enabledProtocols[i] = tlsVersion.javaName(); + i++; + } + } + + @Override + public String[] getDefaultCipherSuites() { + return internalSSLSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return internalSSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableTLSOnSocket(Socket socket) { + if (socket != null && (socket instanceof SSLSocket)) { + SSLSocket sslSocket = (SSLSocket) socket; + + List supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); + List filteredEnabledProtocols = new ArrayList<>(); + + for (String protocol : enabledProtocols) { + if (supportedProtocols.contains(protocol)) { + filteredEnabledProtocols.add(protocol); + } + } + + if (!filteredEnabledProtocols.isEmpty()) { + try { + sslSocket.setEnabledProtocols(filteredEnabledProtocols.toArray(new String[filteredEnabledProtocols.size()])); + } catch (Exception e) { + Timber.e(e, "Exception"); + } + } + } + return socket; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java new file mode 100644 index 0000000000..518989a272 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * Thrown when we are given a certificate that does match the certificate we were told to + * expect. + */ +public class UnrecognizedCertificateException extends CertificateException { + private final X509Certificate mCert; + private final Fingerprint mFingerprint; + + public UnrecognizedCertificateException(X509Certificate cert, Fingerprint fingerprint, Throwable cause) { + super("Unrecognized certificate with unknown fingerprint: " + cert.getSubjectDN(), cause); + mCert = cert; + mFingerprint = fingerprint; + } + + public X509Certificate getCertificate() { + return mCert; + } + + public Fingerprint getFingerprint() { + return mFingerprint; + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnown.kt new file mode 100644 index 0000000000..457741d910 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnown.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "m.homeserver": {
+ *         "base_url": "https://matrix.org"
+ *     },
+ *     "m.identity_server": {
+ *         "base_url": "https://vector.im"
+ *     }
+ *     "m.integrations": {
+ *          "managers": [
+ *              {
+ *                  "api_url": "https://integrations.example.org",
+ *                  "ui_url": "https://integrations.example.org/ui"
+ *              },
+ *              {
+ *                  "api_url": "https://bots.example.org"
+ *              }
+ *          ]
+ *    }
+ *     "im.vector.riot.jitsi": {
+ *         "preferredDomain": "https://jitsi.riot.im/"
+ *     }
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnown { + + @JvmField + @Json(name = "m.homeserver") + var homeServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.identity_server") + var identityServer: WellKnownBaseConfig? = null + + @JvmField + @Json(name = "m.integrations") + var integrations: Map? = null + + /** + * Returns the list of integration managers proposed + */ + fun getIntegrationManagers(): List { + val managers = ArrayList() + integrations?.get("managers")?.let { + (it as? ArrayList<*>)?.let { configs -> + configs.forEach { config -> + (config as? Map<*, *>)?.let { map -> + val apiUrl = map["api_url"] as? String + val uiUrl = map["ui_url"] as? String ?: apiUrl + if (apiUrl != null + && apiUrl.startsWith("https://") + && uiUrl!!.startsWith("https://")) { + managers.add(WellKnownManagerConfig( + apiUrl = apiUrl, + uiUrl = uiUrl + )) + } + } + } + } + } + return managers + } + + @JvmField + @Json(name = "im.vector.riot.jitsi") + var jitsiServer: WellKnownPreferredConfig? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownBaseConfig.kt new file mode 100644 index 0000000000..8b0df815e2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownBaseConfig.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "base_url": "https://vector.im"
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnownBaseConfig { + + @JvmField + @Json(name = "base_url") + var baseURL: String? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownManagerConfig.kt new file mode 100644 index 0000000000..56291f60ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownManagerConfig.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +data class WellKnownManagerConfig( + val apiUrl : String, + val uiUrl: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownPreferredConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownPreferredConfig.kt new file mode 100644 index 0000000000..4581459928 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/WellKnownPreferredConfig.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.legacy.riot + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "preferredDomain": "https://jitsi.riot.im/"
+ * }
+ * 
+ */ +@JsonClass(generateAdapter = true) +class WellKnownPreferredConfig { + + @JvmField + @Json(name = "preferredDomain") + var preferredDomain: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java new file mode 100755 index 0000000000..7065d05c67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.androidsdk.crypto.data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +public class MXDeviceInfo implements Serializable { + private static final long serialVersionUID = 20129670646382964L; + + // This device is a new device and the user was not warned it has been added. + public static final int DEVICE_VERIFICATION_UNKNOWN = -1; + + // The user has not yet verified this device. + public static final int DEVICE_VERIFICATION_UNVERIFIED = 0; + + // The user has verified this device. + public static final int DEVICE_VERIFICATION_VERIFIED = 1; + + // The user has blocked this device. + public static final int DEVICE_VERIFICATION_BLOCKED = 2; + + /** + * The id of this device. + */ + public String deviceId; + + /** + * the user id + */ + public String userId; + + /** + * The list of algorithms supported by this device. + */ + public List algorithms; + + /** + * A map from : to >. + */ + public Map keys; + + /** + * The signature of this MXDeviceInfo. + * A map from : to >. + */ + public Map> signatures; + + /* + * Additional data from the home server. + */ + public Map unsigned; + + /** + * Verification state of this device. + */ + public int mVerified; + + /** + * Constructor + */ + public MXDeviceInfo() { + mVerified = DEVICE_VERIFICATION_UNKNOWN; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java new file mode 100755 index 0000000000..a8f70d1ec2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.androidsdk.crypto.data; + +import org.matrix.olm.OlmInboundGroupSession; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/* + * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose + */ + +/** + * This class adds more context to a OLMInboundGroupSession object. + * This allows additional checks. The class implements NSCoding so that the context can be stored. + */ +public class MXOlmInboundGroupSession2 implements Serializable { + // define a serialVersionUID to avoid having to redefine the class after updates + private static final long serialVersionUID = 201702011617L; + + // The associated olm inbound group session. + public OlmInboundGroupSession mSession; + + // The room in which this session is used. + public String mRoomId; + + // The base64-encoded curve25519 key of the sender. + public String mSenderKey; + + // Other keys the sender claims. + public Map mKeysClaimed; + + // Devices which forwarded this session to us (normally empty). + public List mForwardingCurve25519KeyChain = new ArrayList<>(); +} \ No newline at end of file diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index d46bdfa062..137ba13ae7 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,3 +162,6 @@ Formatter\.formatShortFileSize===1 ### Use kotlin stdlib to test or compare strings # DISABLED # android\.text\.TextUtils + +### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt +enum class===72 \ No newline at end of file diff --git a/tools/release/sign_apk.sh b/tools/release/sign_apk.sh index 866510ba13..77af5823c4 100755 --- a/tools/release/sign_apk.sh +++ b/tools/release/sign_apk.sh @@ -18,7 +18,7 @@ PARAM_APK=$2 # Other params BUILD_TOOLS_VERSION="29.0.3" -MIN_SDK_VERSION=19 +MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/tools/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh index bf021e8345..b145ad45da 100755 --- a/tools/release/sign_apk_unsafe.sh +++ b/tools/release/sign_apk_unsafe.sh @@ -24,7 +24,7 @@ PARAM_KEY_PASS=$4 # Other params BUILD_TOOLS_VERSION="29.0.3" -MIN_SDK_VERSION=19 +MIN_SDK_VERSION=21 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/vector/build.gradle b/vector/build.gradle index f253501177..c80b716008 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -14,9 +14,10 @@ androidExtensions { experimental = true } +// Note: 2 digits max for each value ext.versionMajor = 0 -ext.versionMinor = 23 -ext.versionPatch = 0 +ext.versionMinor = 91 +ext.versionPatch = 2 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -106,7 +107,7 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 android { compileSdkVersion 29 defaultConfig { - applicationId "im.vector.riotx" + applicationId "im.vector.app" // Set to API 21: see #405 minSdkVersion 21 targetSdkVersion 29 @@ -177,7 +178,7 @@ android { buildTypes { debug { applicationIdSuffix ".debug" - resValue "string", "app_name", "RiotX dbg" + resValue "string", "app_name", "Riot.imX dbg" resValue "bool", "debug_mode", "true" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" @@ -186,7 +187,7 @@ android { } release { - resValue "string", "app_name", "RiotX" + resValue "string", "app_name", "Riot.imX" resValue "bool", "debug_mode", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" @@ -230,6 +231,9 @@ android { lintOptions { lintConfig file("lint.xml") + + // TODO Restore true once pb with WorkManager is fixed + abortOnError false } compileOptions { diff --git a/vector/src/gplay/debug/google-services.json b/vector/src/gplay/debug/google-services.json index 185f7afb66..713c1d4e03 100644 --- a/vector/src/gplay/debug/google-services.json +++ b/vector/src/gplay/debug/google-services.json @@ -10,7 +10,7 @@ "client_info": { "mobilesdk_app_id": "1:912726360885:android:4ef8f3a0021e774d", "android_client_info": { - "package_name": "im.vector.riotx.debug" + "package_name": "im.vector.app.debug" } }, "oauth_client": [ diff --git a/vector/src/gplay/release/google-services.json b/vector/src/gplay/release/google-services.json index 7ee7277e2a..fb8f769f45 100644 --- a/vector/src/gplay/release/google-services.json +++ b/vector/src/gplay/release/google-services.json @@ -10,7 +10,7 @@ "client_info": { "mobilesdk_app_id": "1:912726360885:android:4ef8f3a0021e774d", "android_client_info": { - "package_name": "im.vector.riotx" + "package_name": "im.vector.app" } }, "oauth_client": [ diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6cfe02dd0f..20107c9b65 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -15,7 +15,8 @@ - + + + + diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index ab723c2b3d..d0a3174227 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -37,6 +37,7 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.legacy.LegacySessionImporter import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.DaggerVectorComponent import im.vector.riotx.core.di.HasVectorInjector @@ -57,15 +58,15 @@ import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.push.fcm.FcmHelper import timber.log.Timber import java.text.SimpleDateFormat -import java.util.concurrent.Executors import java.util.Date import java.util.Locale +import java.util.concurrent.Executors import javax.inject.Inject class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { lateinit var appContext: Context - // font thread handler + @Inject lateinit var legacySessionImporter: LegacySessionImporter @Inject lateinit var authenticationService: AuthenticationService @Inject lateinit var vectorConfiguration: VectorConfiguration @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider @@ -84,6 +85,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager lateinit var vectorComponent: VectorComponent + // font thread handler private var fontThreadHandler: Handler? = null override fun onCreate() { @@ -121,6 +123,10 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. emojiCompatWrapper.init(fontRequest) notificationUtils.createNotificationChannels() + + // It can takes time, but do we care? + legacySessionImporter.process() + if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! activeSessionHolder.setActiveSession(lastAuthenticatedSession) diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt index 3665df31dd..50cfeaf415 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorModule.kt @@ -25,6 +25,7 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.legacy.LegacySessionImporter import im.vector.matrix.android.api.session.Session import im.vector.riotx.core.error.DefaultErrorFormatter import im.vector.riotx.core.error.ErrorFormatter @@ -64,6 +65,12 @@ abstract class VectorModule { return activeSessionHolder.getActiveSession() } + @Provides + @JvmStatic + fun providesLegacySessionImporter(matrix: Matrix): LegacySessionImporter { + return matrix.legacySessionImporter() + } + @Provides @JvmStatic fun providesAuthenticationService(matrix: Matrix): AuthenticationService { diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index d9020afab4..ad5e2c6d3d 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -220,7 +220,7 @@ class BugReporter @Inject constructor( } if (!mIsCancelled) { - val text = "[RiotX] " + + val text = "[Riot.imX] " + if (forSuggestion) { "[Suggestion] " } else { @@ -292,7 +292,7 @@ class BugReporter @Inject constructor( builder.addFormDataPart("label", context.getString(R.string.git_branch_name)) // Special for RiotX - builder.addFormDataPart("label", "[RiotX]") + builder.addFormDataPart("label", "[Riot.imX]") // Suggestion if (forSuggestion) {