From 2fa73da9fa0eb727437d8a89de3a4e42c6c0541c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:16:47 +0100 Subject: [PATCH 1/6] Setting version for the release 1.6.10 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8ace02205..71e99deaa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ vector.httpLogLevel=NONE # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin GROUP=org.matrix.android POM_ARTIFACT_ID=matrix-android-sdk2 -VERSION_NAME=1.5.30 +VERSION_NAME=1.6.10 POM_PACKAGING=aar From 8a647d83f20d72a964700678e1797b7435ee611a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:23:03 +0100 Subject: [PATCH 2/6] Jcenter dependencies have been removed. --- build.gradle | 7 ------- 1 file changed, 7 deletions(-) diff --git a/build.gradle b/build.gradle index bf97cb972..7d8848817 100644 --- a/build.gradle +++ b/build.gradle @@ -42,13 +42,6 @@ allprojects { groups.mavenCentral.group.each { includeGroup it } } } - //noinspection JcenterRepositoryObsolete - jcenter { - content { - groups.jcenter.regex.each { includeGroupByRegex it } - groups.jcenter.group.each { includeGroup it } - } - } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { From c7836a3c2381a5a27edaec9e09eef48f8514ae9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:25:57 +0100 Subject: [PATCH 3/6] Add module :library:external:realmfieldnameshelper --- .../realmfieldnameshelper/build.gradle | 23 ++ .../dk/ilios/realmfieldnames/ClassData.kt | 24 +++ .../realmfieldnames/FieldNameFormatter.kt | 79 +++++++ .../dk/ilios/realmfieldnames/FileGenerator.kt | 77 +++++++ .../RealmFieldNamesProcessor.kt | 197 ++++++++++++++++++ .../gradle/incremental.annotation.processors | 1 + .../javax.annotation.processing.Processor | 1 + settings.gradle | 1 + 8 files changed, 403 insertions(+) create mode 100644 library/external/realmfieldnameshelper/build.gradle create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle new file mode 100644 index 000000000..e05155021 --- /dev/null +++ b/library/external/realmfieldnameshelper/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'kotlin' +apply plugin: 'java' + +sourceCompatibility = versions.sourceCompat +targetCompatibility = versions.sourceCompat + +dependencies { + implementation 'com.squareup:javapoet:1.13.0' +} + +task javadocJar(type: Jar, dependsOn: 'javadoc') { + from javadoc.destinationDir + classifier = 'javadoc' +} +task sourcesJar(type: Jar, dependsOn: 'classes') { + from sourceSets.main.allSource + classifier = 'sources' +} + +sourceSets { + main.java.srcDirs += 'src/main/kotlin' +} + diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt new file mode 100644 index 000000000..d683a2ade --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt @@ -0,0 +1,24 @@ +package dk.ilios.realmfieldnames + +import java.util.TreeMap + +/** + * Class responsible for keeping track of the metadata for each Realm model class. + */ +class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) { + + val fields = TreeMap() // + + fun addField(field: String, linkedType: String?) { + fields.put(field, linkedType) + } + + val qualifiedClassName: String + get() { + if (packageName != null && !packageName.isEmpty()) { + return packageName + "." + simpleClassName + } else { + return simpleClassName + } + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt new file mode 100644 index 000000000..95f002472 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -0,0 +1,79 @@ +package dk.ilios.realmfieldnames + +import java.util.Locale + +/** + * Class for encapsulating the rules for converting between the field name in the Realm model class + * and the matching name in the "<class>Fields" class. + */ +class FieldNameFormatter { + + @JvmOverloads + fun format(fieldName: String?, locale: Locale = Locale.US): String { + if (fieldName == null || fieldName == "") { + return "" + } + + // Normalize word separator chars + val normalizedFieldName: String = fieldName.replace('-', '_') + + // Iterate field name using the following rules + // lowerCase m followed by upperCase anything is considered hungarian notation + // lowercase char followed by uppercase char is considered camel case + // Two uppercase chars following each other is considered non-standard camelcase + // _ and - are treated as word separators + val result = StringBuilder(normalizedFieldName.length) + + if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) { + result.append(normalizedFieldName) + } else { + var previousCodepoint: Int? + var currentCodepoint: Int? = null + val length = normalizedFieldName.length + var offset = 0 + while (offset < length) { + previousCodepoint = currentCodepoint + currentCodepoint = normalizedFieldName.codePointAt(offset) + + if (previousCodepoint != null) { + if (Character.isUpperCase(currentCodepoint) && + !Character.isUpperCase(previousCodepoint) && + previousCodepoint === 'm'.code as Int? && + result.length == 1 + ) { + // Hungarian notation starting with: mX + result.delete(0, 1) + result.appendCodePoint(currentCodepoint) + } else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) { + // InvalidCamelCase: XXYx (should have been xxYx) + if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) { + val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint)) + if (Character.isLowerCase(nextCodePoint)) { + result.append("_") + } + } + result.appendCodePoint(currentCodepoint) + } else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) { + // Word-separator: x-x or x_x + result.append("_") + } else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit( + previousCodepoint + )) { + // camelCase: xX + result.append("_") + result.appendCodePoint(currentCodepoint) + } else { + // Unknown type + result.appendCodePoint(currentCodepoint) + } + } else { + // Only triggered for first code point + result.appendCodePoint(currentCodepoint) + } + offset += Character.charCount(currentCodepoint) + } + } + + return result.toString().uppercase(locale) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt new file mode 100644 index 000000000..2ddba1ccb --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -0,0 +1,77 @@ +package dk.ilios.realmfieldnames + +import com.squareup.javapoet.FieldSpec +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.TypeSpec +import java.io.IOException +import javax.annotation.processing.Filer +import javax.lang.model.element.Modifier + +/** + * Class responsible for creating the final output files. + */ +class FileGenerator(private val filer: Filer) { + private val formatter: FieldNameFormatter + + init { + this.formatter = FieldNameFormatter() + } + + /** + * Generates all the "<class>Fields" fields with field name references. + * @param fileData Files to create. + * * + * @return `true` if the files where generated, `false` if not. + */ + fun generate(fileData: Set): Boolean { + return fileData + .filter { !it.libraryClass } + .all { generateFile(it, fileData) } + } + + private fun generateFile(classData: ClassData, classPool: Set): Boolean { + val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n", + classData.packageName, classData.simpleClassName) + + // Add a static field reference to each queryable field in the Realm model class + classData.fields.forEach { fieldName, value -> + if (value != null) { + // Add linked field names (only up to depth 1) + for (data in classPool) { + if (data.qualifiedClassName == value) { + val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) + val linkedClassFields = data.fields + addField(linkedTypeSpec, "$", fieldName) + for (linkedFieldName in linkedClassFields.keys) { + addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName) + } + fileBuilder.addType(linkedTypeSpec.build()) + } + } + } else { + // Add normal field name + addField(fileBuilder, fieldName, fieldName) + } + } + + val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build() + try { + javaFile.writeTo(filer) + return true + } catch (e: IOException) { + // e.printStackTrace() + return false + } + } + + private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) { + val field = FieldSpec.builder(String::class.java, formatter.format(fieldName)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("\$S", fieldNameValue) + .build() + fileBuilder.addField(field) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt new file mode 100644 index 000000000..29d044c46 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -0,0 +1,197 @@ +package dk.ilios.realmfieldnames + +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.Messager +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.Diagnostic + +/** + * The Realm Field Names Generator is a processor that looks at all available Realm model classes + * and create an companion class with easy, type-safe access to all field names. + */ + +@SupportedAnnotationTypes("io.realm.annotations.RealmClass") +class RealmFieldNamesProcessor : AbstractProcessor() { + + private val classes = HashSet() + private lateinit var typeUtils: Types + private lateinit var messager: Messager + private lateinit var elementUtils: Elements + private var ignoreAnnotation: TypeMirror? = null + private var realmClassAnnotation: TypeElement? = null + private var realmModelInterface: TypeMirror? = null + private var realmListClass: DeclaredType? = null + private var realmResultsClass: DeclaredType? = null + private var fileGenerator: FileGenerator? = null + private var done = false + + @Synchronized + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + typeUtils = processingEnv.typeUtils!! + messager = processingEnv.messager!! + elementUtils = processingEnv.elementUtils!! + + // If the Realm class isn't found something is wrong the project setup. + // Most likely Realm isn't on the class path, so just disable the + // annotation processor + val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null + if (!isRealmAvailable) { + done = true + } else { + ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType() + realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass") + realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType() + realmListClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmList"), + typeUtils.getWildcardType(null, null) + ) + realmResultsClass = typeUtils.getDeclaredType( + elementUtils.getTypeElement("io.realm.RealmResults"), + typeUtils.getWildcardType(null, null) + ) + fileGenerator = FileGenerator(processingEnv.filer) + } + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latestSupported() + } + + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + if (done) { + return CONSUME_ANNOTATIONS + } + + // Create all proxy classes + roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement -> + if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) { + val classData = processClass(classElement as TypeElement) + classes.add(classData) + } + } + + // If a model class references a library class, the library class will not be part of this + // annotation processor round. For all those references we need to pull field information + // from the classpath instead. + val libraryClasses = HashMap() + classes.forEach { + it.fields.forEach { _, value -> + // Analyze the library class file the first time it is encountered. + if (value != null) { + if (classes.all { it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) { + libraryClasses.put(value, processLibraryClass(value)) + } + } + } + } + classes.addAll(libraryClasses.values) + + done = fileGenerator!!.generate(classes) + return CONSUME_ANNOTATIONS + } + + private fun processClass(classElement: TypeElement): ClassData { + val packageName = getPackageName(classElement) + val className = classElement.simpleName.toString() + val data = ClassData(packageName, className) + + // Find all appropriate fields + classElement.enclosedElements.forEach { + val elementKind = it.kind + if (elementKind == ElementKind.FIELD) { + val variableElement = it as VariableElement + + val modifiers = variableElement.modifiers + if (modifiers.contains(Modifier.STATIC)) { + return@forEach // completely ignore any static fields + } + + // Don't add any fields marked with @Ignore + val ignoreField = variableElement.annotationMirrors + .map { it.annotationType.toString() } + .contains("io.realm.annotations.Ignore") + + if (!ignoreField) { + data.addField(it.getSimpleName().toString(), getLinkedFieldType(it)) + } + } + } + + return data + } + + private fun processLibraryClass(qualifiedClassName: String): ClassData { + val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath + val packageName = libraryClass.`package`.name + val className = libraryClass.simpleName + val data = ClassData(packageName, className, libraryClass = true) + + libraryClass.declaredFields.forEach { field -> + if (java.lang.reflect.Modifier.isStatic(field.modifiers)) { + return@forEach // completely ignore any static fields + } + + // Add field if it is not being ignored. + if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) { + data.addField(field.name, field.type.name) + } + } + + return data + } + + /** + * Returns the qualified name of the linked Realm class field or `null` if it is not a linked + * class. + */ + private fun getLinkedFieldType(field: Element): String? { + if (typeUtils.isAssignable(field.asType(), realmModelInterface)) { + // Object link + val typeElement = elementUtils.getTypeElement(field.asType().toString()) + return typeElement.qualifiedName.toString() + } else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) { + // List link or LinkingObjects + val fieldType = field.asType() + val typeArguments = (fieldType as DeclaredType).typeArguments + if (typeArguments.size == 0) { + return null + } + return typeArguments[0].toString() + } else { + return null + } + } + + private fun getPackageName(classElement: TypeElement): String? { + val enclosingElement = classElement.enclosingElement + + if (enclosingElement.kind != ElementKind.PACKAGE) { + messager.printMessage( + Diagnostic.Kind.ERROR, + "Could not determine the package name. Enclosing element was: " + enclosingElement.kind + ) + return null + } + + val packageElement = enclosingElement as PackageElement + return packageElement.qualifiedName.toString() + } + + companion object { + private const val CONSUME_ANNOTATIONS = false + } +} diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000..57897c829 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..58fadd699 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index ade79d3ac..df1b6fad3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ +include ':library:external:realmfieldnameshelper' include ':matrix-sdk-android' From 12e8ad4bf455998ab956d0438f56a429f04d23f1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:37:51 +0100 Subject: [PATCH 4/6] Changelog for version v1.6.10 --- CHANGES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5e1ba9071..8b148d913 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md +Changes in Matrix-SDK v1.6.10 (2024-01-10) +========================================= + +Imported from Element 1.6.10. (https://github.com/vector-im/element-android/releases/tag/v1.6.10) + Changes in Matrix-SDK v1.5.30 (2023-04-11) ========================================= From d4aa5ed12a2c086aa0f8fd5e2d001d39bf229175 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:38:32 +0100 Subject: [PATCH 5/6] Add some .idea files --- .idea/compiler.xml | 4 +++- .idea/kotlinc.xml | 6 ++++++ .idea/migrations.xml | 10 ++++++++++ .idea/misc.xml | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/migrations.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8a4..a1edb3ab6 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,8 @@ - + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 000000000..0fc311313 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 000000000..f8051a6f9 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index ef61796f5..55c0ec2c4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + From 408e57f7a7d196a34e5d764ba9449a40765594a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 10 Jan 2024 16:22:21 +0100 Subject: [PATCH 6/6] Import v1.6.10 from Element Android --- .idea/misc.xml | 1 - dependencies.gradle | 27 +- dependencies_groups.gradle | 34 +- matrix-sdk-android/build.gradle | 23 +- .../assets/crypto_store_migration_16.realm | 3 + .../sdk/account/DeactivateAccountTest.kt | 10 +- .../android/sdk/common/CommonTestHelper.kt | 158 +- .../android/sdk/common/CryptoTestData.kt | 6 + .../android/sdk/common/CryptoTestHelper.kt | 426 +++- .../matrix/android/sdk/common/TestMatrix.kt | 6 - .../TestRoomDisplayNameFallbackProvider.kt | 2 + .../sdk/internal/crypto/CryptoStoreHelper.kt | 44 - .../sdk/internal/crypto/CryptoStoreTest.kt | 142 -- .../crypto/DecryptRedactedEventTest.kt | 2 +- .../internal/crypto/E2EShareKeysConfigTest.kt | 153 +- .../sdk/internal/crypto/E2eeConfigTest.kt | 50 +- .../sdk/internal/crypto/E2eeSanityTests.kt | 622 +++--- .../crypto/E2eeShareKeysHistoryTest.kt | 162 +- .../crypto/E2eeTestVerificationTestDirty.kt | 98 + .../sdk/internal/crypto/PreShareKeysTest.kt | 90 - .../sdk/internal/crypto/RoomShieldTest.kt | 133 ++ .../sdk/internal/crypto/UnwedgingTest.kt | 228 -- .../crypto/crosssigning/ExtensionsKtTest.kt | 1 + .../crypto/crosssigning/XSigningTest.kt | 140 +- .../crypto/gossiping/KeyShareTests.kt | 71 +- .../crypto/gossiping/WithHeldTests.kt | 82 +- .../crypto/keysbackup/BackupStateHelper.kt | 47 + .../keysbackup/KeysBackupScenarioData.kt | 3 +- .../crypto/keysbackup/KeysBackupTest.kt | 558 +++-- .../crypto/keysbackup/KeysBackupTestHelper.kt | 65 +- .../crypto/keysbackup/StateObserver.kt | 5 + .../crypto/replayattack/ReplayAttackTest.kt | 4 + .../sdk/internal/crypto/ssss/QuadSTests.kt | 2 +- ...icElementAndroidToElementRMigrationTest.kt | 147 ++ .../internal/crypto/verification/SASTest.kt | 611 ------ .../verification/SasVerificationTestHelper.kt | 116 ++ .../crypto/verification/VerificationTest.kt | 235 +++ .../crypto/verification/qrcode/QrCodeTest.kt | 251 --- .../verification/qrcode/SharedSecretTest.kt | 46 - .../verification/qrcode/VerificationTest.kt | 135 +- .../database/CryptoSanityMigrationTest.kt | 39 +- .../session/room/send/TestPermalinkService.kt | 4 + .../room/timeline/PollAggregationTest.kt | 28 +- .../sdk/session/space/SpaceCreationTest.kt | 4 +- .../sdk/session/space/SpaceHierarchyTest.kt | 2 +- .../java/org/matrix/android/sdk/api/Matrix.kt | 7 - .../android/sdk/api/MatrixConfiguration.kt | 3 + .../matrix/android/sdk/api/MatrixPatterns.kt | 2 +- .../android/sdk/api/auth/data/Credentials.kt | 4 +- .../sdk/api/auth/data/DiscoveryInformation.kt | 8 +- .../android/sdk/api/auth/data/WellKnown.kt | 6 + .../android/sdk/api/auth/login/LoginWizard.kt | 2 +- .../auth/registration/RegistrationWizard.kt | 2 +- .../android/sdk/api/crypto/CryptoConstants.kt | 3 + .../android/sdk/api/extensions/Strings.kt | 10 + .../sdk/api/listeners/StepProgressListener.kt | 1 + .../sdk/api/metrics/CryptoMetricPlugin.kt | 124 ++ .../RoomDisplayNameFallbackProvider.kt | 4 + .../android/sdk/api/rendezvous/Rendezvous.kt | 24 +- .../channels/ECDHRendezvousChannel.kt | 4 +- .../sdk/api/session/crypto/CryptoService.kt | 101 +- .../api/session/crypto/NewSessionListener.kt | 3 +- .../crosssigning/CrossSigningService.kt | 103 +- .../crypto/crosssigning/UserTrustResult.kt | 9 +- .../crypto/keysbackup/BackupRecoveryKey.kt | 76 + .../session/crypto/keysbackup/BackupUtils.kt} | 17 +- .../IBackupRecoveryKey.kt} | 33 +- .../crypto/keysbackup/KeysBackupService.kt | 118 +- .../keysbackup/MegolmBackupCreationInfo.kt | 4 +- .../keysbackup/SavedKeyBackupKeyInfo.kt | 2 +- .../session/crypto/model/CryptoRoomInfo.kt | 33 + .../crypto/model/ImportRoomKeysResult.kt | 4 +- .../crypto/model/MXEventDecryptionResult.kt | 11 +- .../session/crypto/model/MXUsersDevicesMap.kt | 6 +- .../crypto/model/OlmDecryptionResult.kt | 6 + ...onTransaction.kt => EVerificationState.kt} | 28 +- .../PendingVerificationRequest.kt | 67 +- .../QrCodeVerificationTransaction.kt | 22 +- .../verification/SasTransactionState.kt | 46 + .../SasVerificationTransaction.kt | 36 +- .../crypto/verification/VerificationEvent.kt | 42 + .../verification/VerificationService.kt | 97 +- .../verification/VerificationTransaction.kt | 20 +- .../verification/VerificationTxState.kt | 67 - .../sdk/api/session/events/model/Event.kt | 7 +- .../sdk/api/session/events/model/EventType.kt | 1 + .../homeserver/HomeServerCapabilities.kt | 16 +- .../session/permalinks/PermalinkService.kt | 11 + .../sdk/api/session/pushrules/Action.kt | 4 +- .../api/session/pushrules/rest/PushRule.kt | 4 +- .../sdk/api/session/room/RoomService.kt | 9 + .../session/room/UpdatableLivePageResult.kt | 1 + .../session/room/model/message/MessageType.kt | 4 +- .../MessageVerificationAcceptContent.kt | 64 - .../MessageVerificationCancelContent.kt | 28 +- .../message/MessageVerificationDoneContent.kt | 22 +- .../message/MessageVerificationKeyContent.kt | 26 +- .../message/MessageVerificationMacContent.kt | 28 +- .../MessageVerificationReadyContent.kt | 28 +- .../MessageVerificationRequestContent.kt | 15 +- .../MessageVerificationStartContent.kt | 28 +- .../room/model/relation/RelationService.kt | 2 +- .../sdk/api/session/room/send/SendService.kt | 4 +- .../session/room/timeline/TimelineEvent.kt | 40 +- .../SharedSecretStorageService.kt | 6 + .../sdk/api/session/signout/SignOutService.kt | 3 +- .../api/session/sync/model/SyncResponse.kt | 11 +- .../org/matrix/android/sdk/api/util/Base64.kt | 13 + .../android/sdk/api/util/ContentUtils.kt | 31 +- .../matrix/android/sdk/api/util/MimeTypes.kt | 1 + .../android/sdk/internal/auth/AuthModule.kt | 5 - .../auth/DefaultAuthenticationService.kt | 7 +- .../internal/auth/data/LoginFlowResponse.kt | 10 +- .../internal/auth/login/DefaultLoginWizard.kt | 4 +- .../sdk/internal/auth/version/Versions.kt | 13 +- .../coroutines/builder/FlowBuilders.kt | 46 + .../crypto/ComputeShieldForGroupUseCase.kt | 69 + .../sdk/internal/crypto/CryptoModule.kt | 30 +- .../crypto/CryptoSessionInfoProvider.kt | 82 +- .../crypto/DecryptRoomEventUseCase.kt | 45 + .../internal/crypto/DefaultCryptoService.kt | 1428 ------------- .../android/sdk/internal/crypto/Device.kt | 194 ++ .../sdk/internal/crypto/DeviceListManager.kt | 601 ------ .../crypto/EncryptEventContentUseCase.kt | 61 + .../internal/crypto/EnsureUsersKeysUseCase.kt | 62 + .../sdk/internal/crypto/EventDecryptor.kt | 259 +-- .../sdk/internal/crypto/FlowCollectors.kt | 113 + .../internal/crypto/GetRoomUserIdsUseCase.kt | 27 + .../internal/crypto/GetUserIdentityUseCase.kt | 98 + .../crypto/InboundGroupSessionStore.kt | 126 -- .../crypto/IncomingKeyRequestManager.kt | 465 ----- .../sdk/internal/crypto/MXOlmDevice.kt | 963 --------- .../crypto/MegolmSessionImportManager.kt | 78 + .../sdk/internal/crypto/MyDeviceInfoHolder.kt | 80 - .../sdk/internal/crypto/ObjectSigner.kt | 54 - .../android/sdk/internal/crypto/OlmMachine.kt | 957 +++++++++ .../sdk/internal/crypto/OlmSessionStore.kt | 160 -- .../internal/crypto/OneTimeKeysUploader.kt | 247 --- .../crypto/OutgoingKeyRequestManager.kt | 526 ----- .../PerSessionBackupQueryRateLimiter.kt | 20 +- .../crypto/PrepareToEncryptUseCase.kt | 187 ++ .../internal/crypto/RoomDecryptorProvider.kt | 106 - .../internal/crypto/RoomEncryptorsStore.kt | 60 - .../crypto/RustCrossSigningService.kt | 243 +++ .../sdk/internal/crypto/RustCryptoService.kt | 921 ++++++++ .../crypto/RustEncryptionConfiguration.kt | 35 + .../sdk/internal/crypto/SecretShareManager.kt | 293 +-- .../ShouldEncryptForInvitedMembersUseCase.kt | 29 + .../sdk/internal/crypto/UserIdentities.kt | 263 +++ .../EnsureOlmSessionsForDevicesAction.kt | 171 -- .../EnsureOlmSessionsForUsersAction.kt | 50 - .../actions/MegolmSessionDataImporter.kt | 126 -- .../crypto/actions/MessageEncrypter.kt | 89 - .../actions/SetDeviceVerificationAction.kt | 55 - .../crypto/algorithms/IMXDecrypting.kt | 47 - .../crypto/algorithms/IMXEncrypting.kt | 39 - .../crypto/algorithms/IMXGroupEncryption.kt | 54 - .../algorithms/megolm/MXMegolmDecryption.kt | 365 ---- .../megolm/MXMegolmDecryptionFactory.kt | 52 - .../algorithms/megolm/MXMegolmEncryption.kt | 611 ------ .../megolm/MXMegolmEncryptionFactory.kt | 68 - .../megolm/MXOutboundSessionInfo.kt | 77 - .../algorithms/megolm/SharedWithHelper.kt | 43 - .../megolm/UnRequestedForwardManager.kt | 132 +- .../crypto/algorithms/olm/MXOlmDecryption.kt | 269 --- .../crypto/algorithms/olm/MXOlmEncryption.kt | 80 - .../algorithms/olm/MXOlmEncryptionFactory.kt | 46 - .../sdk/internal/crypto/api/CryptoApi.kt | 8 +- .../crypto/crosssigning/ComputeTrustTask.kt | 93 - .../crypto/crosssigning/CrossSigningOlm.kt | 91 - .../DefaultCrossSigningService.kt | 833 -------- .../crypto/crosssigning/Extensions.kt | 42 - .../crypto/crosssigning/UpdateTrustWorker.kt | 315 +-- .../UpdateTrustWorkerDataRepository.kt | 9 +- .../keysbackup/DefaultKeysBackupService.kt | 1561 -------------- .../keysbackup/KeysBackupStateManager.kt | 12 +- .../crypto/keysbackup/RustKeyBackupService.kt | 978 +++++++++ .../model/rest/DefaultKeysAlgorithmAndData.kt | 37 + .../tasks/GetKeysBackupLastVersionTask.kt | 2 +- .../tasks/GetKeysBackupVersionTask.kt | 2 +- .../keysbackup/tasks/StoreSessionsDataTask.kt | 2 +- .../tasks/UpdateKeysBackupVersionTask.kt | 2 +- .../model/rest/KeyVerificationAccept.kt | 90 - .../model/rest/KeyVerificationCancel.kt | 57 - .../crypto/model/rest/KeyVerificationDone.kt | 32 - .../crypto/model/rest/KeyVerificationKey.kt | 48 - .../crypto/model/rest/KeyVerificationMac.kt | 42 - .../crypto/model/rest/KeyVerificationReady.kt | 34 - .../model/rest/KeyVerificationRequest.kt | 35 - .../crypto/model/rest/KeyVerificationStart.kt | 45 - .../crypto/model/rest/KeysClaimResponse.kt | 5 + .../crypto/model/rest/KeysUploadBody.kt | 2 +- .../network/OutgoingRequestsProcessor.kt | 197 ++ .../internal/crypto/network/RequestSender.kt | 328 +++ .../DefaultSharedSecretStorageService.kt | 5 + .../crypto/store/IMXCommonCryptoStore.kt | 156 ++ .../internal/crypto/store/IMXCryptoStore.kt | 602 ------ .../internal/crypto/store/RustCryptoStore.kt | 388 ++++ .../crypto/store/db/RealmCryptoStore.kt | 1856 ----------------- .../store/db/RealmCryptoStoreMigration.kt | 12 +- .../db/RustMigrationInfoProvider.kt} | 23 +- .../store/db/mapper/CryptoRoomInfoMapper.kt | 38 + .../store/db/migration/MigrateCryptoTo021.kt | 43 + .../store/db/migration/MigrateCryptoTo022.kt | 49 + .../rust/ExtractMigrationDataFailure.kt} | 14 +- .../rust/ExtractMigrationDataUseCase.kt | 96 + .../store/db/migration/rust/RealmToMigrate.kt | 332 +++ .../crypto/store/db/model/CryptoRoomEntity.kt | 7 +- .../ClaimOneTimeKeysForUsersDeviceTask.kt | 29 +- .../crypto/tasks/DownloadKeysForUsersTask.kt | 2 +- .../internal/crypto/tasks/EncryptEventTask.kt | 76 +- .../tasks/InitializeCrossSigningTask.kt | 190 -- .../internal/crypto/tasks/RedactEventTask.kt | 10 +- .../internal/crypto/tasks/SendEventTask.kt | 1 + .../internal/crypto/tasks/SendToDeviceTask.kt | 5 +- .../tasks/SendVerificationMessageTask.kt | 16 +- .../internal/crypto/tasks/UploadKeysTask.kt | 24 +- .../crypto/tasks/UploadSignaturesTask.kt | 7 +- .../crypto/tasks/UploadSigningKeysTask.kt | 2 +- ...comingSASDefaultVerificationTransaction.kt | 265 --- ...tgoingSASDefaultVerificationTransaction.kt | 257 --- .../DefaultVerificationService.kt | 1532 -------------- .../DefaultVerificationTransaction.kt | 118 -- .../verification/RustVerificationService.kt | 386 ++++ .../SASDefaultVerificationTransaction.kt | 428 ---- .../crypto/verification/SasVerification.kt | 240 +++ .../crypto/verification/VerificationEmoji.kt | 52 + .../crypto/verification/VerificationInfo.kt | 33 - .../verification/VerificationInfoAccept.kt | 83 - .../verification/VerificationInfoCancel.kt | 45 - .../verification/VerificationInfoDone.kt | 26 - .../verification/VerificationInfoKey.kt | 45 - .../verification/VerificationInfoMac.kt | 53 - .../verification/VerificationInfoReady.kt | 54 - .../verification/VerificationInfoRequest.kt | 52 - .../verification/VerificationInfoStart.kt | 125 -- .../VerificationListenersHolder.kt | 71 + .../VerificationMessageProcessor.kt | 141 -- .../verification/VerificationRequest.kt | 398 ++++ .../verification/VerificationTransport.kt | 128 -- .../VerificationTransportRoomMessage.kt | 302 --- ...VerificationTransportRoomMessageFactory.kt | 50 - .../VerificationTransportToDevice.kt | 287 --- .../VerificationTransportToDeviceFactory.kt | 35 - .../verification/VerificationsProvider.kt | 61 + .../DefaultQrCodeVerificationTransaction.kt | 284 --- .../crypto/verification/qrcode/Extensions.kt | 127 -- .../crypto/verification/qrcode/QrCodeData.kt | 105 - .../verification/qrcode/QrCodeVerification.kt | 231 ++ .../database/RealmSessionStoreMigration.kt | 8 +- .../database/helper/ChunkEntityHelper.kt | 12 +- .../database/helper/ThreadSummaryHelper.kt | 2 +- .../internal/database/mapper/EventMapper.kt | 2 + .../mapper/HomeServerCapabilitiesMapper.kt | 4 +- .../database/migration/MigrateSessionTo051.kt | 1 - .../database/migration/MigrateSessionTo052.kt | 30 + .../database/migration/MigrateSessionTo053.kt | 30 + .../database/migration/MigrateSessionTo054.kt | 31 + .../internal/database/model/EventEntity.kt | 6 +- .../model/HomeServerCapabilitiesEntity.kt | 2 + .../android/sdk/internal/di/FileQualifiers.kt | 4 + .../sdk/internal/di/WorkManagerProvider.kt | 22 +- .../legacy/DefaultLegacySessionImporter.kt | 228 -- .../sdk/internal/legacy/riot/Credentials.java | 110 - .../sdk/internal/legacy/riot/Fingerprint.java | 94 - .../riot/HomeServerConnectionConfig.java | 674 ------ .../internal/legacy/riot/LoginStorage.java | 206 -- .../sdk/internal/legacy/riot/WellKnown.kt | 96 - .../legacy/riot/WellKnownBaseConfig.kt | 37 - .../legacy/riot/WellKnownPreferredConfig.kt | 37 - .../android/sdk/internal/network/Request.kt | 2 + .../network/parsing/CheckNumberType.kt | 13 +- .../sdk/internal/session/DefaultSession.kt | 5 +- .../session/DefaultToDeviceService.kt | 16 +- .../session/MigrateEAtoEROperation.kt | 78 + .../sdk/internal/session/SessionComponent.kt | 7 +- .../sdk/internal/session/SessionModule.kt | 26 +- .../internal/session/call/MxCallFactory.kt | 6 +- .../homeserver/GetCapabilitiesResult.kt | 9 +- .../GetHomeServerCapabilitiesTask.kt | 23 +- .../permalinks/DefaultPermalinkService.kt | 6 + .../session/pushers/DefaultPushersService.kt | 6 +- .../session/room/DefaultRoomService.kt | 6 + .../session/room/EventEditValidator.kt | 28 +- .../session/room/RoomAvatarResolver.kt | 23 +- .../sdk/internal/session/room/RoomGetter.kt | 8 +- .../room/create/CreateLocalRoomTask.kt | 9 +- .../room/create/CreateRoomBodyBuilder.kt | 7 +- .../room/crypto/DefaultRoomCryptoService.kt | 5 +- .../room/membership/LoadRoomMembersTask.kt | 10 +- .../membership/RoomDisplayNameResolver.kt | 5 +- .../room/notification/RoomPushRuleMapper.kt | 4 +- .../session/room/read/DefaultReadService.kt | 7 +- .../session/room/read/SetReadMarkersTask.kt | 5 +- .../room/relation/DefaultRelationService.kt | 2 +- .../session/room/relation/EventEditor.kt | 3 +- .../threads/FetchThreadSummariesTask.kt | 4 +- .../threads/FetchThreadTimelineTask.kt | 8 +- .../session/room/send/DefaultSendService.kt | 16 +- .../room/send/LocalEchoEventFactory.kt | 20 +- .../session/room/send/RedactEventWorker.kt | 4 +- .../room/send/model/EventRedactBody.kt | 9 +- .../room/send/queue/EventSenderProcessor.kt | 4 +- .../queue/EventSenderProcessorCoroutine.kt | 12 +- .../session/room/send/queue/QueueMemento.kt | 2 +- .../room/send/queue/QueuedTaskFactory.kt | 4 +- .../room/send/queue/RedactQueuedTask.kt | 4 +- .../room/summary/RoomSummaryDataSource.kt | 22 +- .../room/summary/RoomSummaryUpdater.kt | 36 +- .../session/room/timeline/GetEventTask.kt | 6 +- .../timeline/RoomSummaryEventDecryptor.kt | 133 ++ .../session/room/timeline/TimelineChunk.kt | 22 +- .../room/timeline/TimelineEventDecryptor.kt | 7 +- .../timeline/TimelineSendEventWorkCommon.kt | 6 +- .../session/signout/DefaultSignOutService.kt | 9 +- .../internal/session/signout/SignOutTask.kt | 7 +- .../session/sync/DefaultSyncService.kt | 16 +- .../session/sync/SyncResponseHandler.kt | 93 +- .../SyncResponsePostTreatmentAggregator.kt | 3 +- .../session/sync/handler/CryptoSyncHandler.kt | 137 -- .../sync/handler/ShieldSummaryUpdater.kt | 60 + ...cResponsePostTreatmentAggregatorHandler.kt | 16 +- .../handler/UserAccountDataSyncHandler.kt | 3 +- .../sync/handler/room/RoomSyncHandler.kt | 50 +- .../internal/session/sync/job/SyncWorker.kt | 10 +- .../workmanager/DefaultWorkManagerConfig.kt | 41 + .../workmanager/WorkManagerConfig.kt} | 9 +- .../src/main/res/values-ar/strings_sas.xml | 8 +- .../src/main/res/values-cs/strings_sas.xml | 2 +- .../src/main/res/values-es/strings_sas.xml | 2 +- .../src/main/res/values-fa/strings_sas.xml | 68 + .../src/main/res/values-id/strings_sas.xml | 68 + .../src/main/res/values-ja/strings_sas.xml | 4 +- .../src/main/res/values-pt/strings_sas.xml | 68 + .../src/main/res/values-sk/strings_sas.xml | 52 +- .../src/main/res/values-sq/strings_sas.xml | 67 + .../src/main/res/values-vi/strings_sas.xml | 68 + .../main/res/values-zh-rTW/strings_sas.xml | 68 + .../crypto/DefaultSendToDeviceTaskTest.kt | 4 +- .../sdk/internal/crypto/MoshiNumbersAsInt.kt | 77 + .../crypto/UnRequestedKeysManagerTest.kt | 250 --- .../pushers/DefaultPushersServiceTest.kt | 3 + .../session/room/EventEditValidatorTest.kt | 44 +- .../sdk/test/fakes/FakeWorkManagerConfig.kt} | 15 +- .../sdk/test/fixtures/CredentialsFixture.kt | 2 +- 345 files changed, 12521 insertions(+), 23895 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/assets/crypto_store_migration_16.realm delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/RoomShieldTest.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupStateHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal/crypto/verification/qrcode/SharedSecret.kt => api/session/crypto/keysbackup/BackupUtils.kt} (56%) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/{verification/IncomingSasVerificationTransaction.kt => keysbackup/IBackupRecoveryKey.kt} (53%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoRoomInfo.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/{OutgoingSasVerificationTransaction.kt => EVerificationState.kt} (62%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasTransactionState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt delete mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt delete mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt delete mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionImportManager.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt delete mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt create mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustEncryptionConfiguration.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/DefaultKeysAlgorithmAndData.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/{algorithms/olm/MXOlmDecryptionFactory.kt => store/db/RustMigrationInfoProvider.kt} (53%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CryptoRoomInfoMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo021.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/{legacy/riot/WellKnownManagerConfig.kt => crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt} (62%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo053.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java delete mode 100755 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/{crypto/GossipRequestType.kt => session/workmanager/WorkManagerConfig.kt} (74%) create mode 100644 matrix-sdk-android/src/main/res/values-fa/strings_sas.xml create mode 100644 matrix-sdk-android/src/main/res/values-id/strings_sas.xml create mode 100644 matrix-sdk-android/src/main/res/values-pt/strings_sas.xml create mode 100644 matrix-sdk-android/src/main/res/values-sq/strings_sas.xml create mode 100644 matrix-sdk-android/src/main/res/values-vi/strings_sas.xml create mode 100644 matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/MoshiNumbersAsInt.kt delete mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt rename matrix-sdk-android/src/{main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt => test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt} (65%) diff --git a/.idea/misc.xml b/.idea/misc.xml index 55c0ec2c4..0f866762d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/dependencies.gradle b/dependencies.gradle index 56b81653c..79368a897 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,23 +11,23 @@ def gradle = "7.4.2" def kotlin = "1.8.10" def kotlinCoroutines = "1.6.4" def dagger = "2.45" -def firebaseBom = "31.4.0" -def appDistribution = "16.0.0-beta06" +def firebaseBom = "32.0.0" +def appDistribution = "16.0.0-beta08" def retrofit = "2.9.0" def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.188.0" +def flipper = "0.190.0" def epoxy = "5.0.0" -def mavericks = "3.0.2" +def mavericks = "3.0.7" def glide = "4.15.1" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" def vanniktechEmoji = "0.16.0" -def sentry = "6.17.0" +def sentry = "6.18.1" // Use 1.6.0 alpha to fix issue with test -def fragment = "1.6.0-alpha08" +def fragment = "1.6.0-beta01" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.5.1" @@ -47,17 +47,17 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'activity' : "androidx.activity:activity-ktx:1.7.0", + 'activity' : "androidx.activity:activity-ktx:1.7.2", 'appCompat' : "androidx.appcompat:appcompat:1.6.1", 'biometric' : "androidx.biometric:biometric:1.1.0", - 'core' : "androidx.core:core-ktx:1.9.0", + 'core' : "androidx.core:core-ktx:1.10.1", 'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", - 'work' : "androidx.work:work-runtime-ktx:2.8.0", + 'work' : "androidx.work:work-runtime-ktx:2.8.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", 'junit' : "androidx.test.ext:junit:1.1.5", @@ -80,13 +80,13 @@ ext.libs = [ 'transition' : "androidx.transition:transition:1.4.1", ], google : [ - 'material' : "com.google.android.material:material:1.8.0", + 'material' : "com.google.android.material:material:1.9.0", 'firebaseBom' : "com.google.firebase:firebase-bom:$firebaseBom", 'messaging' : "com.google.firebase:firebase-messaging", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.8" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.11" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:1.2.2" + 'wysiwyg' : "io.element.android:wysiwyg:2.24.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -169,9 +169,10 @@ ext.libs = [ 'sentryAndroid' : "io.sentry:sentry-android:$sentry" ], tests : [ - 'kluent' : "org.amshove.kluent:kluent-android:1.72", + 'kluent' : "org.amshove.kluent:kluent-android:1.73", 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", 'junit' : "junit:junit:4.13.2", + 'robolectric' : "org.robolectric:robolectric:4.9", ] ] diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 67893b50c..a6ddd5c79 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -1,21 +1,21 @@ ext.groups = [ - jitpack : [ + jitpack : [ regex: [ ], group: [ 'com.github.Armen101', 'com.github.chrisbanes', + 'com.github.element-hq', 'com.github.hyuwah', 'com.github.jetradarmobile', 'com.github.MatrixFrog', 'com.github.tapadoo', 'com.github.UnifiedPush', - 'com.github.vector-im', 'com.github.yalantis', 'com.github.Zhuinden', ] ], - jitsi : [ + jitsi : [ regex: [ ], group: [ @@ -24,7 +24,7 @@ ext.groups = [ 'org.webkit', ] ], - google : [ + google : [ regex: [ 'androidx\\..*', 'com\\.android\\.tools\\..*', @@ -44,6 +44,13 @@ ext.groups = [ group: [ ] ], + mavenSnapshots: [ + regex: [ + ], + group: [ + 'org.matrix.rustcomponents' + ] + ], mavenCentral: [ regex: [ ], @@ -108,6 +115,7 @@ ext.groups = [ 'com.linkedin.dexmaker', 'com.mapbox.mapboxsdk', 'com.nulab-inc', + 'com.otaliastudios', 'com.otaliastudios.opengl', 'com.parse.bolts', 'com.pinterest', @@ -182,6 +190,7 @@ ext.groups = [ 'org.codehaus.groovy', 'org.codehaus.mojo', 'org.codehaus.woodstox', + 'org.conscrypt', 'org.eclipse.ee4j', 'org.ec4j.core', 'org.freemarker', @@ -196,6 +205,7 @@ ext.groups = [ 'org.jetbrains.kotlin', 'org.jetbrains.kotlinx', 'org.jetbrains.trove4j', + 'org.jitsi', 'org.json', 'org.jsoup', 'org.junit', @@ -204,6 +214,7 @@ ext.groups = [ 'org.jvnet.staxex', 'org.maplibre.gl', 'org.matrix.android', + 'org.matrix.rustcomponents', 'org.mockito', 'org.mongodb', 'org.objenesis', @@ -212,6 +223,7 @@ ext.groups = [ 'org.ow2.asm', 'org.ow2.asm', 'org.reactivestreams', + 'org.robolectric', 'org.slf4j', 'org.sonatype.oss', 'org.testng', @@ -223,18 +235,4 @@ ext.groups = [ 'xml-apis', ] ], - jcenter : [ - regex: [ - ], - group: [ - 'com.amulyakhare', - 'com.otaliastudios', - 'com.yqritc', - // https://github.com/cmelchior/realmfieldnameshelper/issues/42 - 'dk.ilios', - 'im.dlg', - 'me.dm7.barcodescanner', - 'me.gujun.android', - ] - ] ] diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 187f2acdc..ed2f0c5ab 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath "io.realm:realm-gradle-plugin:10.11.1" + classpath "io.realm:realm-gradle-plugin:10.16.0" } } @@ -78,7 +78,7 @@ android { testOptions { // Comment to run on Android 12 -// execution 'ANDROIDX_TEST_ORCHESTRATOR' + execution 'ANDROIDX_TEST_ORCHESTRATOR' } buildTypes { @@ -127,6 +127,7 @@ android { java.srcDirs += "src/sharedTest/java" } } + } static def gitRevision() { @@ -144,12 +145,23 @@ static def gitRevisionDate() { return cmd.execute().text.trim() } +configurations.all { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid + +// implementation(name: 'crypto-android-release', ext: 'aar') + implementation 'net.java.dev.jna:jna:5.13.0@aar' + + // implementation libs.androidx.appCompat implementation libs.androidx.core + implementation libs.androidx.lifecycleLivedata + // Lifecycle implementation libs.androidx.lifecycleCommon implementation libs.androidx.lifecycleProcess @@ -163,7 +175,7 @@ dependencies { // - https://github.com/square/okhttp/issues/3278 // - https://github.com/square/okhttp/issues/4455 // - https://github.com/square/okhttp/issues/3146 - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.11.0")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' @@ -179,7 +191,7 @@ dependencies { // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' - kapt 'dk.ilios:realmfieldnameshelper:2.0.0' + kapt project(":library:external:realmfieldnameshelper") // Shared Preferences implementation libs.androidx.preferenceKtx @@ -206,6 +218,9 @@ dependencies { implementation libs.google.phonenumber + implementation("org.matrix.rustcomponents:crypto-android:0.3.16") +// api project(":library:rustCrypto") + testImplementation libs.tests.junit // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 testImplementation libs.mockk.mockk diff --git a/matrix-sdk-android/src/androidTest/assets/crypto_store_migration_16.realm b/matrix-sdk-android/src/androidTest/assets/crypto_store_migration_16.realm new file mode 100644 index 000000000..4995bfc4a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/assets/crypto_store_migration_16.realm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59b4957aa2f9cdc17b14ec8546e144537fac9dee050c6eb173f56fa8602c2736 +size 2097152 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index bb5618b81..61bd1b42e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -64,9 +64,15 @@ class DeactivateAccountTest : InstrumentedTest { // Test the error assertTrue( + "Unexpected deactivated error $throwable", throwable is Failure.ServerError && - throwable.error.code == MatrixError.M_USER_DEACTIVATED && - throwable.error.message == "This account has been deactivated" + ( + (throwable.error.code == MatrixError.M_USER_DEACTIVATED && + throwable.error.message == "This account has been deactivated") || + // Workaround for a breaking change on synapse to fix CI + // https://github.com/matrix-org/synapse/issues/15747 + throwable.error.code == MatrixError.M_FORBIDDEN + ) ) // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index eeb2def58..983e00b9e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -20,6 +20,7 @@ import android.content.Context import android.net.Uri import android.util.Log import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,6 +45,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure +import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.send.SendState @@ -82,7 +84,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } @OptIn(ExperimentalCoroutinesApi::class) - internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { + internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { val testHelper = CommonTestHelper(context, cryptoConfig) val cryptoTestHelper = CryptoTestHelper(testHelper) return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { @@ -97,6 +99,23 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } } } + + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runLongCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { + val testHelper = CommonTestHelper(context, cryptoConfig) + val cryptoTestHelper = CryptoTestHelper(testHelper) + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis * 4) { + try { + withContext(Dispatchers.Default) { + block(cryptoTestHelper, testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } + } + } + } } internal val matrix: TestMatrix @@ -181,6 +200,110 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: return sentEvents } + suspend fun sendMessageInRoom(room: Room, text: String): String { + Log.v("#E2E TEST", "sendMessageInRoom room:${room.roomId} <$text>") + room.sendService().sendTextMessage(text) + + val timeline = room.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + + val messageSent = CompletableDeferred() + timeline.addListener(object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } + Log.v("#E2E TEST", "Timeline snapshot is $message") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + if (decryptedMsg != null) { + timeline.dispose() + messageSent.complete(decryptedMsg.eventId) + } + } + }) + return messageSent.await().also { + Log.v("#E2E TEST", "Message <${text}> sent and synced with id $it") + } + // return withTimeout(TestConstants.timeOutMillis) { messageSent.await() } + } + + suspend fun ensureMessage(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)) { + Log.v("#E2E TEST", "ensureMessage room:${room.roomId} <$eventId>") + val timeline = room.timelineService().createTimeline(null, TimelineSettings(60, buildReadReceipts = false)) + + // check if not already there? + val existing = withContext(Dispatchers.Main) { + room.getTimelineEvent(eventId) + } + if (existing != null && block(existing)) return Unit.also { + Log.v("#E2E TEST", "Already received") + } + + val messageSent = CompletableDeferred() + + timeline.addListener(object : Timeline.Listener { + override fun onNewTimelineEvents(eventIds: List) { + Log.v("#E2E TEST", "onNewTimelineEvents snapshot is $eventIds") + } + + override fun onTimelineUpdated(snapshot: List) { + val success = timeline.getSnapshot() + // .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { + "${it.eventId}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}" + } + Log.v("#E2E TEST", "Timeline snapshot is $message") + } + .firstOrNull { it.eventId == eventId } + ?.let { + block(it) + } ?: false + if (success) { + messageSent.complete(Unit) + timeline.dispose() + } + } + }) + + timeline.start() + + return messageSent.await() + // withTimeout(TestConstants.timeOutMillis) { + // messageSent.await() + // } + } + + fun ensureMessagePromise(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)): CompletableDeferred { + val timeline = room.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + val messageSent = CompletableDeferred() + timeline.addListener(object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { + val success = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { + "${it.root.type}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}" + } + Log.v("#E2E TEST", "Promise Timeline snapshot is $message") + } + .firstOrNull { it.eventId == eventId } + ?.let { + block(it) + } ?: false + if (success) { + messageSent.complete(Unit) + timeline.dispose() + } + } + }) + return messageSent + } + /** * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ @@ -239,18 +362,18 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { - retryPeriodically { + retryWithBackoff { val roomSummary = otherSession.getRoomSummary(roomID) (roomSummary != null && roomSummary.membership == Membership.INVITE).also { if (it) { - Log.v("# TEST", "${otherSession.myUserId} can see the invite") + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite") } } } // not sure why it's taking so long :/ wrapWithTimeout(90_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") + Log.v("#E2E TEST", "${otherSession.myUserId.take(10)} tries to join room $roomID") try { otherSession.roomService().joinRoom(roomID) } catch (ex: JoinRoomFailure.JoinedWithTimeout) { @@ -259,7 +382,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - retryPeriodically { + retryWithBackoff { val roomSummary = otherSession.getRoomSummary(roomID) roomSummary != null && roomSummary.membership == Membership.JOIN } @@ -432,6 +555,31 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: } } + private val backoff = listOf(60L, 75L, 100L, 300L, 300L, 500L, 1_000L, 1_000L, 1_500L, 1_500L, 3_000L) + suspend fun retryWithBackoff( + timeout: Long = TestConstants.timeOutMillis, + // we use on fail to let caller report a proper error that will show nicely in junit test result with correct line + // just call fail with your message + onFail: (() -> Unit)? = null, + predicate: suspend () -> Boolean, + ) { + var backoffTry = 0 + val now = System.currentTimeMillis() + while (!predicate()) { + Timber.v("## retryWithBackoff Trial nb $backoffTry") + withContext(Dispatchers.IO) { + delay(backoff[backoffTry]) + } + backoffTry++ + if (backoffTry >= backoff.size) backoffTry = 0 + if (System.currentTimeMillis() - now > timeout) { + Timber.v("## retryWithBackoff Trial fail") + onFail?.invoke() + return + } + } + } + suspend fun waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { return wrapWithTimeout(timeout) { suspendCoroutine { continuation -> diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 8cd5bee56..09fd22ff1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -37,4 +37,10 @@ data class CryptoTestData( testHelper.signOutAndClose(it) } } + + suspend fun initializeCrossSigning(testHelper: CryptoTestHelper) { + sessions.forEach { + testHelper.initializeCrossSigning(it) + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 74292daf1..c1bd5da6a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -17,6 +17,13 @@ package org.matrix.android.sdk.common import android.util.Log +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.launch import org.amshove.kluent.fail import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -33,18 +40,23 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import org.matrix.android.sdk.api.session.crypto.verification.dbgState +import org.matrix.android.sdk.api.session.crypto.verification.getRequest +import org.matrix.android.sdk.api.session.crypto.verification.getTransaction import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.getRoomSummary +import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -52,7 +64,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.KeyRef -import org.matrix.android.sdk.api.util.toBase64NoPadding import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -121,6 +132,82 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } + suspend fun inviteNewUsersAndWaitForThemToJoin(session: Session, roomId: String, usernames: List): List { + val newSessions = usernames.map { username -> + testHelper.createAccount(username, SessionTestParams(true)).also { + if (it.cryptoService().supportsDisablingKeyGossiping()) { + it.cryptoService().enableKeyGossiping(false) + } + } + } + + val room = session.getRoom(roomId)!! + + Log.v("#E2E TEST", "accounts for ${usernames.joinToString(",") { it.take(10) }} created") + // we want to invite them in the room + newSessions.forEach { newSession -> + Log.v("#E2E TEST", "${session.myUserId.take(10)} invites ${newSession.myUserId.take(10)}") + room.membershipService().invite(newSession.myUserId) + } + + // All user should accept invite + newSessions.forEach { newSession -> + waitForAndAcceptInviteInRoom(newSession, roomId) + Log.v("#E2E TEST", "${newSession.myUserId.take(10)} joined room $roomId") + } + ensureMembersHaveJoined(session, newSessions, roomId) + return newSessions + } + + private suspend fun ensureMembersHaveJoined(session: Session, invitedUserSessions: List, roomId: String) { + testHelper.retryWithBackoff( + onFail = { + fail("Members ${invitedUserSessions.map { it.myUserId.take(10) }} should have join from the pov of ${session.myUserId.take(10)}") + } + ) { + invitedUserSessions.map { invitedUserSession -> + session.roomService().getRoomMember(invitedUserSession.myUserId, roomId)?.membership?.also { + Log.v("#E2E TEST", "${invitedUserSession.myUserId.take(10)} membership is $it") + } + }.all { + it == Membership.JOIN + } + } + } + + private suspend fun waitForAndAcceptInviteInRoom(session: Session, roomId: String) { + testHelper.retryWithBackoff( + onFail = { + fail("${session.myUserId} cannot see the invite from ${session.myUserId.take(10)}") + } + ) { + val roomSummary = session.getRoomSummary(roomId) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${session.myUserId.take(10)} can see the invite from ${roomSummary?.inviterId}") + } + } + } + + // not sure why it's taking so long :/ + Log.v("#E2E TEST", "${session.myUserId.take(10)} tries to join room $roomId") + try { + session.roomService().joinRoom(roomId) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after + } + + Log.v("#E2E TEST", "${session.myUserId} waiting for join echo ...") + testHelper.retryWithBackoff( + onFail = { + fail("${session.myUserId.take(10)} cannot see the join echo for ${roomId}") + } + ) { + val roomSummary = session.getRoomSummary(roomId) + roomSummary != null && roomSummary.membership == Membership.JOIN + } + } + /** * @return Alice and Bob sessions */ @@ -137,37 +224,22 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! // Alice sends a message - testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1).first().eventId.let { sentEventId -> - // ensure bob got it - ensureEventReceived(aliceRoomId, sentEventId, bobSession, true) - } + ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromAlicePOV, messagesFromAlice[0]), bobSession, true) // Bob send 3 messages - testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1).first().eventId.let { sentEventId -> - // ensure alice got it - ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true) - } - - testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1).first().eventId.let { sentEventId -> - // ensure alice got it - ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true) - } - testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1).first().eventId.let { sentEventId -> - // ensure alice got it - ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true) + for (msg in messagesFromBob) { + ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromBobPOV, msg), aliceSession, true) } // Alice sends a message - testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1).first().eventId.let { sentEventId -> - // ensure bob got it - ensureEventReceived(aliceRoomId, sentEventId, bobSession, true) - } + ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromAlicePOV, messagesFromAlice[1]), bobSession, true) return cryptoTestData } private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) + Log.d("#E2E", "ensureEventReceived $eventId => ${timeLineEvent?.senderInfo?.userId}| ${timeLineEvent?.root?.getClearType()}") if (andCanDecrypt) { timeLineEvent != null && timeLineEvent.isEncrypted() && @@ -189,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return MegolmBackupCreationInfo( algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, authData = createFakeMegolmBackupAuthData(), - recoveryKey = "fake" + recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW") ) } @@ -221,7 +293,6 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { } suspend fun initializeCrossSigning(session: Session) { - testHelper.waitForCallback { session.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -234,9 +305,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { ) ) } - }, it - ) - } + }) } /** @@ -272,16 +341,13 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { ) // set up megolm backup - val creationInfo = testHelper.waitForCallback { - session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) - } - val version = testHelper.waitForCallback { - session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } + val creationInfo = session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null) + val version = session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo) + // Save it for gossiping session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) - extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> + creationInfo.recoveryKey.toBase64().let { secret -> ssssService.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, secret, @@ -291,82 +357,262 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { } suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + val scope = CoroutineScope(SupervisorJob()) + assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) val aliceVerificationService = alice.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService() - val localId = UUID.randomUUID().toString() - aliceVerificationService.requestKeyVerificationInDMs( - localId = localId, + val bobSeesVerification = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request != null) { + bobSeesVerification.complete(request) + return@collect cancel() + } + } + } + + val aliceReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + aliceVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request?.state == EVerificationState.Ready) { + aliceReady.complete(request) + return@collect cancel() + } + } + } + val bobReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request?.state == EVerificationState.Ready) { + bobReady.complete(request) + return@collect cancel() + } + } + } + + val requestID = aliceVerificationService.requestKeyVerificationInDMs( methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), otherUserId = bob.myUserId, roomId = roomId ).transactionId - testHelper.retryPeriodically { - bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { - it.requestInfo?.fromDevice == alice.sessionParams.deviceId - } != null - } - val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { - it.requestInfo?.fromDevice == alice.sessionParams.deviceId + bobSeesVerification.await() + bobVerificationService.readyPendingVerification( + listOf(VerificationMethod.SAS), + alice.myUserId, + requestID + ) + aliceReady.await() + bobReady.await() + + val bobCode = CompletableDeferred() + + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val transaction = it.getTransaction() + Log.v("#E2E TEST", "#TEST flow ${bob.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}") + val tx = transaction as? SasVerificationTransaction + if (tx?.state() == SasTransactionState.SasShortCodeReady) { + Log.v("#E2E TEST", "COMPLETE BOB CODE") + bobCode.complete(tx) + return@collect cancel() + } + if (it.getRequest()?.state == EVerificationState.Cancelled) { + Log.v("#E2E TEST", "EXCEPTION BOB CODE") + bobCode.completeExceptionally(AssertionError("Request as been cancelled")) + return@collect cancel() + } + } } - bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, incomingRequest.transactionId!!) - var requestID: String? = null - // wait for it to be readied - testHelper.retryPeriodically { - val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) - .firstOrNull { it.localId == localId } - if (outgoingRequest?.isReady == true) { - requestID = outgoingRequest.transactionId!! - true - } else { - false - } + val aliceCode = CompletableDeferred() + + scope.launch(Dispatchers.IO) { + aliceVerificationService.requestEventFlow() + .cancellable() + .collect { + val transaction = it.getTransaction() + Log.v("#E2E TEST", "#TEST flow ${alice.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}") + val tx = transaction as? SasVerificationTransaction + if (tx?.state() == SasTransactionState.SasShortCodeReady) { + Log.v("#E2E TEST", "COMPLETE ALICE CODE") + aliceCode.complete(tx) + return@collect cancel() + } + if (it.getRequest()?.state == EVerificationState.Cancelled) { + Log.v("#E2E TEST", "EXCEPTION ALICE CODE") + aliceCode.completeExceptionally(AssertionError("Request as been cancelled")) + return@collect cancel() + } + } } - aliceVerificationService.beginKeyVerificationInDMs( + Log.v("#E2E TEST", "#TEST let alice start the verification") + val id = aliceVerificationService.startKeyVerification( VerificationMethod.SAS, - requestID!!, - roomId, bob.myUserId, - bob.sessionParams.credentials.deviceId!! + requestID, ) + Log.v("#E2E TEST", "#TEST alice started: $id") + + val bobTx = bobCode.await() + val aliceTx = aliceCode.await() + Log.v("#E2E TEST", "#TEST Alice code ${aliceTx.getDecimalCodeRepresentation()}") + Log.v("#E2E TEST", "#TEST Bob code ${bobTx.getDecimalCodeRepresentation()}") + assertEquals("SAS code do not match", aliceTx.getDecimalCodeRepresentation()!!, bobTx.getDecimalCodeRepresentation()) + + val aliceDone = CompletableDeferred() + scope.launch(Dispatchers.IO) { + aliceVerificationService.requestEventFlow() + .cancellable() + .collect { + val transaction = it.getTransaction() + Log.v("#E2E TEST", "#TEST flow ${alice.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}") + + val request = it.getRequest() + Log.v("#E2E TEST", "#TEST flow request ${alice.myUserId.take(5)} ${request?.transactionId}|${request?.state}") + if (request?.state == EVerificationState.Done || request?.state == EVerificationState.WaitingForDone) { + aliceDone.complete(Unit) + return@collect cancel() + } + } + } + val bobDone = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val transaction = it.getTransaction() + Log.v("#E2E TEST", "#TEST flow ${bob.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}") + + val request = it.getRequest() + Log.v("#E2E TEST", "#TEST flow request ${bob.myUserId.take(5)} ${request?.transactionId}|${request?.state}") + + if (request?.state == EVerificationState.Done || request?.state == EVerificationState.WaitingForDone) { + bobDone.complete(Unit) + return@collect cancel() + } + } + } - // we should reach SHOW SAS on both - var alicePovTx: OutgoingSasVerificationTransaction? = null - var bobPovTx: IncomingSasVerificationTransaction? = null + Log.v("#E2E TEST", "#TEST Bob confirm sas code") + bobTx.userHasVerifiedShortCode() + Log.v("#E2E TEST", "#TEST Alice confirm sas code") + aliceTx.userHasVerifiedShortCode() - testHelper.retryPeriodically { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction - Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") - alicePovTx?.state == VerificationTxState.ShortCodeReady + Log.v("#E2E TEST", "#TEST Waiting for Done..") + bobDone.await() + aliceDone.await() + Log.v("#E2E TEST", "#TEST .. ok") + + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId) + + scope.cancel() + } + + suspend fun verifyNewSession(oldDevice: Session, newDevice: Session) { + val scope = CoroutineScope(SupervisorJob()) + + assertTrue(oldDevice.cryptoService().crossSigningService().canCrossSign()) + + val verificationServiceOld = oldDevice.cryptoService().verificationService() + val verificationServiceNew = newDevice.cryptoService().verificationService() + + val oldSeesVerification = CompletableDeferred() + scope.launch(Dispatchers.IO) { + verificationServiceOld.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + Log.d("#E2E", "Verification request received: $request") + if (request != null) { + oldSeesVerification.complete(request) + return@collect cancel() + } + } } - // wait for alice to get the ready - testHelper.retryPeriodically { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") - if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() - } - bobPovTx?.state == VerificationTxState.ShortCodeReady + + val newReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + verificationServiceNew.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + Log.d("#E2E", "new state: ${request?.state}") + if (request?.state == EVerificationState.Ready) { + newReady.complete(request) + return@collect cancel() + } + } } - assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) + val txId = verificationServiceNew.requestSelfKeyVerification(listOf(VerificationMethod.SAS)).transactionId + oldSeesVerification.await() - bobPovTx!!.userHasVerifiedShortCode() - alicePovTx!!.userHasVerifiedShortCode() + verificationServiceOld.readyPendingVerification( + listOf(VerificationMethod.SAS), + oldDevice.myUserId, + txId + ) - testHelper.retryPeriodically { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) + newReady.await() + + val newConfirmed = CompletableDeferred() + scope.launch(Dispatchers.IO) { + verificationServiceNew.requestEventFlow() + .cancellable() + .collect { + val tx = it.getTransaction() as? SasVerificationTransaction + Log.d("#E2E", "new tx state: ${tx?.state()}") + if (tx?.state() == SasTransactionState.SasShortCodeReady) { + tx.userHasVerifiedShortCode() + newConfirmed.complete(Unit) + return@collect cancel() + } + } } + val oldConfirmed = CompletableDeferred() + scope.launch(Dispatchers.IO) { + verificationServiceOld.requestEventFlow() + .cancellable() + .collect { + val tx = it.getTransaction() as? SasVerificationTransaction + Log.d("#E2E", "old tx state: ${tx?.state()}") + if (tx?.state() == SasTransactionState.SasShortCodeReady) { + tx.userHasVerifiedShortCode() + oldConfirmed.complete(Unit) + return@collect cancel() + } + } + } + + verificationServiceNew.startKeyVerification(VerificationMethod.SAS, newDevice.myUserId, txId) + + newConfirmed.await() + oldConfirmed.await() + testHelper.retryPeriodically { - bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId) + oldDevice.cryptoService().crossSigningService().isCrossSigningVerified() } + + Log.d("#E2E", "New session is trusted") } suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { @@ -393,9 +639,9 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { suspend fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root - ?: return@retryPeriodically false + ?: return@retryWithBackoff false try { session.cryptoService().decryptEvent(event, "").let { result -> event.mxDecryptionResult = OlmDecryptionResult( @@ -403,13 +649,13 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe + verificationState = result.messageVerificationState ) } } catch (error: MXCryptoError) { // nop } - Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") + Log.v("#E2E TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") event.getClearType() == EventType.MESSAGE && messagesText[index] == event.getClearContent()?.toModel()?.body } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt index 5864a801e..60201b34c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrix.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService -import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.network.ApiInterceptorListener import org.matrix.android.sdk.api.network.ApiPath import org.matrix.android.sdk.api.raw.RawService @@ -46,7 +45,6 @@ import javax.inject.Inject */ internal class TestMatrix(context: Context, matrixConfiguration: MatrixConfiguration) { - @Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var rawService: RawService @Inject internal lateinit var userAgentHolder: UserAgentHolder @@ -88,10 +86,6 @@ internal class TestMatrix(context: Context, matrixConfiguration: MatrixConfigura fun homeServerHistoryService() = homeServerHistoryService - fun legacySessionImporter(): LegacySessionImporter { - return legacySessionImporter - } - fun registerApiInterceptorListener(path: ApiPath, listener: ApiInterceptorListener) { apiInterceptor.addListener(path, listener) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt index a74f5010c..773f480b5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt @@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { + override fun excludedUserIds(roomId: String) = emptyList() + override fun getNameForRoomInvite() = "Room invite" diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt deleted file mode 100644 index 48cfbebe5..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import io.realm.RealmConfiguration -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.util.time.DefaultClock -import kotlin.random.Random - -internal class CryptoStoreHelper { - - fun createStore(): IMXCryptoStore { - return RealmCryptoStore( - realmConfiguration = RealmConfiguration.Builder() - .name("test.realm") - .modules(RealmCryptoStoreModule()) - .build(), - crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()), - userId = "userId_" + Random.nextInt(), - deviceId = "deviceId_sample", - clock = DefaultClock(), - myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper() - ) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt deleted file mode 100644 index dbc6929e3..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.Realm -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.common.RetryTestRule -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.time.DefaultClock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmManager -import org.matrix.olm.OlmSession - -private const val DUMMY_DEVICE_KEY = "DeviceKey" - -@RunWith(AndroidJUnit4::class) -@Ignore -class CryptoStoreTest : InstrumentedTest { - - @get:Rule val rule = RetryTestRule(3) - - private val cryptoStoreHelper = CryptoStoreHelper() - private val clock = DefaultClock() - - @Before - fun setup() { - Realm.init(context()) - } - -// @Test -// fun test_metadata_realm_ok() { -// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() -// -// assertFalse(cryptoStore.hasData()) -// -// cryptoStore.open() -// -// assertEquals("deviceId_sample", cryptoStore.getDeviceId()) -// -// assertTrue(cryptoStore.hasData()) -// -// // Cleanup -// cryptoStore.close() -// cryptoStore.deleteStore() -// } - - @Test - fun test_lastSessionUsed() { - // Ensure Olm is initialized - OlmManager() - - val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore() - - assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - val olmAccount1 = OlmAccount().apply { - generateOneTimeKeys(1) - } - - val olmSession1 = OlmSession().apply { - initOutboundSession( - olmAccount1, - olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() - ) - } - - val sessionId1 = olmSession1.sessionIdentifier() - val olmSessionWrapper1 = OlmSessionWrapper(olmSession1) - - cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) - - assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - val olmAccount2 = OlmAccount().apply { - generateOneTimeKeys(1) - } - - val olmSession2 = OlmSession().apply { - initOutboundSession( - olmAccount2, - olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY], - olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first() - ) - } - - val sessionId2 = olmSession2.sessionIdentifier() - val olmSessionWrapper2 = OlmSessionWrapper(olmSession2) - - cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) - - // Ensure sessionIds are distinct - assertNotEquals(sessionId1, sessionId2) - - // Note: we cannot be sure what will be the result of getLastUsedSessionId() here - - olmSessionWrapper2.onMessageReceived(clock.epochMillis()) - cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY) - - // sessionId2 is returned now - assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - Thread.sleep(2) - - olmSessionWrapper1.onMessageReceived(clock.epochMillis()) - cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY) - - // sessionId1 is returned now - assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY)) - - // Cleanup - olmSession1.releaseSession() - olmSession2.releaseSession() - - olmAccount1.releaseAccount() - olmAccount2.releaseAccount() - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt index 4e1efbb70..4e447af09 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -46,7 +46,7 @@ class DecryptRedactedEventTest : InstrumentedTest { roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) // get the event from bob - testHelper.retryPeriodically { + testHelper.retryWithBackoff { bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt index cbbc4dc74..71e856d12 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -20,6 +20,7 @@ import android.util.Log import androidx.test.filters.LargeTest import org.amshove.kluent.internal.assertEquals import org.junit.Assert +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -27,11 +28,8 @@ import org.junit.runners.JUnit4 import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -62,15 +60,15 @@ class E2EShareKeysConfigTest : InstrumentedTest { enableEncryption() }) - commonTestHelper.retryPeriodically { + commonTestHelper.retryWithBackoff { aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! // send some messages - val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + val withSession1 = commonTestHelper.sendMessageInRoom(roomAlice, "Hello") aliceSession.cryptoService().discardOutboundSession(roomId) - val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + val withSession2 = commonTestHelper.sendMessageInRoom(roomAlice, "World") // Create bob account val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) @@ -82,7 +80,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { // Bob has join but should not be able to decrypt history cryptoTestHelper.ensureCannotDecrypt( - withSession1.map { it.eventId } + withSession2.map { it.eventId }, + listOf(withSession1, withSession2), bobSession, roomId ) @@ -90,44 +88,53 @@ class E2EShareKeysConfigTest : InstrumentedTest { // We don't need bob anymore commonTestHelper.signOutAndClose(bobSession) - // Now let's enable history key sharing on alice side - aliceSession.cryptoService().enableShareKeyOnInvite(true) - - // let's add a new message first - val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1) - - // Worth nothing to check that the session was rotated - Assert.assertNotEquals( - "Session should have been rotated", - withSession2.first().root.content?.get("session_id")!!, - afterFlagOn.first().root.content?.get("session_id")!! - ) - - // Invite a new user - val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) - - // Let alice invite sam - roomAlice.membershipService().invite(samSession.myUserId) - - commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) - - // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session - cryptoTestHelper.ensureCannotDecrypt( - withSession1.map { it.eventId } + withSession2.map { it.eventId }, - samSession, - roomId - ) - - cryptoTestHelper.ensureCanDecrypt( - afterFlagOn.map { it.eventId }, - samSession, - roomId, - afterFlagOn.map { it.root.getClearContent()?.get("body") as String }) + if (aliceSession.cryptoService().supportsShareKeysOnInvite()) { + // Now let's enable history key sharing on alice side + aliceSession.cryptoService().enableShareKeyOnInvite(true) + + // let's add a new message first + val afterFlagOn = commonTestHelper.sendMessageInRoom(roomAlice, "After") + + // Worth nothing to check that the session was rotated + Assert.assertNotEquals( + "Session should have been rotated", + aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(withSession1)?.root?.content?.get("session_id")!!, + aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(afterFlagOn)?.root?.content?.get("session_id")!! + ) + + // Invite a new user + val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + roomAlice.membershipService().invite(samSession.myUserId) + + commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session + cryptoTestHelper.ensureCannotDecrypt( + listOf(withSession1, withSession2), + samSession, + roomId + ) + + cryptoTestHelper.ensureCanDecrypt( + listOf(afterFlagOn), + samSession, + roomId, + listOf(aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(afterFlagOn)?.root?.getClearContent()?.get("body") as String) + ) + } } @Test fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + + Assume.assumeTrue("Shared key on invite needed to test this", + testData.firstSession.cryptoService().supportsShareKeysOnInvite() + ) + val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(false) } @@ -155,6 +162,11 @@ class E2EShareKeysConfigTest : InstrumentedTest { @Test fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) + + Assume.assumeTrue("Shared key on invite needed to test this", + testData.firstSession.cryptoService().supportsShareKeysOnInvite() + ) + val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) } @@ -197,6 +209,11 @@ class E2EShareKeysConfigTest : InstrumentedTest { @Test fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) + + Assume.assumeTrue("Shared key on invite needed to test this", + aliceSession.cryptoService().supportsShareKeysOnInvite() + ) + aliceSession.cryptoService().enableShareKeyOnInvite(false) val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { historyVisibility = RoomHistoryVisibility.SHARED @@ -204,75 +221,85 @@ class E2EShareKeysConfigTest : InstrumentedTest { enableEncryption() }) - commonTestHelper.retryPeriodically { + commonTestHelper.retryWithBackoff { aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! // send some messages - val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1) + val notSharableMessage = commonTestHelper.sendMessageInRoom(roomAlice, "Hello") + aliceSession.cryptoService().enableShareKeyOnInvite(true) - val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1) + val sharableMessage = commonTestHelper.sendMessageInRoom(roomAlice, "World") Log.v("#E2E TEST", "Create and start key backup for bob ...") val keysBackupService = aliceSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = commonTestHelper.waitForCallback { - keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) - } - val version = commonTestHelper.waitForCallback { - keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) - } + val megolmBackupCreationInfo = keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null) + val version = keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo) + + Log.v("#E2E TEST", "... Backup created.") - commonTestHelper.waitForCallback { - keysBackupService.backupAllGroupSessions(null, it) + commonTestHelper.retryPeriodically { + Log.v("#E2E TEST", "Backup status ${keysBackupService.getTotalNumbersOfBackedUpKeys()}/${keysBackupService.getTotalNumbersOfKeys()}") + keysBackupService.getTotalNumbersOfKeys() == keysBackupService.getTotalNumbersOfBackedUpKeys() } + val aliceId = aliceSession.myUserId // signout + + Log.v("#E2E TEST", "Sign out alice") commonTestHelper.signOutAndClose(aliceSession) - val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + Log.v("#E2E TEST", "Sign in a new alice device") + val newAliceSession = commonTestHelper.logIntoAccount(aliceId, SessionTestParams(true)) + newAliceSession.cryptoService().enableShareKeyOnInvite(true) newAliceSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = commonTestHelper.waitForCallback { - kbs.getVersion(version.version, it) - } + val keyVersionResult = kbs.getVersion(version.version) - val importedResult = commonTestHelper.waitForCallback { - kbs.restoreKeyBackupWithPassword( + Log.v("#E2E TEST", "Restore new backup") + val importedResult = kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, null, null, null, - it ) - } assertEquals(2, importedResult.totalNumberOfKeys) } // Now let's invite sam // Invite a new user + + Log.v("#E2E TEST", "Create Sam account") val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam + Log.v("#E2E TEST", "Let alice invite sam") newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session cryptoTestHelper.ensureCannotDecrypt( - notSharableMessage.map { it.eventId }, + listOf(notSharableMessage), samSession, roomId ) cryptoTestHelper.ensureCanDecrypt( - sharableMessage.map { it.eventId }, + listOf(sharableMessage), samSession, roomId, - sharableMessage.map { it.root.getClearContent()?.get("body") as String }) + listOf(newAliceSession.getRoom(roomId)!! + .getTimelineEvent(sharableMessage) + ?.root + ?.getClearContent() + ?.get("body") as String + ) + ) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt index 8b12092b7..7979a1258 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto +import android.util.Log import androidx.test.filters.LargeTest import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder @@ -52,43 +53,46 @@ class E2eeConfigTest : InstrumentedTest { val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! - val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + val sentMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you are blocked") val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! // ensure other received - testHelper.retryPeriodically { - roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null - } + testHelper.ensureMessage(roomBobPOV, sentMessage) { true } - cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId) + cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage), cryptoTestData.secondSession!!, cryptoTestData.roomId) } @Test fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + Log.v("#E2E TEST", "Initializing cross signing for alice and bob...") cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession) cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!) + Log.v("#E2E TEST", "... Initialized") + Log.v("#E2E TEST", "Start User Verification") cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId) cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! - val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + Log.v("#E2E TEST", "Send message in room") + val sentMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you can read") val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! // ensure other received - testHelper.retryPeriodically { - roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null - } + + testHelper.ensureMessage(roomBobPOV, sentMessage) { true } cryptoTestHelper.ensureCanDecrypt( - listOf(sentMessage.eventId), + listOf(sentMessage), cryptoTestData.secondSession!!, cryptoTestData.roomId, - listOf(sentMessage.getLastMessageContent()!!.body) + listOf( + roomBobPOV.timelineService().getTimelineEvent(sentMessage)?.getLastMessageContent()!!.body + ) ) } @@ -98,32 +102,34 @@ class E2eeConfigTest : InstrumentedTest { val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!! - val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first() + val beforeMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you can read") val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!! // ensure other received - testHelper.retryPeriodically { - roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null - } + Log.v("#E2E TEST", "Wait for bob to get the message") + testHelper.ensureMessage(roomBobPOV, beforeMessage) { true } + Log.v("#E2E TEST", "ensure bob Can Decrypt first message") cryptoTestHelper.ensureCanDecrypt( - listOf(beforeMessage.eventId), + listOf(beforeMessage), cryptoTestData.secondSession!!, cryptoTestData.roomId, - listOf(beforeMessage.getLastMessageContent()!!.body) + listOf("you can read") ) + Log.v("#E2E TEST", "setRoomBlockUnverifiedDevices true") cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true) - val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first() + Log.v("#E2E TEST", "let alice send the message") + val afterMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you are blocked") // ensure received - testHelper.retryPeriodically { - cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null - } + + Log.v("#E2E TEST", "Ensure bob received second message") + testHelper.ensureMessage(roomBobPOV, afterMessage) { true } cryptoTestHelper.ensureCannotDecrypt( - listOf(afterMessage.eventId), + listOf(afterMessage), cryptoTestData.secondSession!!, cryptoTestData.roomId, MXCryptoError.ErrorType.KEYS_WITHHELD diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index a36ba8ac0..204a1ec18 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -18,12 +18,7 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.filters.LargeTest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay -import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert @@ -40,27 +35,13 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest @@ -92,79 +73,56 @@ class E2eeSanityTests : InstrumentedTest { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val e2eRoomID = cryptoTestData.roomId - val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! // we want to disable key gossiping to just check initial sending of keys - aliceSession.cryptoService().enableKeyGossiping(false) - cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) + if (aliceSession.cryptoService().supportsDisablingKeyGossiping()) { + aliceSession.cryptoService().enableKeyGossiping(false) + } + if (cryptoTestData.secondSession?.cryptoService()?.supportsDisablingKeyGossiping() == true) { + cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) + } // add some more users and invite them val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)).also { - it.cryptoService().enableKeyGossiping(false) - } + .let { + cryptoTestHelper.inviteNewUsersAndWaitForThemToJoin(aliceSession, e2eRoomID, it) } - Log.v("#E2E TEST", "All accounts created") - // we want to invite them in the room - otherAccounts.forEach { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } - - // All user should accept invite - otherAccounts.forEach { otherSession -> - testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) - Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") - } - - // check that alice see them as joined (not really necessary?) - ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID) - Log.v("#E2E TEST", "All users have joined the room") Log.v("#E2E TEST", "Alice is sending the message") val text = "This is my message" - val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) - // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() - Assert.assertTrue("Message should be sent", sentEventId != null) + val sentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, text) + Log.v("#E2E TEST", "Alice just sent message with id:$sentEventId") // All should be able to decrypt otherAccounts.forEach { otherSession -> - testHelper.retryPeriodically { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE && - timeLineEvent.root.mxDecryptionResult?.isSafe == true + val room = otherSession.getRoom(e2eRoomID)!! + testHelper.ensureMessage(room, sentEventId) { + it.isEncrypted() && + it.root.getClearType() == EventType.MESSAGE && + it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE } } - + Log.v("#E2E TEST", "Everybody received the encrypted message and could decrypt") // Add a new user to the room, and check that he can't decrypt + Log.v("#E2E TEST", "Create some new accounts and invite them") val newAccount = listOf("adam") // , "adam", "manu") - .map { - testHelper.createAccount(it, SessionTestParams(true)) + .let { + cryptoTestHelper.inviteNewUsersAndWaitForThemToJoin(aliceSession, e2eRoomID, it) } - newAccount.forEach { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } - - newAccount.forEach { - testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID) - } - - ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) - // wait a bit delay(3_000) // check that messages are encrypted (uisi) newAccount.forEach { otherSession -> - testHelper.retryPeriodically { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { + testHelper.retryWithBackoff( + onFail = { + fail("New Users shouldn't be able to decrypt history") + } + ) { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId).also { Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } timelineEvent != null && @@ -177,12 +135,17 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends a new message") val secondMessage = "2 This is my message" - val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) + val secondSentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, secondMessage) // new members should be able to decrypt it newAccount.forEach { otherSession -> - testHelper.retryPeriodically { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { + // ("${otherSession.myUserId} should be able to decrypt") + testHelper.retryWithBackoff( + onFail = { + fail("New user ${otherSession.myUserId.take(10)} should be able to decrypt the second message") + } + ) { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId).also { Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } timelineEvent != null && @@ -223,13 +186,10 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val bobKeysBackupService = bobSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = testHelper.waitForCallback { - bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) - } - val version = testHelper.waitForCallback { - bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) - } - Log.v("#E2E TEST", "... Key backup started and enabled for bob") + val megolmBackupCreationInfo = bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null) + val version = bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo) + + Log.v("#E2E TEST", "... Key backup started and enabled for bob: version:$version") // Bob session should now have val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! @@ -238,11 +198,15 @@ class E2eeSanityTests : InstrumentedTest { val sentEventIds = mutableListOf() val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { + val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also { sentEventIds.add(it) } - testHelper.retryPeriodically { + testHelper.retryWithBackoff( + onFail = { + fail("Bob should be able to decrypt all messages") + } + ) { val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) timeLineEvent != null && timeLineEvent.isEncrypted() && @@ -256,7 +220,14 @@ class E2eeSanityTests : InstrumentedTest { // Let's wait a bit to be sure that bob has backed up the session Log.v("#E2E TEST", "Force key backup for Bob...") - testHelper.waitForCallback { bobKeysBackupService.backupAllGroupSessions(null, it) } + testHelper.retryWithBackoff( + onFail = { + fail("All keys should be backedup") + } + ) { + Log.v("#E2E TEST", "backedUp=${ bobKeysBackupService.getTotalNumbersOfBackedUpKeys()}, known=${bobKeysBackupService.getTotalNumbersOfKeys()}") + bobKeysBackupService.getTotalNumbersOfBackedUpKeys() == bobKeysBackupService.getTotalNumbersOfKeys() + } Log.v("#E2E TEST", "... Key backup done for Bob") // Now lets logout both alice and bob to ensure that we won't have any gossiping @@ -276,7 +247,7 @@ class E2eeSanityTests : InstrumentedTest { // check that bob can't currently decrypt Log.v("#E2E TEST", "check that bob can't currently decrypt") sentEventIds.forEach { sentEventId -> - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") } @@ -284,37 +255,41 @@ class E2eeSanityTests : InstrumentedTest { } } // after initial sync events are not decrypted, so we have to try manually - cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + // TODO CHANGE WHEN AVAILABLE FROM RUST + cryptoTestHelper.ensureCannotDecrypt( + sentEventIds, + newBobSession, + e2eRoomID, + MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + ) // MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // Let's now import keys from backup - + Log.v("#E2E TEST", "Restore backup for the new session") newBobSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = testHelper.waitForCallback { - kbs.getVersion(version.version, it) - } + val keyVersionResult = kbs.getVersion(version.version) - val importedResult = testHelper.waitForCallback { - kbs.restoreKeyBackupWithPassword( - keyVersionResult!!, - keyBackupPassword, - null, - null, - null, - it - ) - } + val importedResult = kbs.restoreKeyBackupWithPassword( + keyVersionResult!!, + keyBackupPassword, + null, + null, + null, + ) assertEquals(3, importedResult.totalNumberOfKeys) } // ensure bob can now decrypt + + Log.v("#E2E TEST", "Check that bob can decrypt now") cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) // Check key trust + Log.v("#E2E TEST", "Check key safety") sentEventIds.forEach { sentEventId -> val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!! val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "") - assertEquals("Keys from history should be deniable", false, result.isSafe) + assertEquals("Keys from history should be deniable", MessageVerificationState.UNSAFE_SOURCE, result.messageVerificationState) } } @@ -338,11 +313,15 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends some messages") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { + val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also { sentEventIds.add(it) } - testHelper.retryPeriodically { + testHelper.retryWithBackoff( + onFail = { + fail("${bobSession.myUserId.take(10)} should be able to decrypt message sent by alice}") + } + ) { val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) timeLineEvent != null && timeLineEvent.isEncrypted() && @@ -358,52 +337,40 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Create a new session for Bob") val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // ensure first session is aware of the new one + bobSession.cryptoService().downloadKeysIfNeeded(listOf(bobSession.myUserId), true) + // check that new bob can't currently decrypt Log.v("#E2E TEST", "check that new bob can't currently decrypt") cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) // Try to request - sentEventIds.forEach { sentEventId -> - val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - newBobSession.cryptoService().requestRoomKeyForEvent(event) - } - - // Ensure that new bob still can't decrypt (keys must have been withheld) +// +// Log.v("#E2E TEST", "Let bob re-request") // sentEventIds.forEach { sentEventId -> -// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! -// .getTimelineEvent(sentEventId)!! -// .root.content.toModel()!!.sessionId -// testHelper.retryPeriodically { -// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() -// .first { -// it.sessionId == megolmSessionId && -// it.roomId == e2eRoomID -// } -// .results.also { -// Log.w("##TEST", "result list is $it") -// } -// .firstOrNull { it.userId == aliceSession.myUserId } -// ?.result -// aliceReply != null && -// aliceReply is RequestResult.Failure && -// WithHeldCode.UNAUTHORISED == aliceReply.code -// } +// val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root +// newBobSession.cryptoService().reRequestRoomKeyForEvent(event) // } - - cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) +// +// Log.v("#E2E TEST", "Should not be able to decrypt as not verified") +// cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) // Now mark new bob session as verified - bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) - newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!) + Log.v("#E2E TEST", "Mark all as verified") + bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId) + newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId) // now let new session re-request + + Log.v("#E2E TEST", "Re-request") sentEventIds.forEach { sentEventId -> val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root newBobSession.cryptoService().reRequestRoomKeyForEvent(event) } + Log.v("#E2E TEST", "Now should be able to decrypt") cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) } @@ -429,9 +396,9 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends some messages") firstMessage.let { text -> - firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! + firstEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text) - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) timeLineEvent != null && timeLineEvent.isEncrypted() && @@ -455,9 +422,9 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends some messages") secondMessage.let { text -> - secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! + secondEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text) - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) timeLineEvent != null && timeLineEvent.isEncrypted() && @@ -488,11 +455,11 @@ class E2eeSanityTests : InstrumentedTest { // Now let's verify bobs session, and re-request keys bobSessionWithBetterKey.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId) newBobSession.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId) // now let new session request newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root) @@ -501,7 +468,7 @@ class E2eeSanityTests : InstrumentedTest { // old session should have shared the key at earliest known index now // we should be able to decrypt both - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val canDecryptFirst = try { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") true @@ -518,101 +485,79 @@ class E2eeSanityTests : InstrumentedTest { } } - private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { - var sentEventId: String? = null - aliceRoomPOV.sendService().sendTextMessage(text) - - val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) - timeline.start() - testHelper.retryPeriodically { - val decryptedMsg = timeline.getSnapshot() - .filter { it.root.getClearType() == EventType.MESSAGE } - .also { list -> - val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } - Log.v("#E2E TEST", "Timeline snapshot is $message") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } - timeline.dispose() - return sentEventId - } - /** * Test that if a better key is forwared (lower index, it is then used) */ - @Test - fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - - val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) - cryptoTestHelper.bootstrapSecurity(aliceSession) - - // now let's create a new login from alice - - val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) - - val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId) - val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId) - // initiate self verification - aliceSession.cryptoService().verificationService().requestKeyVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceNewSession.myUserId, - listOf(aliceNewSession.sessionParams.deviceId!!) - ) - - val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode) - - assertEquals("Decimal code should have matched", oldCode, newCode) - - // Assert that devices are verified - val newDeviceFromOldPov: CryptoDeviceInfo? = - aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) - val oldDeviceFromNewPov: CryptoDeviceInfo? = - aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) - - Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) - Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) - - // wait for secret gossiping to happen - testHelper.retryPeriodically { - aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() - } - - testHelper.retryPeriodically { - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null - } - - assertEquals( - "MSK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master - ) - assertEquals( - "USK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user - ) - - assertEquals( - "SSK Private parts should be the same", - aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, - aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned - ) - - // Let's check that we have the megolm backup key - assertEquals( - "Megolm key should be the same", - aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey - ) - assertEquals( - "Megolm version should be the same", - aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version - ) - } +// @Test +// fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> +// +// val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) +// cryptoTestHelper.bootstrapSecurity(aliceSession) +// +// // now let's create a new login from alice +// +// val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) +// +// val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId) +// val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId) +// // initiate self verification +// aliceSession.cryptoService().verificationService().requestSelfKeyVerification( +// listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), +// // aliceNewSession.myUserId, +// // listOf(aliceNewSession.sessionParams.deviceId!!) +// ) +// +// val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode) +// +// assertEquals("Decimal code should have matched", oldCode, newCode) +// +// // Assert that devices are verified +// val newDeviceFromOldPov: CryptoDeviceInfo? = +// aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) +// val oldDeviceFromNewPov: CryptoDeviceInfo? = +// aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) +// +// Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) +// Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) +// +// // wait for secret gossiping to happen +// testHelper.retryPeriodically { +// aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() +// } +// +// testHelper.retryPeriodically { +// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null +// } +// +// assertEquals( +// "MSK Private parts should be the same", +// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, +// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master +// ) +// assertEquals( +// "USK Private parts should be the same", +// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, +// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user +// ) +// +// assertEquals( +// "SSK Private parts should be the same", +// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, +// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned +// ) +// +// // Let's check that we have the megolm backup key +// assertEquals( +// "Megolm key should be the same", +// aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, +// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey +// ) +// assertEquals( +// "Megolm version should be the same", +// aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, +// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version +// ) +// } @Test fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> @@ -625,26 +570,23 @@ class E2eeSanityTests : InstrumentedTest { user = aliceSession.myUserId, password = TestConstants.PASSWORD ) + val bobAuthParams = UserPasswordAuth( user = bobSession!!.myUserId, password = TestConstants.PASSWORD ) - testHelper.waitForCallback { - aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(aliceAuthParams) - } - }, it) - } + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }) - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) - } + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) // add a second session for bob but not cross signed @@ -656,15 +598,15 @@ class E2eeSanityTests : InstrumentedTest { val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!! Timber.v("#TEST: Send a first message that should be withheld") - val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!! + val sentEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "Hello") // wait for it to be synced back the other side Timber.v("#TEST: Wait for message to be synced back") - testHelper.retryPeriodically { + testHelper.retryWithBackoff { bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null } - testHelper.retryPeriodically { + testHelper.retryWithBackoff { secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null } @@ -679,13 +621,13 @@ class E2eeSanityTests : InstrumentedTest { Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt") - val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!! + val secondEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "World") Timber.v("#TEST: Wait for message to be synced back") - testHelper.retryPeriodically { + testHelper.retryWithBackoff { bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null } - testHelper.retryPeriodically { + testHelper.retryWithBackoff { secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null } @@ -693,104 +635,94 @@ class E2eeSanityTests : InstrumentedTest { cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId) } - private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { - return scope.async { - suspendCancellableCoroutine { continuation -> - var oldCode: String? = null - val listener = object : VerificationService.Listener { - - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - val readyInfo = pr.readyInfo - if (readyInfo != null) { - beginKeyVerification( - VerificationMethod.SAS, - userId, - readyInfo.fromDevice, - readyInfo.transactionId - - ) - } - } - - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "exitsingPov: $tx") - val sasTx = tx as OutgoingSasVerificationTransaction - when (sasTx.uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - // for the test we just accept? - oldCode = sasTx.getDecimalCodeRepresentation() - sasTx.userHasVerifiedShortCode() - } - OutgoingSasVerificationTransaction.UxState.VERIFIED -> { - removeListener(this) - // we can release this latch? - continuation.resume(oldCode!!) - } - else -> Unit - } - } - } - addListener(listener) - continuation.invokeOnCancellation { removeListener(listener) } - } - } - } - - private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { - return scope.async { - suspendCancellableCoroutine { continuation -> - var newCode: String? = null - - val listener = object : VerificationService.Listener { - - override fun verificationRequestCreated(pr: PendingVerificationRequest) { - // let's ready - readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - userId, - pr.transactionId!! - ) - } - - var matchOnce = true - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "newPov: $tx") - - val sasTx = tx as IncomingSasVerificationTransaction - when (sasTx.uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - // no need to accept as there was a request first it will auto accept - } - IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - if (matchOnce) { - sasTx.userHasVerifiedShortCode() - newCode = sasTx.getDecimalCodeRepresentation() - matchOnce = false - } - } - IncomingSasVerificationTransaction.UxState.VERIFIED -> { - removeListener(this) - continuation.resume(newCode!!) - } - else -> Unit - } - } - } - addListener(listener) - continuation.invokeOnCancellation { removeListener(listener) } - } - } - } - - private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.retryPeriodically { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } - } - } +// private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { +// return scope.async { +// suspendCancellableCoroutine { continuation -> +// var oldCode: String? = null +// val listener = object : VerificationService.Listener { +// +// override fun verificationRequestUpdated(pr: PendingVerificationRequest) { +// val readyInfo = pr.readyInfo +// if (readyInfo != null) { +// beginKeyVerification( +// VerificationMethod.SAS, +// userId, +// readyInfo.fromDevice, +// readyInfo.transactionId +// +// ) +// } +// } +// +// override fun transactionUpdated(tx: VerificationTransaction) { +// Log.d("##TEST", "exitsingPov: $tx") +// val sasTx = tx as OutgoingSasVerificationTransaction +// when (sasTx.uxState) { +// OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { +// // for the test we just accept? +// oldCode = sasTx.getDecimalCodeRepresentation() +// sasTx.userHasVerifiedShortCode() +// } +// OutgoingSasVerificationTransaction.UxState.VERIFIED -> { +// removeListener(this) +// // we can release this latch? +// continuation.resume(oldCode!!) +// } +// else -> Unit +// } +// } +// } +// addListener(listener) +// continuation.invokeOnCancellation { removeListener(listener) } +// } +// } +// } +// +// private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { +// return scope.async { +// suspendCancellableCoroutine { continuation -> +// var newCode: String? = null +// +// val listener = object : VerificationService.Listener { +// +// override fun verificationRequestCreated(pr: PendingVerificationRequest) { +// // let's ready +// readyPendingVerification( +// listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), +// userId, +// pr.transactionId!! +// ) +// } +// +// var matchOnce = true +// override fun transactionUpdated(tx: VerificationTransaction) { +// Log.d("##TEST", "newPov: $tx") +// +// val sasTx = tx as IncomingSasVerificationTransaction +// when (sasTx.uxState) { +// IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { +// // no need to accept as there was a request first it will auto accept +// } +// IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { +// if (matchOnce) { +// sasTx.userHasVerifiedShortCode() +// newCode = sasTx.getDecimalCodeRepresentation() +// matchOnce = false +// } +// } +// IncomingSasVerificationTransaction.UxState.VERIFIED -> { +// removeListener(this) +// continuation.resume(newCode!!) +// } +// else -> Unit +// } +// } +// } +// addListener(listener) +// continuation.invokeOnCancellation { removeListener(listener) } +// } +// } +// } private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { sentEventIds.forEach { sentEventId -> diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 91e0026c9..095d88545 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -18,9 +18,11 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.filters.LargeTest +import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertNotEquals import org.junit.Assert +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -32,7 +34,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership @@ -42,7 +43,6 @@ import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.wrapWithTimeout @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -79,9 +79,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { runCryptoTest(context()) { cryptoTestHelper, testHelper -> val aliceMessageText = "Hello Bob, I am Alice!" val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) - val e2eRoomID = cryptoTestData.roomId + Assume.assumeTrue(cryptoTestData.firstSession.cryptoService().supportsShareKeysOnInvite()) // Alice val aliceSession = cryptoTestData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) @@ -99,19 +99,26 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper) Assert.assertTrue("Message should be sent", aliceMessageId != null) - Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") + Log.v("#E2E TEST", "Alice has sent message to roomId: $e2eRoomID") // Bob should be able to decrypt the message - testHelper.retryPeriodically { - val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE && - timelineEvent.root.mxDecryptionResult?.isSafe == true).also { - if (it) { - Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") - } + testHelper.retryWithBackoff( + onFail = { + fail("Bob should be able to decrypt $aliceMessageId") } + ) { + val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)?.also { + Log.v("#E2E TEST", "Bob sees ${it.root.getClearType()}|${it.root.mxDecryptionResult?.verificationState}") + } + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE + // && timelineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE + ).also { + if (it) { + Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") + } + } } // Create a new user @@ -135,23 +142,31 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { null -> { // Aris should be able to decrypt the message - testHelper.retryPeriodically { - val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE && - timelineEvent.root.mxDecryptionResult?.isSafe == false - ).also { - if (it) { - Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") - } + testHelper.retryWithBackoff( + onFail = { + fail("Aris should be able to decrypt $aliceMessageId") + } + ) { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE // && + // timelineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE + ).also { + if (it) { + Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } + } } } RoomHistoryVisibility.INVITED, RoomHistoryVisibility.JOINED -> { // Aris should not even be able to get the message - testHelper.retryPeriodically { + testHelper.retryWithBackoff( + onFail = { + fail("Aris should not even be able to get the message") + } + ) { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) ?.timelineService() ?.getTimelineEvent(aliceMessageId!!) @@ -160,7 +175,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } } - testHelper.signOutAndClose(arisSession) cryptoTestData.cleanUp(testHelper) } @@ -237,6 +251,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId + Assume.assumeTrue(cryptoTestData.firstSession.cryptoService().supportsShareKeysOnInvite()) + // Alice val aliceSession = cryptoTestData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) @@ -258,11 +274,17 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { // Bob should be able to decrypt the message var firstAliceMessageMegolmSessionId: String? = null - val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) - testHelper.retryPeriodically { + val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)!! + testHelper.retryWithBackoff( + onFail = { + fail("Bob should be able to decrypt $aliceMessageId") + } + ) { val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) + .timelineService() + .getTimelineEvent(aliceMessageId!!)?.also { + Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}") + } (timelineEvent != null && timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also { @@ -279,11 +301,17 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId) var secondAliceMessageSessionId: String? = null - sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> - testHelper.retryPeriodically { + sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)!!.let { secondMessage -> + testHelper.retryWithBackoff( + onFail = { + fail("Bob should be able to decrypt the second message $secondMessage") + } + ) { val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(secondMessage) + .timelineService() + .getTimelineEvent(secondMessage)?.also { + Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}") + } (timelineEvent != null && timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also { @@ -309,29 +337,44 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr ).toContent() ) + Log.v("#E2E TEST ROTATION", "State update sent") // ensure that the state did synced down - testHelper.retryPeriodically { - aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content + testHelper.retryWithBackoff( + onFail = { + fail("Alice state should be updated to ${nextRoomHistoryVisibility.historyVisibilityStr}") + } + ) { + aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.also { + Log.v("#E2E TEST ROTATION", "Alice sees state as $it") + } ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } - testHelper.retryPeriodically { - val roomVisibility = aliceSession.getRoom(e2eRoomID)!! - .stateService() - .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") - roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } +// testHelper.retryPeriodically { +// val roomVisibility = aliceSession.getRoom(e2eRoomID)!! +// .stateService() +// .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) +// ?.content +// ?.toModel() +// Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") +// roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility +// } var aliceThirdMessageSessionId: String? = null - sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> - testHelper.retryPeriodically { + sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)!!.let { thirdMessage -> + testHelper.retryWithBackoff( + onFail = { + fail("Bob should be able to decrypt $thirdMessage") + } + ) { val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(thirdMessage) + .timelineService() + .getTimelineEvent(thirdMessage)?.also { + Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}") + } (timelineEvent != null && timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also { @@ -341,7 +384,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } } } - + Log.v("#E2E TEST ROTATION", "second session id $secondAliceMessageSessionId") + Log.v("#E2E TEST ROTATION", "third session id $aliceThirdMessageSessionId") when { initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> { assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId) @@ -352,8 +396,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST ROTATION", "Rotation is needed!") } } - - cryptoTestData.cleanUp(testHelper) } private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { @@ -364,7 +406,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.retryPeriodically { + testHelper.retryWithBackoff { otherAccounts.map { aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership }.all { @@ -374,7 +416,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) (roomSummary != null && roomSummary.membership == Membership.INVITE).also { if (it) { @@ -383,17 +425,15 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } } - wrapWithTimeout(60_000) { - Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") - try { - otherSession.roomService().joinRoom(e2eRoomID) - } catch (ex: JoinRoomFailure.JoinedWithTimeout) { - // it's ok we will wait after - } + Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") + try { + otherSession.roomService().joinRoom(e2eRoomID) + } catch (ex: JoinRoomFailure.JoinedWithTimeout) { + // it's ok we will wait after } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.retryPeriodically { + testHelper.retryWithBackoff { val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) roomSummary != null && roomSummary.membership == Membership.JOIN } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt new file mode 100644 index 000000000..a1142daae --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeTestVerificationTestDirty.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.test.filters.LargeTest +import junit.framework.TestCase.fail +import kotlinx.coroutines.delay +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class E2eeTestVerificationTestDirty : InstrumentedTest { + + @Test + fun testVerificationStateRefreshedAfterKeyDownload() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val e2eRoomID = cryptoTestData.roomId + + // We are going to setup a second session for bob that will send a message while alice session + // has stopped syncing. + + aliceSession.syncService().stopSync() + aliceSession.syncService().stopAnyBackgroundSync() + // wait a bit for session to be really closed + delay(1_000) + + Log.v("#E2E TEST", "Create a new session for Bob") + val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + + Log.v("#E2E TEST", "New bob session will send a message") + val eventId = testHelper.sendMessageInRoom(newBobSession.getRoom(e2eRoomID)!!, "I am unknown") + + aliceSession.syncService().startSync(true) + + // Check without starting a timeline so that it doesn't update itself + testHelper.retryWithBackoff( + onFail = { + fail("${aliceSession.myUserId.take(10)} should not have downloaded the device at time of decryption") + }) { + val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also { + Log.v("#E2E TEST", "Verification state is ${it?.root?.mxDecryptionResult?.verificationState}") + } + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE && + timeLineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UNKNOWN_DEVICE + } + + // After key download it should be dirty (that will happen after sync completed) + testHelper.retryWithBackoff( + onFail = { + fail("${aliceSession.myUserId.take(10)} should be dirty") + }) { + val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also { + Log.v("#E2E TEST", "Is verification state dirty ${it?.root?.verificationStateIsDirty}") + } + timeLineEvent?.root?.verificationStateIsDirty.orFalse() + } + + Log.v("#E2E TEST", "Start timeline and check that verification state is updated") + // eventually should be marked as dirty then have correct state when a timeline is started + testHelper.ensureMessage(aliceSession.getRoom(e2eRoomID)!!, eventId) { + it.isEncrypted() && + it.root.getClearType() == EventType.MESSAGE && + it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt deleted file mode 100644 index 5c817443c..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.Log -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class PreShareKeysTest : InstrumentedTest { - - @Test - fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) - val e2eRoomID = testData.roomId - val aliceSession = testData.firstSession - val bobSession = testData.secondSession!! - - // clear any outbound session - aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - - val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - - assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) - Log.d("#Test", "Room Key Received from alice $preShareCount") - - // Force presharing of new outbound key - testHelper.waitForCallback { - aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) - } - - testHelper.retryPeriodically { - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - newKeysCount > preShareCount - } - - val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting - val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier() - - val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting - val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!! - val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!) - assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) - assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) - - val megolmSessionId = bobInboundForAlice.session.sessionIdentifier() - - assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) - - val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) - .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) - - assertEquals("The session received by bob should match what alice sent", 0, sharedIndex) - - // Just send a real message as test - val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() - - assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId) - testHelper.retryPeriodically { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE - } - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/RoomShieldTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/RoomShieldTest.kt new file mode 100644 index 000000000..8e2284d58 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/RoomShieldTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.Log +import androidx.lifecycle.Observer +import androidx.test.filters.LargeTest +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.TestConstants +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +@LargeTest +class RoomShieldTest : InstrumentedTest { + + @Test + fun testShieldNoVerification() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, _ -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val roomId = testData.roomId + + cryptoTestHelper.initializeCrossSigning(testData.firstSession) + cryptoTestHelper.initializeCrossSigning(testData.secondSession!!) + + // Test are flaky unless I use liveData observer on main thread + // Just calling getRoomSummary() with retryWithBackOff keeps an outdated version of the value + testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Default) + testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Default) + } + + @Test + fun testShieldInOneOne() = CommonTestHelper.runLongCryptoTest(context()) { cryptoTestHelper, testHelper -> + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val roomId = testData.roomId + + Log.v("#E2E TEST", "Initialize cross signing...") + cryptoTestHelper.initializeCrossSigning(testData.firstSession) + cryptoTestHelper.initializeCrossSigning(testData.secondSession!!) + Log.v("#E2E TEST", "... Initialized.") + + // let alive and bob verify + Log.v("#E2E TEST", "Alice and Bob verify each others...") + cryptoTestHelper.verifySASCrossSign(testData.firstSession, testData.secondSession!!, testData.roomId) + + // Add a new session for bob + // This session will be unverified for now + + Log.v("#E2E TEST", "Log in a new bob device...") + val bobSecondSession = testHelper.logIntoAccount(testData.secondSession!!.myUserId, SessionTestParams(true)) + + Log.v("#E2E TEST", "Bob session logged in ${bobSecondSession.myUserId.take(6)}") + + Log.v("#E2E TEST", "Assert room shields...") + testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning) + // in 1:1 we ignore our own status + testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted) + + // Adding another user should make bob consider his devices now and see same shield as alice + Log.v("#E2E TEST", "Create Sam account") + val samSession = testHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) + + // Let alice invite sam + Log.v("#E2E TEST", "Let alice invite sam") + testData.firstSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) + testHelper.waitForAndAcceptInviteInRoom(samSession, roomId) + + Log.v("#E2E TEST", "Assert room shields...") + testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning) + testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Warning) + + // Now let's bob verify his session + + Log.v("#E2E TEST", "Bob verifies his new session") + cryptoTestHelper.verifyNewSession(testData.secondSession!!, bobSecondSession) + + testData.firstSession.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted) + testData.secondSession!!.assertRoomShieldIs(roomId, RoomEncryptionTrustLevel.Trusted) + } + + @OptIn(DelicateCoroutinesApi::class) + private suspend fun Session.assertRoomShieldIs(roomId: String, state: RoomEncryptionTrustLevel?) { + val lock = CountDownLatch(1) + val roomLiveData = withContext(Dispatchers.Main) { + roomService().getRoomSummaryLive(roomId) + } + val observer = object : Observer> { + override fun onChanged(value: Optional) { + Log.v("#E2E TEST ${this@assertRoomShieldIs.myUserId.take(6)}", "Shield Update ${value.getOrNull()?.roomEncryptionTrustLevel}") + if (value.getOrNull()?.roomEncryptionTrustLevel == state) { + lock.countDown() + roomLiveData.removeObserver(this) + } + } + } + GlobalScope.launch(Dispatchers.Main) { roomLiveData.observeForever(observer) } + + lock.await(40_000, TimeUnit.MILLISECONDS) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt deleted file mode 100644 index 889cc9a56..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.suspendCancellableCoroutine -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Assert -import org.junit.Before -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm -import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm -import org.matrix.olm.OlmSession -import timber.log.Timber -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume - -/** - * Ref: - * - https://github.com/matrix-org/matrix-doc/pull/1719 - * - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages - * - https://github.com/matrix-org/matrix-js-sdk/pull/780 - * - https://github.com/matrix-org/matrix-ios-sdk/pull/778 - * - https://github.com/matrix-org/matrix-ios-sdk/pull/784 - */ -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class UnwedgingTest : InstrumentedTest { - - private lateinit var messagesReceivedByBob: List - - @Before - fun init() { - messagesReceivedByBob = emptyList() - } - - /** - * - Alice & Bob in a e2e room - * - Alice sends a 1st message with a 1st megolm session - * - Store the olm session between A&B devices - * - Alice sends a 2nd message with a 2nd megolm session - * - Simulate Alice using a backup of her OS and make her crypto state like after the first message - * - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - * - * What Bob must see: - * -> No issue with the 2 first messages - * -> The third event must fail to decrypt at first because Bob the olm session is wedged - * -> This is automatically fixed after SDKs restarted the olm session - */ - @Test - fun testUnwedging() = runCryptoTest( - context(), - cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) - ) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - val bobSession = cryptoTestData.secondSession!! - - val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting - val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest - - val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - - val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) - bobTimeline.start() - - messagesReceivedByBob = emptyList() - - // - Alice sends a 1st message with a 1st megolm session - roomFromAlicePOV.sendService().sendTextMessage("First message") - - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1) - - messagesReceivedByBob.size shouldBeEqualTo 1 - val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - - // - Store the olm session between A&B devices - // Let us pickle our session with bob here so we can later unpickle it - // and wedge our session. - val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) - sessionIdsForBob!!.size shouldBeEqualTo 1 - val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! - - val oldSession = serializeForRealm(olmSession.olmSession) - - aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - - messagesReceivedByBob = emptyList() - Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") - // - Alice sends a 2nd message with a 2nd megolm session - roomFromAlicePOV.sendService().sendTextMessage("Second message") - - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2) - - messagesReceivedByBob.size shouldBeEqualTo 2 - // Session should have changed - val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - Assert.assertNotEquals(firstMessageSession, secondMessageSession) - - // Let us wedge the session now. Set crypto state like after the first message - Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message") - - aliceCryptoStore.storeSession( - OlmSessionWrapper(deserializeFromRealm(oldSession)!!), - bobSession.cryptoService().getMyDevice().identityKey()!! - ) - olmDevice.clearOlmSessionCache() - - // Force new session, and key share - aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - - Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") - // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - roomFromAlicePOV.sendService().sendTextMessage("Third message") - // Bob should not be able to decrypt, because the session key could not be sent - // Wait for the message to be received by Bob - messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3) - - messagesReceivedByBob.size shouldBeEqualTo 3 - - val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! - Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") - Assert.assertNotEquals(secondMessageSession, thirdMessageSession) - - Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType()) - Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) - Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) - // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged - - Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) - - // It's a trick to force key request on fail to decrypt - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, it - ) - } - - // Wait until we received back the key - testHelper.retryPeriodically { - // we should get back the key and be able to decrypt - val result = tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") - } - Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") - result != null - } - - bobTimeline.dispose() - } -} - -private suspend fun Timeline.waitForMessages(expectedCount: Int): List { - return suspendCancellableCoroutine { continuation -> - val listener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED } - - if (messagesReceived.size == expectedCount) { - removeListener(this) - continuation.resume(messagesReceived) - } - } - } - - addListener(listener) - continuation.invokeOnCancellation { removeListener(listener) } - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt index 936dc6a87..cf7493470 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt @@ -20,6 +20,7 @@ import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldBeTrue import org.junit.Test import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.api.util.fromBase64Safe @Suppress("SpellCheckingInspection") class ExtensionsKtTest { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index c4fb89693..12c63edf9 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams @@ -54,7 +53,6 @@ class XSigningTest : InstrumentedTest { fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { @@ -66,10 +64,10 @@ class XSigningTest : InstrumentedTest { ) ) } - }, it) - } + }) + + val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() - val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() val masterPubKey = myCrossSigningKeys?.masterKey() assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) val selfSigningKey = myCrossSigningKeys?.selfSigningKey() @@ -79,13 +77,14 @@ class XSigningTest : InstrumentedTest { assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) - assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + val userTrustResult = aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId) + assertTrue("Signing Keys should be trusted", userTrustResult.isVerified()) testHelper.signOutAndClose(aliceSession) } @Test - fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, _ -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession @@ -100,39 +99,30 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.waitForCallback { - aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(aliceAuthParams) - } - }, it) - } - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) - } + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }) + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) // Check that alice can see bob keys - testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobSession.myUserId), true) val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey()) assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey()) - assertEquals( - "Bob keys from alice pov should match", - bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, - bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey - ) - assertEquals( - "Bob keys from alice pov should match", - bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, - bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey - ) + val myKeys = bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys() + + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, myKeys?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, myKeys?.selfSigningKey()?.unpaddedBase64PublicKey) assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) } @@ -153,40 +143,34 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.waitForCallback { - aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(aliceAuthParams) - } - }, it) - } - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) - } + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }) + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) // Check that alice can see bob keys val bobUserId = bobSession.myUserId - testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true) val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) + assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + aliceSession.cryptoService().crossSigningService().trustUser(bobUserId) // Now bobs logs in on a new device and verifies it // We will want to test that in alice POV, this new device would be trusted by cross signing val bobSession2 = testHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! - + val bobSecondDeviceId = bobSession2.sessionParams.deviceId // Check that bob first session sees the new login - val data = testHelper.waitForCallback> { - bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) - } + val data = bobSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true) if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { fail("Bob should see the new device") @@ -196,14 +180,10 @@ class XSigningTest : InstrumentedTest { assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) - } + bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId) // Now alice should cross trust bob's second device - val data2 = testHelper.waitForCallback> { - aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) - } + val data2 = aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true) // check that the device is seen if (data2.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { @@ -216,11 +196,15 @@ class XSigningTest : InstrumentedTest { @Test fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession + // Remove when https://github.com/matrix-org/matrix-rust-sdk/issues/1129 + Assume.assumeTrue("Not yet supported by rust", aliceSession.cryptoService().name() != "rust-sdk") + val aliceAuthParams = UserPasswordAuth( user = aliceSession.myUserId, password = TestConstants.PASSWORD @@ -230,20 +214,16 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.waitForCallback { - aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(aliceAuthParams) - } - }, it) - } - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) - } + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }) + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId) @@ -267,13 +247,11 @@ class XSigningTest : InstrumentedTest { .getUserCrossSigningKeys(bobSession.myUserId)!! .masterKey()!!.unpaddedBase64PublicKey!! - testHelper.waitForCallback { - bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume(bobAuthParams) - } - }, it) - } + bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }) testHelper.retryPeriodically { val newBobMsk = aliceSession.cryptoService().crossSigningService() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 8e001b84d..9b94553fd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -19,12 +19,13 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue -import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Assert.assertNull +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -59,6 +60,8 @@ class KeyShareTests : InstrumentedTest { fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message @@ -70,8 +73,9 @@ class KeyShareTests : InstrumentedTest { ) val room = aliceSession.getRoom(roomId) assertNotNull(room) - Thread.sleep(4_000) - assertTrue(room?.roomCryptoService()?.isEncrypted() == true) + commonTestHelper.retryWithBackoff { + room?.roomCryptoService()?.isEncrypted() == true + } val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first() val sentEventId = sentEvent.eventId @@ -100,7 +104,7 @@ class KeyShareTests : InstrumentedTest { // Try to request aliceSession2.cryptoService().enableKeyGossiping(true) - aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) + aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId @@ -163,30 +167,34 @@ class KeyShareTests : InstrumentedTest { // Mark the device as trusted - Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}") - val aliceSecondSession = aliceSession2.cryptoService().getMyDevice() + Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyCryptoDevice().identityKey()}") + val aliceSecondSession = aliceSession2.cryptoService().getMyCryptoDevice() Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}") aliceSession.cryptoService().setDeviceVerification( DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.deviceId ?: "" + aliceSession2.sessionParams.deviceId ) // We only accept forwards from trusted session, so we need to trust on other side to aliceSession2.cryptoService().setDeviceVerification( DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession.sessionParams.deviceId ?: "" + aliceSession.sessionParams.deviceId ) - aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true + aliceSession.cryptoService().deviceWithIdentityKey( + aliceSecondSession.userId, + aliceSecondSession.identityKey()!!, + MXCRYPTO_ALGORITHM_OLM + )!!.isVerified shouldBeEqualTo true // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: "")) - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(aliceSession2) +// commonTestHelper.signOutAndClose(aliceSession) +// commonTestHelper.signOutAndClose(aliceSession2) } // See E2ESanityTest for a test regarding secret sharing @@ -203,6 +211,9 @@ class KeyShareTests : InstrumentedTest { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession + + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! val bobSession = testData.secondSession!! @@ -235,6 +246,9 @@ class KeyShareTests : InstrumentedTest { val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) val aliceSession = testData.firstSession + + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) @@ -257,11 +271,11 @@ class KeyShareTests : InstrumentedTest { outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success } + +// commonTestHelper.signOutAndClose(aliceSession) +// commonTestHelper.signOutAndClose(aliceNewSession) } - /** - * Tests that keys reshared with own verified session are done from the earliest known index - */ @Test fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest( context(), @@ -270,6 +284,9 @@ class KeyShareTests : InstrumentedTest { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession + + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) + val bobSession = testData.secondSession!! val roomFromBob = bobSession.getRoom(testData.roomId)!! @@ -331,10 +348,10 @@ class KeyShareTests : InstrumentedTest { // Mark the new session as verified aliceSession.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId) aliceNewSession.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId) // Let's now try to request aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) @@ -370,14 +387,11 @@ class KeyShareTests : InstrumentedTest { result != null && result is RequestResult.Success && result.chainIndex == 3 } - commonTestHelper.signOutAndClose(aliceNewSession) - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) +// commonTestHelper.signOutAndClose(aliceNewSession) +// commonTestHelper.signOutAndClose(aliceSession) +// commonTestHelper.signOutAndClose(bobSession) } - /** - * Tests that we don't cancel a request to early on first forward if the index is not good enough - */ @Test fun test_dontCancelToEarly() = runCryptoTest( context(), @@ -385,6 +399,9 @@ class KeyShareTests : InstrumentedTest { ) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = testData.firstSession + + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) + val bobSession = testData.secondSession!! val roomFromBob = bobSession.getRoom(testData.roomId)!! @@ -419,10 +436,10 @@ class KeyShareTests : InstrumentedTest { // Mark the new session as verified aliceSession.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId) aliceNewSession.cryptoService() .verificationService() - .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!) + .markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId) // /!\ Stop initial alice session syncing so that it can't reply aliceSession.cryptoService().enableKeyGossiping(false) @@ -462,8 +479,8 @@ class KeyShareTests : InstrumentedTest { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state) - commonTestHelper.signOutAndClose(aliceNewSession) - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) +// commonTestHelper.signOutAndClose(aliceNewSession) +// commonTestHelper.signOutAndClose(aliceSession) +// commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index b55ddbc97..e0df83924 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -20,13 +20,14 @@ import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import org.junit.Assert +import org.junit.Assume import org.junit.FixMethodOrder +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -71,6 +72,7 @@ class WithHeldTests : InstrumentedTest { val roomAlicePOV = aliceSession.getRoom(roomId)!! val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) + // ============================= // ACT // ============================= @@ -78,14 +80,14 @@ class WithHeldTests : InstrumentedTest { // Alice decide to not send to unverified sessions aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true) - val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() + val eventId = testHelper.sendMessageInRoom(roomAlicePOV, "Hello Bob") // await for bob unverified session to get the message - testHelper.retryPeriodically { - bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null + testHelper.retryWithBackoff { + bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(eventId) != null } - val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! + val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(eventId)!! val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!! // ============================= @@ -94,6 +96,7 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld // .. might need to wait a bit for stability? + // WILL FAIL for rust until this fixed https://github.com/matrix-org/matrix-rust-sdk/issues/1806 mustFail( message = "This session should not be able to decrypt", failureBlock = { failure -> @@ -106,25 +109,27 @@ class WithHeldTests : InstrumentedTest { bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } - // Let's see if the reply we got from bob first session is unverified - testHelper.retryPeriodically { - bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() - .firstOrNull { it.sessionId == megolmSessionId } - ?.results - ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } - ?.result - ?.let { - it as? RequestResult.Failure - } - ?.code == WithHeldCode.UNVERIFIED + if (bobUnverifiedSession.cryptoService().supportKeyRequestInspection()) { + // Let's see if the reply we got from bob first session is unverified + testHelper.retryWithBackoff { + bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() + .firstOrNull { it.sessionId == megolmSessionId } + ?.results + ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } + ?.result + ?.let { + it as? RequestResult.Failure + } + ?.code == WithHeldCode.UNVERIFIED + } } // enable back sending to unverified aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) - val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() + val secondEventId = testHelper.sendMessageInRoom(roomAlicePOV, "Verify your device!!") - testHelper.retryPeriodically { - val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) + testHelper.retryWithBackoff { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEventId) // wait until it's decrypted ev?.root?.getClearType() == EventType.MESSAGE } @@ -144,6 +149,7 @@ class WithHeldTests : InstrumentedTest { } @Test + @Ignore("ignore NoOlm for now, implementation not correct") fun test_WithHeldNoOlm() = runCryptoTest( context(), cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) @@ -151,27 +157,26 @@ class WithHeldTests : InstrumentedTest { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession + Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection()) val bobSession = testData.secondSession!! val aliceInterceptor = testHelper.getTestInterceptor(aliceSession) // Simulate no OTK - aliceInterceptor!!.addRule( - MockOkHttpInterceptor.SimpleRule( - "/keys/claim", - 200, - """ + aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule( + "/keys/claim", + 200, + """ { "one_time_keys" : {} } """ - ) - ) + )) Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}") val roomAlicePov = aliceSession.getRoom(testData.roomId)!! - val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId + val eventId = testHelper.sendMessageInRoom(roomAlicePov, "first message") // await for bob session to get the message - testHelper.retryPeriodically { + testHelper.retryWithBackoff { bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null } @@ -191,10 +196,7 @@ class WithHeldTests : InstrumentedTest { // Ensure that alice has marked the session to be shared with bob val sessionId = eventBobPOV!!.root.content.toModel()!!.sessionId!! - val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( - bobSession.myUserId, - bobSession.sessionParams.credentials.deviceId - ) + val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId) Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex) // Add a new device for bob @@ -210,10 +212,7 @@ class WithHeldTests : InstrumentedTest { bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null } - val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( - bobSecondSession.myUserId, - bobSecondSession.sessionParams.credentials.deviceId - ) + val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId) Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2) @@ -221,6 +220,7 @@ class WithHeldTests : InstrumentedTest { } @Test + @Ignore("Outdated test, we don't request to others") fun test_WithHeldKeyRequest() = runCryptoTest( context(), cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false) @@ -228,6 +228,7 @@ class WithHeldTests : InstrumentedTest { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession + Assume.assumeTrue("Not supported by rust sdk", aliceSession.cryptoService().supportsForwardedKeyWiththeld()) val bobSession = testData.secondSession!! val roomAlicePov = aliceSession.getRoom(testData.roomId)!! @@ -243,8 +244,8 @@ class WithHeldTests : InstrumentedTest { cryptoTestHelper.initializeCrossSigning(bobSecondSession) // Trust bob second device from Alice POV - aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback()) - bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback()) + aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId) + bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId) var sessionId: String? = null // Check that the @@ -265,5 +266,10 @@ class WithHeldTests : InstrumentedTest { val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) wc?.code == WithHeldCode.UNAUTHORISED } +// // Check that bob second session requested the key +// testHelper.retryPeriodically { +// val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) +// wc?.code == WithHeldCode.UNAUTHORISED +// } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupStateHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupStateHelper.kt new file mode 100644 index 000000000..7ed508ce3 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/BackupStateHelper.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.util.Log +import kotlinx.coroutines.CompletableDeferred +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener + +internal class BackupStateHelper( + private val keysBackup: KeysBackupService) : KeysBackupStateListener { + + init { + keysBackup.addListener(this) + } + + val hasBackedUpOnce = CompletableDeferred() + + var backingUpOnce = false + + override fun onStateChange(newState: KeysBackupState) { + Log.d("#E2E", "Keybackup onStateChange $newState") + if (newState == KeysBackupState.BackingUp) { + backingUpOnce = true + } + if (newState == KeysBackupState.ReadyToBackUp || newState == KeysBackupState.WillBackUp) { + if (backingUpOnce) { + hasBackedUpOnce.complete(Unit) + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt index 8679cf3c9..6b8b45f81 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -19,14 +19,13 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestData -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper /** * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] */ internal data class KeysBackupScenarioData( val cryptoTestData: CryptoTestData, - val aliceKeys: List, + val aliceKeysCount: Int, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 01c03b800..485dcd68b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -16,42 +16,36 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import kotlinx.coroutines.suspendCancellableCoroutine +import org.amshove.kluent.internal.assertFails +import org.amshove.kluent.internal.assertFailsWith import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue +import org.junit.Assume import org.junit.FixMethodOrder -import org.junit.Rule +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP -import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.StepProgressListener -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest -import org.matrix.android.sdk.common.RetryTestRule +import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.waitFor import java.security.InvalidParameterException -import java.util.Collections -import java.util.concurrent.CountDownLatch import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @@ -59,7 +53,7 @@ import kotlin.coroutines.resume @LargeTest class KeysBackupTest : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) + // @get:Rule val rule = RetryTestRule(3) /** * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys @@ -67,39 +61,40 @@ class KeysBackupTest : InstrumentedTest { * - Reset keys backup markers */ @Test - fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + @Ignore("Uses internal APIs") + fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { _, _ -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - - // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys - val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store - val sessions = cryptoStore.inboundGroupSessionsToBackup(100) - val sessionsCount = sessions.size - - assertFalse(sessions.isEmpty()) - assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) - assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) - - // - Check backup keys after having marked one as backed up - val session = sessions[0] - - cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session)) - - assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) - assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) - - val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) - assertEquals(sessionsCount - 1, sessions2.size) - - // - Reset keys backup markers - cryptoStore.resetBackupMarkers() - - val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) - assertEquals(sessionsCount, sessions3.size) - assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) - assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) +// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() +// +// // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys +// val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val sessions = cryptoStore.inboundGroupSessionsToBackup(100) +// val sessionsCount = sessions.size +// +// assertFalse(sessions.isEmpty()) +// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) +// assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) +// +// // - Check backup keys after having marked one as backed up +// val session = sessions[0] +// +// cryptoStore.markBackupDoneForInboundGroupSessions(listOf(session)) +// +// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) +// assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) +// +// val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100) +// assertEquals(sessionsCount - 1, sessions2.size) +// +// // - Reset keys backup markers +// cryptoStore.resetBackupMarkers() +// +// val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100) +// assertEquals(sessionsCount, sessions3.size) +// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) +// assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) - cryptoTestData.cleanUp(testHelper) +// cryptoTestData.cleanUp(testHelper) } /** @@ -118,9 +113,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.waitForCallback { - keysBackup.prepareKeysBackupVersion(null, null, it) - } + val megolmBackupCreationInfo = keysBackup.prepareKeysBackupVersion(null, null) assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm) assertNotNull(megolmBackupCreationInfo.authData.publicKey) @@ -136,6 +129,7 @@ class KeysBackupTest : InstrumentedTest { @Test fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) + Log.d("#E2E", "Initializing crosssigning for ${bobSession.myUserId.take(8)}") cryptoTestHelper.initializeCrossSigning(bobSession) val keysBackup = bobSession.cryptoService().keysBackupService() @@ -144,28 +138,24 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.waitForCallback { - keysBackup.prepareKeysBackupVersion(null, null, it) - } + Log.d("#E2E", "prepareKeysBackupVersion") + val megolmBackupCreationInfo = + keysBackup.prepareKeysBackupVersion(null, null) assertFalse(keysBackup.isEnabled()) // Create the version - val version = testHelper.waitForCallback { - keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) - } + Log.d("#E2E", "createKeysBackupVersion") + val version = keysBackup.createKeysBackupVersion(megolmBackupCreationInfo) // Backup must be enable now assertTrue(keysBackup.isEnabled()) // Check that it's signed with MSK - val versionResult = testHelper.waitForCallback { - keysBackup.getVersion(version.version, it) - } - val trust = testHelper.waitForCallback { - keysBackup.getKeysBackupTrust(versionResult!!, it) - } + val versionResult = keysBackup.getVersion(version.version) + val trust = keysBackup.getKeysBackupTrust(versionResult!!) + Log.d("#E2E", "Check backup signatures") assertEquals("Should have 2 signatures", 2, trust.signatures.size) trust.signatures @@ -204,19 +194,17 @@ class KeysBackupTest : InstrumentedTest { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - keysBackupTestHelper.waitForKeybackUpBatching() val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() - val latch = CountDownLatch(1) - assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)) assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)) - val stateObserver = StateObserver(keysBackup, latch, 5) + val stateObserver = BackupStateHelper(keysBackup).hasBackedUpOnce keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) - - testHelper.await(latch) + Log.d("#E2E", "Wait for a backup cycle") + stateObserver.await() + Log.d("#E2E", ".. Ok") val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) @@ -225,15 +213,15 @@ class KeysBackupTest : InstrumentedTest { assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) // Check the several backup state changes - stateObserver.stopAndCheckStates( - listOf( - KeysBackupState.Enabling, - KeysBackupState.ReadyToBackUp, - KeysBackupState.WillBackUp, - KeysBackupState.BackingUp, - KeysBackupState.ReadyToBackUp - ) - ) +// stateObserver.stopAndCheckStates( +// listOf( +// KeysBackupState.Enabling, +// KeysBackupState.ReadyToBackUp, +// KeysBackupState.WillBackUp, +// KeysBackupState.BackingUp, +// KeysBackupState.ReadyToBackUp +// ) +// ) } /** @@ -242,33 +230,27 @@ class KeysBackupTest : InstrumentedTest { @Test fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) - + Log.d("#E2E", "Setting up Alice Bob with messages") val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) + Log.d("#E2E", "Creating key backup...") keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + Log.d("#E2E", "... created") // Check that backupAllGroupSessions returns valid data val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) assertEquals(2, nbOfKeys) - var lastBackedUpKeysProgress = 0 - - testHelper.waitForCallback { - keysBackup.backupAllGroupSessions(object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - assertEquals(nbOfKeys, total) - lastBackedUpKeysProgress = progress - } - }, it) + testHelper.retryWithBackoff { + Log.d("#E2E", "Backup ${keysBackup.getTotalNumbersOfBackedUpKeys()}/${keysBackup.getTotalNumbersOfBackedUpKeys()}") + keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys() } - assertEquals(nbOfKeys, lastBackedUpKeysProgress) - val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true) assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) @@ -285,41 +267,42 @@ class KeysBackupTest : InstrumentedTest { * - Compare the decrypted megolm key with the original one */ @Test - fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) - - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() - - val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService - - val stateObserver = StateObserver(keysBackup) - - // - Pick a megolm key - val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] - - val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo - - // - Check encryptGroupSession() returns stg - val keyBackupData = keysBackup.encryptGroupSession(session) - assertNotNull(keyBackupData) - assertNotNull(keyBackupData!!.sessionData) - - // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption - val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) - assertNotNull(decryption) - // - Check decryptKeyBackupData() returns stg - val sessionData = keysBackup - .decryptKeyBackupData( - keyBackupData, - session.safeSessionId!!, - cryptoTestData.roomId, - decryption!! - ) - assertNotNull(sessionData) - // - Compare the decrypted megolm key with the original one - keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) - - stateObserver.stopAndCheckStates(null) + @Ignore("Uses internal API") + fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { _, _ -> +// val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) +// +// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() +// +// val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService +// +// val stateObserver = StateObserver(keysBackup) +// +// // - Pick a megolm key +// val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] +// +// val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo +// +// // - Check encryptGroupSession() returns stg +// val keyBackupData = keysBackup.encryptGroupSession(session) +// assertNotNull(keyBackupData) +// assertNotNull(keyBackupData!!.sessionData) +// +// // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption +// val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey.toBase58()) +// assertNotNull(decryption) +// // - Check decryptKeyBackupData() returns stg +// val sessionData = keysBackup +// .decryptKeyBackupData( +// keyBackupData, +// session.safeSessionId!!, +// cryptoTestData.roomId, +// keyBackupCreationInfo.recoveryKey +// ) +// assertNotNull(sessionData) +// // - Compare the decrypted megolm key with the original one +// keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData) +// +// stateObserver.stopAndCheckStates(null) } /** @@ -335,16 +318,15 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) // - Restore the e2e backup from the homeserver - val importRoomKeysResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, - null, - null, - null, - it - ) - } + val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null + ) + + Log.d("#E2E", "importRoomKeysResult is $importRoomKeysResult") keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) @@ -401,7 +383,7 @@ class KeysBackupTest : InstrumentedTest { // // Request is either sent or unsent // assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) // -// testData.cleanUp(mTestHelper) +// testData.cleanUp(testHelper) // } /** @@ -430,13 +412,10 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device - testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - true, - it - ) - } + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + true + ) // Wait for backup state to be ReadyToBackUp keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) @@ -446,20 +425,25 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) - }.toKeysVersionResult() + val keysVersionResult = testData.aliceSession2.cryptoService() + .keysBackupService() + .getCurrentVersion()!! + .toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) - } + val keysBackupVersionTrust = testData.aliceSession2.cryptoService() + .keysBackupService() + .getKeysBackupTrust(keysVersionResult) - // - It must be trusted and must have 2 signatures now + // The backup should have a valid signature from that device now assertTrue(keysBackupVersionTrust.usable) - assertEquals(2, keysBackupVersionTrust.signatures.size) + val signature = keysBackupVersionTrust.signatures + .filterIsInstance() + .firstOrNull { it.deviceId == testData.aliceSession2.cryptoService().getMyCryptoDevice().deviceId } + assertNotNull(signature) + assertTrue(signature!!.valid) stateObserver.stopAndCheckStates(null) } @@ -490,36 +474,43 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the recovery key - testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, - it - ) - } + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey + ) // Wait for backup state to be ReadyToBackUp keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) // - Backup must be enabled on the new device, on the same version - assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) + assertEquals( + testData.prepareKeysBackupDataResult.version, + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version + ) assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) - }.toKeysVersionResult() + val keysVersionResult = testData.aliceSession2.cryptoService().keysBackupService() + .getCurrentVersion()!! + .toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) - } + val keysBackupVersionTrust = testData.aliceSession2.cryptoService() + .keysBackupService() + .getKeysBackupTrust(keysVersionResult) - // - It must be trusted and must have 2 signatures now +// // - It must be trusted and must have 2 signatures now +// assertTrue(keysBackupVersionTrust.usable) +// assertEquals(2, keysBackupVersionTrust.signatures.size) + // The backup should have a valid signature from that device now assertTrue(keysBackupVersionTrust.usable) - assertEquals(2, keysBackupVersionTrust.signatures.size) + val signature = keysBackupVersionTrust.signatures + .filterIsInstance() + .firstOrNull { it.deviceId == testData.aliceSession2.cryptoService().getMyCryptoDevice().deviceId } + assertNotNull(signature) + assertTrue(signature!!.valid) stateObserver.stopAndCheckStates(null) } @@ -548,11 +539,10 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong recovery key - testHelper.waitForCallbackError { + assertFails { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "Bad recovery key", - it + BackupUtils.recoveryKeyFromPassphrase("Bad recovery key"), ) } @@ -592,13 +582,10 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the password - testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - password, - it - ) - } + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password + ) // Wait for backup state to be ReadyToBackUp keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) @@ -608,20 +595,28 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) - }.toKeysVersionResult() + val keysVersionResult = testData.aliceSession2.cryptoService().keysBackupService() + .getCurrentVersion()!! + .toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) - } + val keysBackupVersionTrust = testData.aliceSession2.cryptoService() + .keysBackupService() + .getKeysBackupTrust(keysVersionResult) + +// // - It must be trusted and must have 2 signatures now +// assertTrue(keysBackupVersionTrust.usable) +// assertEquals(2, keysBackupVersionTrust.signatures.size) - // - It must be trusted and must have 2 signatures now + // - It must be trusted and signed by current device assertTrue(keysBackupVersionTrust.usable) - assertEquals(2, keysBackupVersionTrust.signatures.size) + val signature = keysBackupVersionTrust.signatures + .filterIsInstance() + .firstOrNull { it.deviceId == testData.aliceSession2.cryptoService().getMyCryptoDevice().deviceId } + assertNotNull(signature) + assertTrue(signature!!.valid) stateObserver.stopAndCheckStates(null) } @@ -653,11 +648,10 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong password - testHelper.waitForCallbackError { + assertFails { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, badPassword, - it ) } @@ -683,18 +677,15 @@ class KeysBackupTest : InstrumentedTest { val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong recovery key - val importRoomKeysResult = testHelper.waitForCallbackError { + assertFailsWith { keysBackupService.restoreKeysWithRecoveryKey( keysBackupService.keysBackupVersion!!, - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"), null, null, null, - it ) } - - assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -705,29 +696,31 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> + val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val password = "password" val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + Assume.assumeTrue( + "Can't report progress same way in rust", + testData.cryptoTestData.firstSession.cryptoService().name() != "rust-sdk" + ) // - Restore the e2e backup with the password val steps = ArrayList() - val importRoomKeysResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - password, - null, - null, - object : StepProgressListener { - override fun onStepProgress(step: StepProgressListener.Step) { - steps.add(step) - } - }, - it - ) - } + val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + password, + null, + null, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + steps.add(step) + } + } + ) // Check steps assertEquals(105, steps.size) @@ -770,18 +763,15 @@ class KeysBackupTest : InstrumentedTest { val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong password - val importRoomKeysResult = testHelper.waitForCallbackError { + assertFailsWith { keysBackupService.restoreKeyBackupWithPassword( keysBackupService.keysBackupVersion!!, wrongPassword, null, null, null, - it ) } - - assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -799,16 +789,13 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) // - Restore the e2e backup with the recovery key. - val importRoomKeysResult = testHelper.waitForCallback { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, - null, - null, - null, - it - ) - } + val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + null + ) keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) } @@ -823,22 +810,19 @@ class KeysBackupTest : InstrumentedTest { fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) - val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword("password") val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a password - val importRoomKeysResult = testHelper.waitForCallbackError { - keysBackupService.restoreKeyBackupWithPassword( - keysBackupService.keysBackupVersion!!, - "password", - null, - null, - null, - it - ) - } + val importRoomKeysResult = keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + "password", + null, + null, + null, + ) - assertTrue(importRoomKeysResult is IllegalStateException) + assertTrue(importRoomKeysResult.importedSessionInfo.isNotEmpty()) } /** @@ -860,14 +844,10 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Get key backup version from the homeserver - val keysVersionResult = testHelper.waitForCallback { - keysBackup.getCurrentVersion(it) - }.toKeysVersionResult() + val keysVersionResult = keysBackup.getCurrentVersion()!!.toKeysVersionResult() // - Check the returned KeyBackupVersion is trusted - val keysBackupVersionTrust = testHelper.waitForCallback { - keysBackup.getKeysBackupTrust(keysVersionResult!!, it) - } + val keysBackupVersionTrust = keysBackup.getKeysBackupTrust(keysVersionResult!!) assertNotNull(keysBackupVersionTrust) assertTrue(keysBackupVersionTrust.usable) @@ -876,7 +856,7 @@ class KeysBackupTest : InstrumentedTest { val signature = keysBackupVersionTrust.signatures[0] as KeysBackupVersionTrustSignature.DeviceSignature assertTrue(signature.valid) assertNotNull(signature.device) - assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) + assertEquals(cryptoTestData.firstSession.cryptoService().getMyCryptoDevice().deviceId, signature.deviceId) assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) @@ -888,7 +868,7 @@ class KeysBackupTest : InstrumentedTest { * - Make alice back up her keys to her homeserver * - Create a new backup with fake data on the homeserver * - Make alice back up all her keys again - * -> That must fail and her backup state must be WrongBackUpVersion + * -> That must fail and her backup state must be WrongBackUpVersion or Not trusted? */ @Test fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> @@ -899,58 +879,28 @@ class KeysBackupTest : InstrumentedTest { val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() - val stateObserver = StateObserver(keysBackup) - assertFalse(keysBackup.isEnabled()) - // Wait for keys backup to be finished - var count = 0 - waitFor( - continueWhen = { - suspendCancellableCoroutine { continuation -> - val listener = object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (newState == KeysBackupState.ReadyToBackUp) { - count++ - - if (count == 2) { - // Remove itself from the list of listeners - keysBackup.removeListener(this) - continuation.resume(Unit) - } - } - } - } - keysBackup.addListener(listener) - continuation.invokeOnCancellation { keysBackup.removeListener(listener) } - } - }, - action = { - // - Make alice back up her keys to her homeserver - keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) - }, - ) - + val backupWaitHelper = BackupStateHelper(keysBackup) + keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) assertTrue(keysBackup.isEnabled()) - // - Create a new backup with fake data on the homeserver, directly using the rest client - val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo() - testHelper.waitForCallback { - (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) - } + backupWaitHelper.hasBackedUpOnce.await() - // Reset the store backup status for keys - (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() + val newSession = testHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, SessionTestParams(true)) + keysBackupTestHelper.prepareAndCreateKeysBackupData(newSession.cryptoService().keysBackupService()) - // - Make alice back up all her keys again - testHelper.waitForCallbackError { keysBackup.backupAllGroupSessions(null, it) } + // Make a new key for alice to backup + cryptoTestData.firstSession.cryptoService().discardOutboundSession(cryptoTestData.roomId) + testHelper.sendMessageInRoom(cryptoTestData.firstSession.getRoom(cryptoTestData.roomId)!!, "new") - // -> That must fail and her backup state must be WrongBackUpVersion - assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) - assertFalse(keysBackup.isEnabled()) + // - Alice first session should not be able to backup + testHelper.retryPeriodically { + Log.d("#E2E", "backup state is ${keysBackup.getState()}") + KeysBackupState.NotTrusted == keysBackup.getState() + } - stateObserver.stopAndCheckStates(null) + assertFalse(keysBackup.isEnabled()) } /** @@ -966,62 +916,62 @@ class KeysBackupTest : InstrumentedTest { * -> It must success */ @Test + @Ignore("Instable on both flavors") fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) // - Create a backup version val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() + cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession) val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() - val stateObserver = StateObserver(keysBackup) - // - Make alice back up her keys to her homeserver keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Wait for keys backup to finish by asking again to backup keys. - testHelper.waitForCallback { - keysBackup.backupAllGroupSessions(null, it) + testHelper.retryWithBackoff { + keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys() + } + testHelper.retryWithBackoff { + keysBackup.getState() == KeysBackupState.ReadyToBackUp } - val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! val oldKeyBackupVersion = keysBackup.currentBackupVersion val aliceUserId = cryptoTestData.firstSession.myUserId // - Log Alice on a new device + Log.d("#E2E", "Log Alice on a new device") val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) // - Post a message to have a new megolm session + Log.d("#E2E", "Post a message to have a new megolm session") aliceSession2.cryptoService().setWarnOnUnknownDevices(false) - val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!! - testHelper.sendTextMessage(room2, "New key", 1) + testHelper.sendMessageInRoom(room2, "New key") // - Try to backup all in aliceSession2, it must fail val keysBackup2 = aliceSession2.cryptoService().keysBackupService() assertFalse("Backup should not be enabled", keysBackup2.isEnabled()) - val stateObserver2 = StateObserver(keysBackup2) - - testHelper.waitForCallbackError { keysBackup2.backupAllGroupSessions(null, it) } - // Backup state must be NotTrusted assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) assertFalse("Backup should not be enabled", keysBackup2.isEnabled()) + val signatures = keysBackup2.getCurrentVersion()?.toKeysVersionResult()?.getAuthDataAsMegolmBackupAuthData()?.signatures + Log.d("#E2E", "keysBackup2 signatures: $signatures") + // - Validate the old device from the new one - aliceSession2.cryptoService().setDeviceVerification( - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), - aliceSession2.myUserId, - oldDeviceId - ) + cryptoTestHelper.verifyNewSession(cryptoTestData.firstSession, aliceSession2) + cryptoTestData.firstSession.cryptoService().keysBackupService().checkAndStartKeysBackup() // -> Backup should automatically enable on the new device suspendCancellableCoroutine { continuation -> val listener = object : KeysBackupStateListener { override fun onStateChange(newState: KeysBackupState) { + Log.d("#E2E", "keysBackup2 onStateChange: $newState") // Check the backup completes if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { // Remove itself from the list of listeners @@ -1037,15 +987,17 @@ class KeysBackupTest : InstrumentedTest { // -> It must use the same backup version assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) - testHelper.waitForCallback { - aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) + // aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) + testHelper.retryWithBackoff { + keysBackup2.getTotalNumbersOfKeys() == keysBackup2.getTotalNumbersOfBackedUpKeys() + } + + testHelper.retryWithBackoff { + aliceSession2.cryptoService().keysBackupService().getState() == KeysBackupState.ReadyToBackUp } // -> It must success assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled()) - - stateObserver.stopAndCheckStates(null) - stateObserver2.stopAndCheckStates(null) } /** @@ -1070,7 +1022,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Delete the backup - testHelper.waitForCallback { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + keysBackup.deleteBackup(keyBackupCreationInfo.version) // Backup is now disabled assertFalse(keysBackup.isEnabled()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 10abf93bc..6122370b5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -18,13 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert -import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.assertDictEquals @@ -53,29 +50,22 @@ internal class KeysBackupTestHelper( waitForKeybackUpBatching() - val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() val stateObserver = StateObserver(keysBackup) - val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100) +// val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100) // - Do an e2e backup to the homeserver val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password) - var lastProgress = 0 - var lastTotal = 0 - testHelper.waitForCallback { - keysBackup.backupAllGroupSessions(object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - lastProgress = progress - lastTotal = total - } - }, it) + testHelper.retryPeriodically { + keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys() } + val totalNumbersOfBackedUpKeys = cryptoTestData.firstSession.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys() - Assert.assertEquals(2, lastProgress) - Assert.assertEquals(2, lastTotal) + Assert.assertEquals(2, totalNumbersOfBackedUpKeys) val aliceUserId = cryptoTestData.firstSession.myUserId @@ -83,19 +73,18 @@ internal class KeysBackupTestHelper( val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync) // Test check: aliceSession2 has no keys at login - Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + val inboundGroupSessionCount = aliceSession2.cryptoService().inboundGroupSessionsCount(false) + Assert.assertEquals(0, inboundGroupSessionCount) // Wait for backup state to be NotTrusted waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted) stateObserver.stopAndCheckStates(null) - return KeysBackupScenarioData( - cryptoTestData, - aliceKeys, + return KeysBackupScenarioData(cryptoTestData, + totalNumbersOfBackedUpKeys, prepareKeysBackupDataResult, - aliceSession2 - ) + aliceSession2) } suspend fun prepareAndCreateKeysBackupData( @@ -104,18 +93,15 @@ internal class KeysBackupTestHelper( ): PrepareKeysBackupDataResult { val stateObserver = StateObserver(keysBackup) - val megolmBackupCreationInfo = testHelper.waitForCallback { - keysBackup.prepareKeysBackupVersion(password, null, it) - } + val megolmBackupCreationInfo = keysBackup.prepareKeysBackupVersion(password, null) Assert.assertNotNull(megolmBackupCreationInfo) Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) // Create the version - val keysVersion = testHelper.waitForCallback { - keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) - } + val keysVersion = + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo) Assert.assertNotNull("Key backup version should not be null", keysVersion.version) @@ -152,7 +138,7 @@ internal class KeysBackupTestHelper( } } - fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { + internal fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { Assert.assertNotNull(keys1) Assert.assertNotNull(keys2) @@ -174,24 +160,27 @@ internal class KeysBackupTestHelper( * - The new device must have the same count of megolm keys * - Alice must have the same keys on both devices */ - fun checkRestoreSuccess( + suspend fun checkRestoreSuccess( testData: KeysBackupScenarioData, total: Int, imported: Int ) { // - Imported keys number must be correct - Assert.assertEquals(testData.aliceKeys.size, total) + Assert.assertEquals(testData.aliceKeysCount, total) Assert.assertEquals(total, imported) // - The new device must have the same count of megolm keys - Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)) + val inboundGroupSessionCount = testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false) + + Assert.assertEquals(testData.aliceKeysCount, inboundGroupSessionCount) // - Alice must have the same keys on both devices - for (aliceKey1 in testData.aliceKeys) { - val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store - .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!) - Assert.assertNotNull(aliceKey2) - assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) - } + // TODO can't access internals as we can switch from rust/kotlin +// for (aliceKey1 in testData.aliceKeys) { +// val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!) +// Assert.assertNotNull(aliceKey2) +// assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) +// } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt index 6c9777454..5c784e818 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/StateObserver.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import android.util.Log import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService @@ -51,10 +52,13 @@ internal class StateObserver( KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver, // This transition happens when we trust the device KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp, + // This transition happens when we create a new backup from an untrusted one + KeysBackupState.NotTrusted to KeysBackupState.Enabling, KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp, KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver, + KeysBackupState.Unknown to KeysBackupState.Enabling, KeysBackupState.WillBackUp to KeysBackupState.BackingUp, @@ -90,6 +94,7 @@ internal class StateObserver( } override fun onStateChange(newState: KeysBackupState) { + Log.d("#E2E", "Keybackup onStateChange $newState") stateList.add(newState) // Check that state transition is valid diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt index 0dfecffbd..7babfc183 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -21,6 +21,7 @@ import org.amshove.kluent.internal.assertFailsWith import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.fail +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +44,9 @@ class ReplayAttackTest : InstrumentedTest { // Alice val aliceSession = cryptoTestData.firstSession + + // Until https://github.com/matrix-org/matrix-rust-sdk/issues/397 + Assume.assumeTrue("Not yet supported by rust", cryptoTestData.firstSession.cryptoService().name() != "rust-sdk") val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!! // Bob diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index 0467d082a..558d3a15d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -88,7 +88,7 @@ class QuadSTests : InstrumentedTest { assertNotNull(defaultKeyAccountData?.content) assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) - testHelper.signOutAndClose(aliceSession) +// testHelper.signOutAndClose(aliceSession) } @Test diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt new file mode 100644 index 000000000..52a75d065 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/store/migration/DynamicElementAndroidToElementRMigrationTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.migration + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.mockk.spyk +import io.realm.Realm +import io.realm.kotlin.where +import org.amshove.kluent.internal.assertEquals +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.TestBuildVersionSdkIntProvider +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils +import org.matrix.android.sdk.internal.crypto.RustEncryptionConfiguration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration +import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.store.db.RustMigrationInfoProvider +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.database.TestRealmConfigurationFactory +import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.android.sdk.test.shared.createTimberTestRule +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmManager +import org.matrix.rustcomponents.sdk.crypto.OlmMachine +import java.io.File +import java.security.KeyStore + +@RunWith(AndroidJUnit4::class) +class DynamicElementAndroidToElementRMigrationTest { + + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + @Rule + fun timberTestRule() = createTimberTestRule() + + var context: Context = InstrumentationRegistry.getInstrumentation().context + var realm: Realm? = null + + @Before + fun setUp() { + // Ensure Olm is initialized + OlmManager() + } + + @After + fun tearDown() { + realm?.close() + } + + private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) } + + private val rustEncryptionConfiguration = RustEncryptionConfiguration( + "foo", + RealmKeysUtils( + context, + SecretStoringUtils(context, keyStore, TestBuildVersionSdkIntProvider(), false) + ) + ) + + private val fakeClock = object : Clock { + override fun epochMillis() = 0L + } + + @Test + fun given_a_valid_crypto_store_realm_file_then_migration_should_be_successful() { + testMigrate(false) + } + + @Test + @Ignore("We don't migrate group sessions for now, and it's making this test suite unstable") + fun given_a_valid_crypto_store_realm_file_no_lazy_then_migration_should_be_successful() { + testMigrate(true) + } + + private fun testMigrate(migrateGroupSessions: Boolean) { + val targetFile = File(configurationFactory.root, "rust-sdk") + + val realmName = "crypto_store_migration_16.realm" + val infoProvider = RustMigrationInfoProvider( + targetFile, + rustEncryptionConfiguration + ).apply { + migrateMegolmGroupSessions = migrateGroupSessions + } + val migration = RealmCryptoStoreMigration(fakeClock, infoProvider) + + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + null, + RealmCryptoStoreModule(), + migration.schemaVersion, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + val metaData = realm!!.where().findFirst()!! + val userId = metaData.userId!! + val deviceId = metaData.deviceId!! + val olmAccount = metaData.getOlmAccount()!! + + val machine = OlmMachine(userId, deviceId, targetFile.path, rustEncryptionConfiguration.getDatabasePassphrase()) + + assertEquals(olmAccount.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY], machine.identityKeys()["ed25519"]) + assertNotNull(machine.getBackupKeys()) + val crossSigningStatus = machine.crossSigningStatus() + assertTrue(crossSigningStatus.hasMaster) + assertTrue(crossSigningStatus.hasSelfSigning) + assertTrue(crossSigningStatus.hasUserSigning) + + if (migrateGroupSessions) { + assertTrue("Some outbound sessions should be migrated", machine.roomKeyCounts().total.toInt() > 0) + assertTrue("There are some backed-up sessions", machine.roomKeyCounts().backedUp.toInt() > 0) + } else { + assertTrue(machine.roomKeyCounts().total.toInt() == 0) + assertTrue(machine.roomKeyCounts().backedUp.toInt() == 0) + } + + // legacy olm sessions should have been deleted + val remainingOlmSessions = realm!!.where().findAll().size + assertEquals("legacy olm sessions should have been removed from store", 0, remainingOlmSessions) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt deleted file mode 100644 index fd2136edd..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ /dev/null @@ -1,611 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import android.util.Log -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.FixMethodOrder -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasMode -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.model.rest.toValue -import java.util.concurrent.CountDownLatch - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Ignore -class SASTest : InstrumentedTest { - - @Test - fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val bobTxCreatedLatch = CountDownLatch(1) - val bobListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - bobTxCreatedLatch.countDown() - } - } - bobVerificationService.addListener(bobListener) - - val txID = aliceVerificationService.beginKeyVerification( - VerificationMethod.SAS, - bobSession.myUserId, - bobSession.cryptoService().getMyDevice().deviceId, - null - ) - assertNotNull("Alice should have a started transaction", txID) - - val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) - assertNotNull("Alice should have a started transaction", aliceKeyTx) - - testHelper.await(bobTxCreatedLatch) - bobVerificationService.removeListener(bobListener) - - val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID) - - assertNotNull("Bob should have started verif transaction", bobKeyTx) - assertTrue(bobKeyTx is SASDefaultVerificationTransaction) - assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) - assertTrue(aliceKeyTx is SASDefaultVerificationTransaction) - assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) - - val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction? - val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction? - - assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state) - assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state) - - // Let's cancel from alice side - val cancelLatch = CountDownLatch(1) - - val bobListener2 = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx.transactionId == txID) { - val immutableState = (tx as SASDefaultVerificationTransaction).state - if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) { - cancelLatch.countDown() - } - } - } - } - bobVerificationService.addListener(bobListener2) - - aliceSasTx.cancel(CancelCode.User) - testHelper.await(cancelLatch) - - assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled) - assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled) - - val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled - val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled - - assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe) - assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe) - - assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode) - assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.cancelCode) - - assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) - assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) - } - - @Test - @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val protocols = listOf("meh_dont_know") - val tid = "00000000" - - // Bob should receive a cancel - var cancelReason: CancelCode? = null - val cancelLatch = CountDownLatch(1) - - val bobListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - if (tx.transactionId == tid && tx.state is VerificationTxState.Cancelled) { - cancelReason = (tx.state as VerificationTxState.Cancelled).cancelCode - cancelLatch.countDown() - } - } - } - bobSession.cryptoService().verificationService().addListener(bobListener) - - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId - - val aliceListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - (tx as IncomingSasVerificationTransaction).performAccept() - } - } - } - aliceSession.cryptoService().verificationService().addListener(aliceListener) - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) - - testHelper.await(cancelLatch) - - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason) - } - - @Test - @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val mac = listOf("shaBit") - val tid = "00000000" - - // Bob should receive a cancel - var canceledToDeviceEvent: Event? = null - val cancelLatch = CountDownLatch(1) - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac) - - testHelper.await(cancelLatch) - - val cancelReq = canceledToDeviceEvent!!.content.toModel()!! - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - } - - @Test - @Ignore("This test will be ignored until it is fixed") - fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - fail("Not passing for the moment") - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val bobSession = cryptoTestData.secondSession!! - - val codes = listOf("bin", "foo", "bar") - val tid = "00000000" - - // Bob should receive a cancel - var canceledToDeviceEvent: Event? = null - val cancelLatch = CountDownLatch(1) - // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { - // TODO override fun onToDeviceEvent(event: Event?) { - // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { - // TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) { - // TODO canceledToDeviceEvent = event - // TODO cancelLatch.countDown() - // TODO } - // TODO } - // TODO } - // TODO }) - - val aliceSession = cryptoTestData.firstSession - val aliceUserID = aliceSession.myUserId - val aliceDevice = aliceSession.cryptoService().getMyDevice().deviceId - - fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes) - - testHelper.await(cancelLatch) - - val cancelReq = canceledToDeviceEvent!!.content.toModel()!! - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) - } - - private fun fakeBobStart( - bobSession: Session, - aliceUserID: String?, - aliceDevice: String?, - tid: String, - protocols: List = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, - hashes: List = SASDefaultVerificationTransaction.KNOWN_HASHES, - mac: List = SASDefaultVerificationTransaction.KNOWN_MACS, - codes: List = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES - ) { - val startMessage = KeyVerificationStart( - fromDevice = bobSession.cryptoService().getMyDevice().deviceId, - method = VerificationMethod.SAS.toValue(), - transactionId = tid, - keyAgreementProtocols = protocols, - hashes = hashes, - messageAuthenticationCodes = mac, - shortAuthenticationStrings = codes - ) - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(aliceUserID, aliceDevice, startMessage) - - // TODO val sendLatch = CountDownLatch(1) - // TODO bobSession.cryptoRestClient.sendToDevice( - // TODO EventType.KEY_VERIFICATION_START, - // TODO contentMap, - // TODO tid, - // TODO TestMatrixCallback(sendLatch) - // TODO ) - } - - // any two devices may only have at most one key verification in flight at a time. - // If a device has two verifications in progress with the same device, then it should cancel both verifications. - @Test - fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - - val aliceCreatedLatch = CountDownLatch(2) - val aliceCancelledLatch = CountDownLatch(2) - val createdTx = mutableListOf() - val aliceListener = object : VerificationService.Listener { - override fun transactionCreated(tx: VerificationTransaction) { - createdTx.add(tx as SASDefaultVerificationTransaction) - aliceCreatedLatch.countDown() - } - - override fun transactionUpdated(tx: VerificationTransaction) { - if ((tx as SASDefaultVerificationTransaction).state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { - aliceCancelledLatch.countDown() - } - } - } - aliceVerificationService.addListener(aliceListener) - - val bobUserId = bobSession!!.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - - testHelper.await(aliceCreatedLatch) - testHelper.await(aliceCancelledLatch) - - cryptoTestData.cleanUp(testHelper) - } - - /** - * Test that when alice starts a 'correct' request, bob agrees. - */ - @Test - @Ignore("This test will be ignored until it is fixed") - fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - var accepted: ValidVerificationInfoAccept? = null - var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null - - val aliceAcceptedLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}") - if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { - val at = tx as SASDefaultVerificationTransaction - accepted = at.accepted - startReq = at.startReq - aliceAcceptedLatch.countDown() - } - } - } - aliceVerificationService.addListener(aliceListener) - - val bobListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}") - if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - bobVerificationService.removeListener(this) - val at = tx as IncomingSasVerificationTransaction - at.performAccept() - } - } - } - bobVerificationService.addListener(bobListener) - - val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - testHelper.await(aliceAcceptedLatch) - - assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) - - // check that agreement is valid - assertTrue("Agreed Protocol should be Valid", accepted != null) - assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol)) - assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash)) - assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode)) - - accepted!!.shortAuthenticationStrings.forEach { - assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it)) - } - } - - @Test - fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as OutgoingSasVerificationTransaction).uxState - when (uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - aliceSASLatch.countDown() - } - else -> Unit - } - } - } - aliceVerificationService.addListener(aliceListener) - - val bobSASLatch = CountDownLatch(1) - val bobListener = object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as IncomingSasVerificationTransaction).uxState - when (uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - tx.performAccept() - } - else -> Unit - } - if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { - bobSASLatch.countDown() - } - } - } - bobVerificationService.addListener(bobListener) - - val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - testHelper.await(aliceSASLatch) - testHelper.await(bobSASLatch) - - val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction - val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction - - assertEquals( - "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), - bobTx.getShortCodeRepresentation(SasMode.DECIMAL) - ) - } - - @Test - fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.Listener { - var matchOnce = true - override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as OutgoingSasVerificationTransaction).uxState - Log.v("TEST", "== aliceState ${uxState.name}") - when (uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - tx.userHasVerifiedShortCode() - } - OutgoingSasVerificationTransaction.UxState.VERIFIED -> { - if (matchOnce) { - matchOnce = false - aliceSASLatch.countDown() - } - } - else -> Unit - } - } - } - aliceVerificationService.addListener(aliceListener) - - val bobSASLatch = CountDownLatch(1) - val bobListener = object : VerificationService.Listener { - var acceptOnce = true - var matchOnce = true - override fun transactionUpdated(tx: VerificationTransaction) { - val uxState = (tx as IncomingSasVerificationTransaction).uxState - Log.v("TEST", "== bobState ${uxState.name}") - when (uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - if (acceptOnce) { - acceptOnce = false - tx.performAccept() - } - } - IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - if (matchOnce) { - matchOnce = false - tx.userHasVerifiedShortCode() - } - } - IncomingSasVerificationTransaction.UxState.VERIFIED -> { - bobSASLatch.countDown() - } - else -> Unit - } - } - } - bobVerificationService.addListener(bobListener) - - val bobUserId = bobSession.myUserId - val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId - aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) - testHelper.await(aliceSASLatch) - testHelper.await(bobSASLatch) - - // Assert that devices are verified - val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = - bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId) - - assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified) - assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified) - } - - @Test - fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession - - val aliceVerificationService = aliceSession.cryptoService().verificationService() - val bobVerificationService = bobSession!!.cryptoService().verificationService() - - val req = aliceVerificationService.requestKeyVerificationInDMs( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - bobSession.myUserId, - cryptoTestData.roomId - ) - - var requestID: String? = null - - testHelper.retryPeriodically { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - requestID = prAlicePOV?.transactionId - Log.v("TEST", "== alicePOV is $prAlicePOV") - prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId - } - - Log.v("TEST", "== requestID is $requestID") - - testHelper.retryPeriodically { - val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() - Log.v("TEST", "== prBobPOV is $prBobPOV") - prBobPOV?.transactionId == requestID - } - - bobVerificationService.readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceSession.myUserId, - requestID!! - ) - - // wait for alice to get the ready - testHelper.retryPeriodically { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - Log.v("TEST", "== prAlicePOV is $prAlicePOV") - prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null - } - - // Start concurrent! - aliceVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID!!, - cryptoTestData.roomId, - bobSession.myUserId, - bobSession.sessionParams.deviceId!! - ) - - bobVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID!!, - cryptoTestData.roomId, - aliceSession.myUserId, - aliceSession.sessionParams.deviceId!! - ) - - // we should reach SHOW SAS on both - var alicePovTx: SasVerificationTransaction? - var bobPovTx: SasVerificationTransaction? - - testHelper.retryPeriodically { - alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== alicePovTx is $alicePovTx") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } - // wait for alice to get the ready - testHelper.retryPeriodically { - bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== bobPovTx is $bobPovTx") - bobPovTx?.state == VerificationTxState.ShortCodeReady - } - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt new file mode 100644 index 000000000..35fe349b1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SasVerificationTestHelper.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.getRequest +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestData + +class SasVerificationTestHelper(private val testHelper: CommonTestHelper) { + suspend fun requestVerificationAndWaitForReadyState( + scope: CoroutineScope, + cryptoTestData: CryptoTestData, supportedMethods: List + ): String { + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() + + val bobSeesVerification = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request != null) { + bobSeesVerification.complete(request) + return@collect cancel() + } + } + } + + val bobUserId = bobSession.myUserId + // Step 1: Alice starts a verification request + val transactionId = aliceVerificationService.requestKeyVerificationInDMs( + supportedMethods, bobUserId, cryptoTestData.roomId + ).transactionId + + val aliceReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + aliceVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request?.state == EVerificationState.Ready) { + aliceReady.complete(request) + return@collect cancel() + } + } + } + + bobSeesVerification.await() + bobVerificationService.readyPendingVerification( + supportedMethods, + aliceSession.myUserId, + transactionId + ) + + aliceReady.await() + return transactionId + } + + suspend fun requestSelfKeyAndWaitForReadyState(session1: Session, session2: Session, supportedMethods: List): String { + val session1VerificationService = session1.cryptoService().verificationService() + val session2VerificationService = session2.cryptoService().verificationService() + + val requestID = session1VerificationService.requestSelfKeyVerification(supportedMethods).transactionId + + val myUserId = session1.myUserId + testHelper.retryWithBackoff { + val incomingRequest = session2VerificationService.getExistingVerificationRequest(myUserId, requestID) + if (incomingRequest != null) { + session2VerificationService.readyPendingVerification( + supportedMethods, + myUserId, + incomingRequest.transactionId + ) + true + } else { + false + } + } + + // wait for alice to see the ready + testHelper.retryPeriodically { + val pendingRequest = session1VerificationService.getExistingVerificationRequest(myUserId, requestID) + pendingRequest?.state == EVerificationState.Ready + } + + return requestID + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt new file mode 100644 index 000000000..aacf6b3f0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTest.kt @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.launch +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.getRequest +import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class VerificationTest : InstrumentedTest { + + data class ExpectedResult( + val sasIsSupported: Boolean = false, + val otherCanScanQrCode: Boolean = false, + val otherCanShowQrCode: Boolean = false + ) + + private val sas = listOf( + VerificationMethod.SAS + ) + + private val sasShow = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW + ) + + private val sasScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SCAN + ) + + private val sasShowScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW, + VerificationMethod.QR_CODE_SCAN + ) + + @Test + fun test_aliceAndBob_sas_sas() = doTest( + sas, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_show() = doTest( + sas, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_sas() = doTest( + sasShow, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_scan() = doTest( + sas, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_sas() = doTest( + sasScan, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_scan() = doTest( + sasScan, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_show() = doTest( + sasShow, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_scan() = doTest( + sasShow, + sasScan, + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true) + ) + + @Test + fun test_aliceAndBob_scan_show() = doTest( + sasScan, + sasShow, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true) + ) + + @Test + fun test_aliceAndBob_all_all() = doTest( + sasShowScan, + sasShowScan, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true) + ) + + private fun doTest( + aliceSupportedMethods: List, + bobSupportedMethods: List, + expectedResultForAlice: ExpectedResult, + expectedResultForBob: ExpectedResult + ) = runCryptoTest(context()) { cryptoTestHelper, _ -> + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + + cryptoTestHelper.initializeCrossSigning(aliceSession) + cryptoTestHelper.initializeCrossSigning(bobSession) + + val scope = CoroutineScope(SupervisorJob()) + + val aliceVerificationService = aliceSession.cryptoService().verificationService() + val bobVerificationService = bobSession.cryptoService().verificationService() + + val bobSeesVerification = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request != null) { + bobSeesVerification.complete(request) + return@collect cancel() + } + } + } + + val aliceReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + aliceVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request?.state == EVerificationState.Ready) { + aliceReady.complete(request) + return@collect cancel() + } + } + } + val bobReady = CompletableDeferred() + scope.launch(Dispatchers.IO) { + bobVerificationService.requestEventFlow() + .cancellable() + .collect { + val request = it.getRequest() + if (request?.state == EVerificationState.Ready) { + bobReady.complete(request) + return@collect cancel() + } + } + } + + val requestID = aliceVerificationService.requestKeyVerificationInDMs( + methods = aliceSupportedMethods, + otherUserId = bobSession.myUserId, + roomId = cryptoTestData.roomId + ).transactionId + + bobSeesVerification.await() + bobVerificationService.readyPendingVerification( + bobSupportedMethods, + aliceSession.myUserId, + requestID + ) + val aliceRequest = aliceReady.await() + val bobRequest = bobReady.await() + + aliceRequest.let { pr -> + pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported + pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode + } + + bobRequest.let { pr -> + pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported + pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode + } + + scope.cancel() + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt deleted file mode 100644 index d7b4d636f..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeNull -import org.amshove.kluent.shouldNotBeNull -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class QrCodeTest : InstrumentedTest { - - private val qrCode1 = QrCodeData.VerifyingAnotherUser( - transactionId = "MaTransaction", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value1 = - "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" - - private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted( - transactionId = "MaTransaction", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value2 = - "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678" - - private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted( - transactionId = "MaTransaction", - deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU", - userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU", - sharedSecret = "MTIzNDU2Nzg" - ) - - private val value3 = - "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678" - - private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1) - - private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5") - - private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55") - - @Test - fun testEncoding1() { - qrCode1.toEncodedString() shouldBeEqualTo value1 - } - - @Test - fun testEncoding2() { - qrCode2.toEncodedString() shouldBeEqualTo value2 - } - - @Test - fun testEncoding3() { - qrCode3.toEncodedString() shouldBeEqualTo value3 - } - - @Test - fun testSymmetry1() { - qrCode1.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode1 - } - - @Test - fun testSymmetry2() { - qrCode2.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode2 - } - - @Test - fun testSymmetry3() { - qrCode3.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode3 - } - - @Test - fun testCase1() { - val url = qrCode1.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 0 - - checkSizeAndTransaction(byteArray) - - compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testCase2() { - val url = qrCode2.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 1 - - checkSizeAndTransaction(byteArray) - compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testCase3() { - val url = qrCode3.toEncodedString() - - val byteArray = url.toByteArray(Charsets.ISO_8859_1) - checkHeader(byteArray) - - // Mode - byteArray[7] shouldBeEqualTo 2 - - checkSizeAndTransaction(byteArray) - compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray) - compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray) - - compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray) - } - - @Test - fun testLongTransactionId() { - // Size on two bytes (2_000 = 0x07D0) - val longTransactionId = "PatternId_".repeat(200) - - val qrCode = qrCode1.copy(transactionId = longTransactionId) - - val result = qrCode.toEncodedString() - val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId") - - result shouldBeEqualTo expected - - // Reverse operation - expected.toQrCodeData() shouldBeEqualTo qrCode - } - - @Test - fun testAnyTransactionId() { - for (qty in 0 until 0x1FFF step 200) { - val longTransactionId = "a".repeat(qty) - - val qrCode = qrCode1.copy(transactionId = longTransactionId) - - // Symmetric operation - qrCode.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode - } - } - - // Error cases - @Test - fun testErrorHeader() { - value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull() - value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull() - value1.replace("MATRIX", "").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorVersion() { - value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull() - value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorSecretTooShort() { - value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorNoTransactionNoKeyNoSecret() { - // But keep transaction length - "MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorNoKeyNoSecret() { - "MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull() - } - - @Test - fun testErrorTransactionLengthTooShort() { - // In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch - value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull() - } - - @Test - fun testErrorTransactionLengthTooBig() { - value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull() - } - - private fun compareArray(actual: ByteArray, expected: ByteArray) { - actual.size shouldBeEqualTo expected.size - - for (i in actual.indices) { - actual[i] shouldBeEqualTo expected[i] - } - } - - private fun checkHeader(byteArray: ByteArray) { - // MATRIX - byteArray[0] shouldBeEqualTo 'M'.code.toByte() - byteArray[1] shouldBeEqualTo 'A'.code.toByte() - byteArray[2] shouldBeEqualTo 'T'.code.toByte() - byteArray[3] shouldBeEqualTo 'R'.code.toByte() - byteArray[4] shouldBeEqualTo 'I'.code.toByte() - byteArray[5] shouldBeEqualTo 'X'.code.toByte() - - // Version - byteArray[6] shouldBeEqualTo 2 - } - - private fun checkSizeAndTransaction(byteArray: ByteArray) { - // Size - byteArray[8] shouldBeEqualTo 0 - byteArray[9] shouldBeEqualTo 13 - - // Transaction - byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldBeEqualTo "MaTransaction" - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt deleted file mode 100644 index 9b10f9e9a..000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecretTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldNotBeEqualTo -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest - -@RunWith(AndroidJUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class SharedSecretTest : InstrumentedTest { - - @Test - fun testSharedSecretLengthCase() { - repeat(100) { - generateSharedSecretV2().length shouldBe 11 - } - } - - @Test - fun testSharedDiffCase() { - val sharedSecret1 = generateSharedSecretV2() - val sharedSecret2 = generateSharedSecretV2() - - sharedSecret1 shouldNotBeEqualTo sharedSecret2 - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 4ecfe5be8..38db134fd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Job +import kotlinx.coroutines.async import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Ignore @@ -29,14 +31,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -164,7 +165,6 @@ class VerificationTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! - testHelper.waitForCallback { callback -> aliceSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -177,11 +177,9 @@ class VerificationTest : InstrumentedTest { ) ) } - }, callback + } ) - } - testHelper.waitForCallback { callback -> bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -194,64 +192,50 @@ class VerificationTest : InstrumentedTest { ) ) } - }, callback + } ) - } val aliceVerificationService = aliceSession.cryptoService().verificationService() val bobVerificationService = bobSession.cryptoService().verificationService() - var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null - var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null - - val latch = CountDownLatch(2) - val aliceListener = object : VerificationService.Listener { - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - // Step 4: Alice receive the ready request - if (pr.isReady) { - aliceReadyPendingVerificationRequest = pr - latch.countDown() - } - } - } - aliceVerificationService.addListener(aliceListener) + val transactionId = aliceVerificationService.requestKeyVerificationInDMs( + aliceSupportedMethods, bobSession.myUserId, cryptoTestData.roomId + ) + .transactionId - val bobListener = object : VerificationService.Listener { - override fun verificationRequestCreated(pr: PendingVerificationRequest) { - // Step 2: Bob accepts the verification request - bobVerificationService.readyPendingVerificationInDMs( + testHelper.retryPeriodically { + val incomingRequest = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, transactionId) + if (incomingRequest != null) { + bobVerificationService.readyPendingVerification( bobSupportedMethods, aliceSession.myUserId, - cryptoTestData.roomId, - pr.transactionId!! + incomingRequest.transactionId ) + true + } else { + false } + } - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - // Step 3: Bob is ready - if (pr.isReady) { - bobReadyPendingVerificationRequest = pr - latch.countDown() - } - } + // wait for alice to see the ready + testHelper.retryPeriodically { + val pendingRequest = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId, transactionId) + pendingRequest?.state == EVerificationState.Ready } - bobVerificationService.addListener(bobListener) - val bobUserId = bobSession.myUserId - // Step 1: Alice starts a verification request - aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) - testHelper.await(latch) + val aliceReadyPendingVerificationRequest = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId, transactionId)!! + val bobReadyPendingVerificationRequest = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, transactionId)!! - aliceReadyPendingVerificationRequest!!.let { pr -> - pr.isSasSupported() shouldBe expectedResultForAlice.sasIsSupported - pr.otherCanShowQrCode() shouldBe expectedResultForAlice.otherCanShowQrCode - pr.otherCanScanQrCode() shouldBe expectedResultForAlice.otherCanScanQrCode + aliceReadyPendingVerificationRequest.let { pr -> + pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported + pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode } - bobReadyPendingVerificationRequest!!.let { pr -> - pr.isSasSupported() shouldBe expectedResultForBob.sasIsSupported - pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode - pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode + bobReadyPendingVerificationRequest.let { pr -> + pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported + pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode + pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode } } @@ -273,21 +257,42 @@ class VerificationTest : InstrumentedTest { val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService() val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService() - serviceOfVerifier.addListener(object : VerificationService.Listener { - override fun verificationRequestCreated(pr: PendingVerificationRequest) { - // Accept verification request - serviceOfVerifier.readyPendingVerification( - verificationMethods, - pr.otherUserId, - pr.transactionId!!, - ) + var job: Job? = null + job = async { + serviceOfVerifier.requestEventFlow().collect { + when (it) { + is VerificationEvent.RequestAdded -> { + val pr = it.request + serviceOfVerifier.readyPendingVerification( + verificationMethods, + pr.otherUserId, + pr.transactionId, + ) + job?.cancel() + } + is VerificationEvent.RequestUpdated, + is VerificationEvent.TransactionAdded, + is VerificationEvent.TransactionUpdated -> { + } + } } - }) - - serviceOfVerified.requestKeyVerification( + } + job.await() +// serviceOfVerifier.addListener(object : VerificationService.Listener { +// override fun verificationRequestCreated(pr: PendingVerificationRequest) { +// // Accept verification request +// runBlocking { +// serviceOfVerifier.readyPendingVerification( +// verificationMethods, +// pr.otherUserId, +// pr.transactionId!!, +// ) +// } +// } +// }) + + serviceOfVerified.requestSelfKeyVerification( methods = verificationMethods, - otherUserId = aliceSessionToVerify.myUserId, - otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId), ) testHelper.retryPeriodically { @@ -295,8 +300,8 @@ class VerificationTest : InstrumentedTest { requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } } - testHelper.signOutAndClose(aliceSessionToVerify) - testHelper.signOutAndClose(aliceSessionThatVerifies) - testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent) +// testHelper.signOutAndClose(aliceSessionToVerify) +// testHelper.signOutAndClose(aliceSessionThatVerifies) +// testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt index 2643bf643..828c0f51d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/CryptoSanityMigrationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Matrix.org Foundation C.I.C. + * Copyright 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,22 @@ package org.matrix.android.sdk.internal.database import android.content.Context import androidx.test.platform.app.InstrumentationRegistry +import io.mockk.spyk import io.realm.Realm import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.TestBuildVersionSdkIntProvider +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils +import org.matrix.android.sdk.internal.crypto.RustEncryptionConfiguration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule +import org.matrix.android.sdk.internal.crypto.store.db.RustMigrationInfoProvider import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.olm.OlmManager +import java.io.File +import java.security.KeyStore class CryptoSanityMigrationTest { @get:Rule val configurationFactory = TestRealmConfigurationFactory() @@ -35,6 +43,8 @@ class CryptoSanityMigrationTest { @Before fun setUp() { + // Ensure Olm is initialized + OlmManager() context = InstrumentationRegistry.getInstrumentation().context } @@ -43,14 +53,31 @@ class CryptoSanityMigrationTest { realm?.close() } + private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) } + @Test fun cryptoDatabaseShouldMigrateGracefully() { val realmName = "crypto_store_20.realm" - val migration = RealmCryptoStoreMigration(object : Clock { - override fun epochMillis(): Long { - return 0L - } - }) + + val rustMigrationInfo = RustMigrationInfoProvider( + File(configurationFactory.root, "test_rust"), + RustEncryptionConfiguration( + "foo", + RealmKeysUtils( + context, + SecretStoringUtils(context, keyStore, TestBuildVersionSdkIntProvider(), false) + ) + ), + ) + val migration = RealmCryptoStoreMigration( + object : Clock { + override fun epochMillis(): Long { + return 0L + } + }, + rustMigrationInfo + ) + val realmConfiguration = configurationFactory.createConfiguration( realmName, "7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca", diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt index 3a267ec69..a0986cc55 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt @@ -48,4 +48,8 @@ class TestPermalinkService : PermalinkService { MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)" } } + + override fun isPermalinkSupported(supportedHosts: Array, url: String): Boolean { + return false + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt index a52e3cd7c..fd8065f1e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.session.room.timeline +import android.util.Log +import kotlinx.coroutines.CompletableDeferred import org.amshove.kluent.fail import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo @@ -45,8 +47,9 @@ import java.util.concurrent.CountDownLatch @FixMethodOrder(MethodSorters.JVM) class PollAggregationTest : InstrumentedTest { + // This test needs to be refactored, I am not sure it's working properly @Test - fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, _ -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) val aliceSession = cryptoTestData.firstSession @@ -57,14 +60,14 @@ class PollAggregationTest : InstrumentedTest { // Bob creates a poll roomFromBobPOV.sendService().sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions) - aliceSession.syncService().startSync(true) val aliceTimeline = roomFromAlicePOV.timelineService().createTimeline(null, TimelineSettings(30)) - aliceTimeline.start() val TOTAL_TEST_COUNT = 7 val lock = CountDownLatch(TOTAL_TEST_COUNT) + val deff = CompletableDeferred() val aliceEventsListener = object : Timeline.Listener { + override fun onTimelineUpdated(snapshot: List) { snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent -> val pollEventId = pollEvent.eventId @@ -123,21 +126,28 @@ class PollAggregationTest : InstrumentedTest { fail("Lock count ${lock.count} didn't handled.") } } + + if (lock.count.toInt() == 0) deff.complete(Unit) } } } + aliceTimeline.start() + aliceTimeline.addListener(aliceEventsListener) - commonTestHelper.await(lock) + // QUICK FIX + // This was locking the thread thus blocking the timeline updates + // Changed to a suspendable but this test is not well constructed.. +// commonTestHelper.await(lock) + deff.await() aliceTimeline.removeAllListeners() - - aliceSession.syncService().stopSync() aliceTimeline.dispose() } private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testInitialPollConditions") // No votes yet, poll summary should be null pollSummary shouldBe null // Question should be the same as intended @@ -150,6 +160,7 @@ class PollAggregationTest : InstrumentedTest { } private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testBobVotesOption1") if (pollSummary == null) { fail("Poll summary shouldn't be null when someone votes") return @@ -165,6 +176,7 @@ class PollAggregationTest : InstrumentedTest { } private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testBobChangesVoteToOption2") if (pollSummary == null) { fail("Poll summary shouldn't be null when someone votes") return @@ -180,6 +192,7 @@ class PollAggregationTest : InstrumentedTest { } private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testAliceAndBobVoteToOption2") if (pollSummary == null) { fail("Poll summary shouldn't be null when someone votes") return @@ -196,6 +209,7 @@ class PollAggregationTest : InstrumentedTest { } private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testAliceVotesOption1AndBobVotesOption2") if (pollSummary == null) { fail("Poll summary shouldn't be null when someone votes") return @@ -215,10 +229,12 @@ class PollAggregationTest : InstrumentedTest { } private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) { + Log.v("#E2E TEST", "testEndedPoll") pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0 } private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) { + Log.v("#E2E TEST", "assertTotalVotesCount") aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index df131cc19..9c72c2161 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -124,8 +124,8 @@ class SpaceCreationTest : InstrumentedTest { assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name) assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic) - commonTestHelper.signOutAndClose(aliceSession) - commonTestHelper.signOutAndClose(bobSession) +// commonTestHelper.signOutAndClose(aliceSession) +// commonTestHelper.signOutAndClose(bobSession) } @Test diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index abe9af5e3..de661275a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -334,7 +334,7 @@ class SpaceHierarchyTest : InstrumentedTest { } ) - commonTestHelper.signOutAndClose(session) +// commonTestHelper.signOutAndClose(session) } data class TestSpaceCreationResult( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 953ebddcb..8893229a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.debug.DebugService -import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.network.ApiInterceptorListener import org.matrix.android.sdk.api.network.ApiPath import org.matrix.android.sdk.api.raw.RawService @@ -55,7 +54,6 @@ import javax.inject.Inject */ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { - @Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var rawService: RawService @Inject internal lateinit var debugService: DebugService @@ -118,11 +116,6 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { */ fun homeServerHistoryService() = homeServerHistoryService - /** - * Return the legacy session importer, useful if you want to migrate an app, which was using the legacy Matrix Android Sdk. - */ - fun legacySessionImporter() = legacySessionImporter - /** * Returns the SecureStorageService used to encrypt and decrypt sensitive data. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 68b6a2ddf..52d4d70b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api import okhttp3.ConnectionSpec import okhttp3.Interceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.metrics.CryptoMetricPlugin import org.matrix.android.sdk.api.metrics.MetricPlugin import org.matrix.android.sdk.api.provider.CustomEventTypesProvider import org.matrix.android.sdk.api.provider.MatrixItemDisplayNameFallbackProvider @@ -82,6 +83,8 @@ data class MatrixConfiguration( * Metrics plugin that can be used to capture metrics from matrix-sdk-android. */ val metricPlugins: List = emptyList(), + + val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null, /** * CustomEventTypesProvider to provide custom event types to the sdk which should be processed with internal events. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 2de95850b..0f7e9ca6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -62,7 +62,7 @@ object MatrixPatterns { // regex pattern to find permalink with message id. // Android does not support in URL so extract it. private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" - private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" + private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/#/(room|user)/" const val SEP_REGEX = "/" private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt index e3728753a..e57eb4c08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt @@ -49,7 +49,7 @@ data class Credentials( /** * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. */ - @Json(name = "device_id") val deviceId: String?, + @Json(name = "device_id") val deviceId: String, /** * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to * reconfigure themselves, optionally validating the URLs within. @@ -59,5 +59,5 @@ data class Credentials( ) internal fun Credentials.sessionId(): String { - return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5() + return (if (deviceId.isBlank()) userId else "$userId|$deviceId").md5() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt index 384dcdce4..94390e2ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/DiscoveryInformation.kt @@ -36,5 +36,11 @@ data class DiscoveryInformation( * Note: matrix.org does not send this field */ @Json(name = "m.identity_server") - val identityServer: WellKnownBaseConfig? = null + val identityServer: WellKnownBaseConfig? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager. + */ + @Json(name = "io.element.disable_network_constraint") + val disableNetworkConstraint: Boolean? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt index 95488bd68..2f5863d1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -61,4 +61,10 @@ data class WellKnown( */ @Json(name = "org.matrix.msc2965.authentication") val unstableDelegatedAuthConfig: DelegatedAuthConfig? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager. + */ + @Json(name = "io.element.disable_network_constraint") + val disableNetworkConstraint: Boolean? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt index 145cdbdc2..79603d89d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.util.JsonDict /** * Set of methods to be able to login to an existing account on a homeserver. * - * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md + * More documentation can be found in the file https://github.com/element-hq/element-android/blob/main/docs/signin.md */ interface LoginWizard { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt index 995fd27ac..df70833f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.util.JsonDict * - Call [createAccount] to start the account creation * - Fulfill all mandatory stages using the methods [performReCaptcha] [acceptTerms] [dummy], etc. * - * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md + * More documentation can be found in the file https://github.com/element-hq/element-android/blob/main/docs/signup.md * and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management */ interface RegistrationWizard { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt index 37b9ac379..aced0ca3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt @@ -42,3 +42,6 @@ const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2" // TODO Refacto: use this constants everywhere const val ed25519 = "ed25519" const val curve25519 = "curve25519" + +const val MEGOLM_DEFAULT_ROTATION_MSGS = 100L +const val MEGOLM_DEFAULT_ROTATION_PERIOD_MS = 7 * 24 * 3600 * 1000L diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt index 9f979098f..ec5a8bc64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Strings.kt @@ -16,6 +16,11 @@ package org.matrix.android.sdk.api.extensions +import java.util.regex.Pattern + +const val emailPattern = "^[a-zA-Z0-9_!#\$%&'*+/=?`{|}~^-]+(?:\\.[a-zA-Z0-9_!#\$%&'*+/=?`{|}~^-]+)*@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*\$" +val emailAddress: Pattern = Pattern.compile(emailPattern) + fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { return when { startsWith(prefix) -> this @@ -23,6 +28,11 @@ fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence { } } +/** + * Check if a CharSequence is an email. + */ +fun CharSequence.isEmail() = emailAddress.matcher(this).matches() + /** * Append a new line and then the provided string. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt index 4b87507c0..e04d3b6e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/listeners/StepProgressListener.kt @@ -24,6 +24,7 @@ interface StepProgressListener { sealed class Step { data class ComputingKey(val progress: Int, val total: Int) : Step() object DownloadingKey : Step() + data class DecryptingKey(val progress: Int, val total: Int) : Step() data class ImportingKey(val progress: Int, val total: Int) : Step() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt new file mode 100644 index 000000000..1c8a6089a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.metrics + +import android.util.LruCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.crypto.MXCryptoError + +sealed class CryptoEvent { + + data class FailedToDecryptToDevice( + val error: String? + ) : CryptoEvent() + + data class FailedToSendToDevice(val eventTye: String) : CryptoEvent() + + data class UnableToDecryptRoomMessage( + val sessionId: String, + val error: String? + ) : CryptoEvent() + + data class LateDecryptRoomMessage(val sessionId: String, val source: String) : CryptoEvent() +} + +abstract class CryptoMetricPlugin { + + internal sealed class Report { + data class RoomE2EEReport(val error: MXCryptoError.Base, val sessionId: String) : Report() + data class ToDeviceDecryptReport(val error: Throwable) : Report() + data class ToDeviceSendReport(val error: Throwable) : Report() + data class OnRoomKeyImported(val sessionId: String, val source: String) : Report() + } + + // should I scope that to some parent job? + val scope = CoroutineScope(SupervisorJob()) + + private val channel = Channel(capacity = Channel.UNLIMITED) + + // Basic to avoid double reporting for same session and detect late reception + private val uisiCache = LruCache(200) + + init { + scope.launch { + for (ev in channel) { + handleEvent(ev) + } + } + } + + private fun handleEvent(ev: Report) { + when (ev) { + is Report.RoomE2EEReport -> { + if (uisiCache.get(ev.sessionId) == null) { + uisiCache.put(ev.sessionId, Unit) + captureEvent( + CryptoEvent.UnableToDecryptRoomMessage( + sessionId = ev.sessionId, + error = ev.error.errorType.toString() + ) + ) + } + } + is Report.ToDeviceDecryptReport -> { + captureEvent(CryptoEvent.FailedToDecryptToDevice(ev.error.message.toString())) + } + is Report.ToDeviceSendReport -> { + captureEvent(CryptoEvent.FailedToSendToDevice(ev.error.message.orEmpty())) + } + is Report.OnRoomKeyImported -> { + if (uisiCache.get(ev.sessionId) != null) { + // ok we have an uisi for this session + captureEvent( + CryptoEvent.LateDecryptRoomMessage( + sessionId = ev.sessionId, + source = ev.source + ) + ) + } + } + } + } + + fun onFailedToDecryptRoomMessage(error: MXCryptoError.Base, sessionId: String) { + channel.trySend( + Report.RoomE2EEReport(error, sessionId) + ) + } + + fun onFailToSendToDevice(failure: Throwable) { + channel.trySend( + Report.ToDeviceSendReport(failure) + ) + } + fun onFailToDecryptToDevice(failure: Throwable) { + channel.trySend( + Report.ToDeviceDecryptReport(failure) + ) + } + + fun onRoomKeyImported(sessionId: String, source: String) { + channel.trySend( + Report.OnRoomKeyImported(sessionId = sessionId, source = source) + ) + } + + protected abstract fun captureEvent(cryptoEvent: CryptoEvent) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt index 37d9b46b0..6dc9f315a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt @@ -25,6 +25,10 @@ package org.matrix.android.sdk.api.provider * *Limitation*: if the locale of the device changes, the methods will not be called again. */ interface RoomDisplayNameFallbackProvider { + /** + * Return the list of user ids to ignore when computing the room display name. + */ + fun excludedUserIds(roomId: String): List fun getNameForRoomInvite(): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String fun getNameFor1member(name: String): String diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt index 28d8230be..d5596ce56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt @@ -34,14 +34,7 @@ import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgori import org.matrix.android.sdk.api.rendezvous.transports.SimpleHttpRendezvousTransport import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.util.MatrixJsonParser -import org.matrix.android.sdk.api.util.awaitCallback import timber.log.Timber /** @@ -168,13 +161,13 @@ class Rendezvous( suspend fun completeVerificationOnNewDevice(session: Session) { val userId = session.myUserId val crypto = session.cryptoService() - val deviceId = crypto.getMyDevice().deviceId - val deviceKey = crypto.getMyDevice().fingerprint() + val deviceId = crypto.getMyCryptoDevice().deviceId + val deviceKey = crypto.getMyCryptoDevice().fingerprint() send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey)) try { // explicitly download keys for ourself rather than racing with initial sync which might not complete in time - awaitCallback> { crypto.downloadKeys(listOf(userId), false, it) } + crypto.downloadKeysIfNeeded(listOf(userId), false) } catch (e: Throwable) { // log as warning and continue as initial sync might still complete Timber.tag(TAG).w(e, "Failed to download keys for self") @@ -225,15 +218,10 @@ class Rendezvous( Timber.tag(TAG).i("No master key given by verifying device") } - // request secrets from the verifying device - Timber.tag(TAG).i("Requesting secrets from $verifyingDeviceId") + // request secrets from other sessions. + Timber.tag(TAG).i("Requesting secrets from other sessions") - session.sharedSecretStorageService().let { - it.requestSecret(MASTER_KEY_SSSS_NAME, verifyingDeviceId) - it.requestSecret(SELF_SIGNING_KEY_SSSS_NAME, verifyingDeviceId) - it.requestSecret(USER_SIGNING_KEY_SSSS_NAME, verifyingDeviceId) - it.requestSecret(KEYBACKUP_SECRET_SSSS_NAME, verifyingDeviceId) - } + session.sharedSecretStorageService().requestMissingSecrets() } else { Timber.tag(TAG).i("Not doing verification") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt index 71b22da33..bcde4a2a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt @@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.rendezvous.RendezvousTransport import org.matrix.android.sdk.api.rendezvous.model.RendezvousError import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm import org.matrix.android.sdk.api.util.MatrixJsonParser -import org.matrix.android.sdk.internal.crypto.verification.SASDefaultVerificationTransaction +import org.matrix.android.sdk.internal.crypto.verification.getDecimalCodeRepresentation import org.matrix.olm.OlmSAS import timber.log.Timber import java.security.SecureRandom @@ -125,7 +125,7 @@ class ECDHRendezvousChannel( aesKey = sas.generateShortCode(aesInfo, 32) val rawChecksum = sas.generateShortCode(aesInfo, 5) - return SASDefaultVerificationTransaction.getDecimalCodeRepresentation(rawChecksum, separator = "-") + return rawChecksum.getDecimalCodeRepresentation(separator = "-") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 971d04261..3ed6dd145 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -20,7 +20,6 @@ import android.content.Context import androidx.annotation.Size import androidx.lifecycle.LiveData import androidx.paging.PagedList -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -30,10 +29,8 @@ import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListen import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap @@ -41,22 +38,28 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse +import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.model.SessionInfo +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator interface CryptoService { + fun name(): String fun verificationService(): VerificationService fun crossSigningService(): CrossSigningService fun keysBackupService(): KeysBackupService - fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) + suspend fun setDeviceName(deviceId: String, deviceName: String) - fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) - fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + suspend fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) fun getCryptoVersion(context: Context, longFormat: Boolean): String @@ -68,15 +71,9 @@ interface CryptoService { fun setWarnOnUnknownDevices(warn: Boolean) - fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) + suspend fun getUserDevices(userId: String): List - fun getUserDevices(userId: String): MutableList - - fun setDevicesKnown(devices: List, callback: MatrixCallback?) - - fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? - - fun getMyDevice(): CryptoDeviceInfo + suspend fun getMyCryptoDevice(): CryptoDeviceInfo fun getGlobalBlacklistUnverifiedDevices(): Boolean @@ -84,6 +81,8 @@ interface CryptoService { fun getLiveGlobalCryptoConfig(): LiveData + fun supportsDisablingKeyGossiping(): Boolean + /** * Enable or disable key gossiping. * Default is true. @@ -93,6 +92,14 @@ interface CryptoService { fun isKeyGossipingEnabled(): Boolean + /* + * Tells if the current crypto implementation supports MSC3061 + */ + fun supportsShareKeysOnInvite(): Boolean + + fun supportsKeyWithheld(): Boolean + fun supportsForwardedKeyWiththeld(): Boolean + /** * As per MSC3061. * If true will make it possible to share part of e2ee room history @@ -109,7 +116,7 @@ interface CryptoService { fun setRoomUnBlockUnverifiedDevices(roomId: String) - fun getDeviceTrackingStatus(userId: String): Int +// fun getDeviceTrackingStatus(userId: String): Int suspend fun importRoomKeys( roomKeysAsArray: ByteArray, @@ -121,11 +128,11 @@ interface CryptoService { fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) - fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? - fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback) + suspend fun getCryptoDeviceInfo(userId: String): List - fun getCryptoDeviceInfo(userId: String): List +// fun getCryptoDeviceInfoFlow(userId: String): Flow> fun getLiveCryptoDeviceInfo(): LiveData> @@ -135,15 +142,13 @@ interface CryptoService { fun getLiveCryptoDeviceInfo(userIds: List): LiveData> - fun requestRoomKeyForEvent(event: Event) - - fun reRequestRoomKeyForEvent(event: Event) + suspend fun reRequestRoomKeyForEvent(event: Event) fun addRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener) - fun fetchDevicesList(callback: MatrixCallback) + suspend fun fetchDevicesList(): List fun getMyDevicesInfo(): List @@ -151,34 +156,41 @@ interface CryptoService { fun getMyDevicesInfoLive(deviceId: String): LiveData> - fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo + + suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun isRoomEncrypted(roomId: String): Boolean // TODO This could be removed from this interface - fun encryptEventContent( + suspend fun encryptEventContent( eventContent: Content, eventType: String, - roomId: String, - callback: MatrixCallback - ) + roomId: String + ): MXEncryptEventContentResult fun discardOutboundSession(roomId: String) @Throws(MXCryptoError::class) suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult - fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) - fun getEncryptionAlgorithm(roomId: String): String? fun shouldEncryptForInvitedMembers(roomId: String): Boolean - fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) + suspend fun downloadKeysIfNeeded(userIds: List, forceDownload: Boolean = false): MXUsersDevicesMap + + suspend fun getCryptoDeviceInfoList(userId: String): List + +// fun getLiveCryptoDeviceInfoList(userId: String): Flow> +// +// fun getLiveCryptoDeviceInfoList(userIds: List): Flow> fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) + fun supportKeyRequestInspection(): Boolean + fun getOutgoingRoomKeyRequests(): List fun getOutgoingRoomKeyRequestsPaged(): LiveData> @@ -202,10 +214,37 @@ interface CryptoService { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. */ - fun prepareToEncrypt(roomId: String, callback: MatrixCallback) + suspend fun prepareToEncrypt(roomId: String) /** * Share all inbound sessions of the last chunk messages to the provided userId devices. */ suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) + + /** + * When LL all room members might not be loaded when setting up encryption. + * This is called after room members have been loaded + * ... not sure if shoud be API + */ + fun onE2ERoomMemberLoadedFromServer(roomId: String) + + suspend fun deviceWithIdentityKey(userId: String, senderKey: String, algorithm: String): CryptoDeviceInfo? + suspend fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) + + fun close() + fun start() + suspend fun onSyncWillProcess(isInitialSync: Boolean) + fun isStarted(): Boolean + + suspend fun receiveSyncChanges( + toDevice: ToDeviceSyncResponse?, + deviceChanges: DeviceListResponse?, + keyCounts: DeviceOneTimeKeysCountSyncResponse?, + deviceUnusedFallbackKeyTypes: List?, + nextBatch: String?) + + suspend fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) + suspend fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {} + suspend fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) + fun logDbUsageInfo() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt index d9e841a50..6cdc36245 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt @@ -23,8 +23,7 @@ interface NewSessionListener { /** * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions - * @param senderKey the sender key of the device which the Megolm session is shared with * @param sessionId the session id of the Megolm session */ - fun onNewSession(roomId: String?, senderKey: String, sessionId: String) + fun onNewSession(roomId: String?, sessionId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 69f314f76..b8c88cf6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -17,76 +17,109 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.util.Optional interface CrossSigningService { + /** + * Is our published identity trusted. + */ + suspend fun isCrossSigningVerified(): Boolean - fun isCrossSigningVerified(): Boolean - - fun isUserTrusted(otherUserId: String): Boolean + // TODO this isn't used anywhere besides in tests? + // Is this the local trust concept that we have for devices? + suspend fun isUserTrusted(otherUserId: String): Boolean /** * Will not force a download of the key, but will verify signatures trust chain. * Checks that my trusted user key has signed the other user UserKey */ - fun checkUserTrust(otherUserId: String): UserTrustResult + suspend fun checkUserTrust(otherUserId: String): UserTrustResult /** * Initialize cross signing for this user. * Users needs to enter credentials */ - fun initializeCrossSigning( - uiaInterceptor: UserInteractiveAuthInterceptor?, - callback: MatrixCallback - ) + suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) - fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null + /** + * Does our own user have a valid cross signing identity uploaded. + * + * In other words has any of our devices uploaded public cross signing keys to the server. + */ + suspend fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null - fun checkTrustFromPrivateKeys( - masterKeyPrivateKey: String?, - uskKeyPrivateKey: String?, - sskPrivateKey: String? - ): UserTrustResult + /** + * Inject the private cross signing keys, likely from backup, into our store. + * + * This will check if the injected private cross signing keys match the public ones provided + * by the server and if they do so + */ + suspend fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String?): UserTrustResult - fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + /** + * Get the public cross signing keys for the given user. + * + * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. + */ + suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? fun getLiveCrossSigningKeys(userId: String): LiveData> - fun getMyCrossSigningKeys(): MXCrossSigningInfo? + /** Get our own public cross signing keys. */ + suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? - fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + /** Get our own private cross signing keys. */ + suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getLiveCrossSigningPrivateKeys(): LiveData> + /** + * Can we sign our other devices or other users? + * + * Returning true means that we have the private self-signing and user-signing keys at hand. + */ fun canCrossSign(): Boolean + /** Do we have all our private cross signing keys in storage? */ fun allPrivateKeysKnown(): Boolean - fun trustUser( - otherUserId: String, - callback: MatrixCallback - ) + /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server. */ + suspend fun trustUser(otherUserId: String) - fun markMyMasterKeyAsTrusted() + /** Mark our own master key as trusted. */ + suspend fun markMyMasterKeyAsTrusted() /** * Sign one of your devices and upload the signature. */ - fun trustDevice( - deviceId: String, - callback: MatrixCallback - ) + @Throws + suspend fun trustDevice(deviceId: String) - fun checkDeviceTrust( - otherUserId: String, - otherDeviceId: String, - locallyTrusted: Boolean? - ): DeviceTrustResult + suspend fun shieldForGroup(userIds: List): RoomEncryptionTrustLevel + + /** + * Check if a device is trusted + * + * This will check that we have a valid trust chain from our own master key to a device, either + * using the self-signing key for our own devices or using the user-signing key and the master + * key of another user. + */ + suspend fun checkDeviceTrust(otherUserId: String, + otherDeviceId: String, + // TODO what is locallyTrusted used for? + locallyTrusted: Boolean?): DeviceTrustResult // FIXME Those method do not have to be in the service - fun onSecretMSKGossip(mskPrivateKey: String) - fun onSecretSSKGossip(sskPrivateKey: String) - fun onSecretUSKGossip(uskPrivateKey: String) + // TODO those three methods doesn't seem to be used anywhere? + suspend fun onSecretMSKGossip(mskPrivateKey: String) + suspend fun onSecretSSKGossip(sskPrivateKey: String) + suspend fun onSecretUSKGossip(uskPrivateKey: String) + suspend fun checkTrustAndAffectedRoomShields(userIds: List) + fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult + fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt index 7fc815cd2..b8c9ba0b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt @@ -23,10 +23,11 @@ sealed class UserTrustResult { // data class UnknownDevice(val deviceID: String) : UserTrustResult() data class CrossSigningNotConfigured(val userID: String) : UserTrustResult() - data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult() - data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult() - data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult() - data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult() + data class Failure(val message: String) : UserTrustResult() +// data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult() +// data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult() +// data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult() +// data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult() } fun UserTrustResult.isVerified() = this is UserTrustResult.Success diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt new file mode 100644 index 000000000..b3b6ef1fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupRecoveryKey.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.keysbackup + +import org.matrix.rustcomponents.sdk.crypto.BackupRecoveryKey as InnerBackupRecoveryKey + +class BackupRecoveryKey internal constructor(internal val inner: InnerBackupRecoveryKey) : IBackupRecoveryKey { + + constructor() : this(InnerBackupRecoveryKey()) + + companion object { + + fun fromBase58(key: String): BackupRecoveryKey { + val inner = InnerBackupRecoveryKey.fromBase58(key) + return BackupRecoveryKey(inner) + } + + fun fromBase64(key: String): BackupRecoveryKey { + val inner = InnerBackupRecoveryKey.fromBase64(key) + return BackupRecoveryKey(inner) + } + + fun fromPassphrase(passphrase: String, salt: String, rounds: Int): BackupRecoveryKey { + val inner = InnerBackupRecoveryKey.fromPassphrase(passphrase, salt, rounds) + return BackupRecoveryKey(inner) + } + + fun newFromPassphrase(passphrase: String): BackupRecoveryKey { + val inner = InnerBackupRecoveryKey.newFromPassphrase(passphrase) + return BackupRecoveryKey(inner) + } + } + + override fun equals(other: Any?): Boolean { + if (other !is BackupRecoveryKey) return false + return this.toBase58() == other.toBase58() + } + + override fun hashCode(): Int { + return toBase58().hashCode() + } + + override fun toBase58() = inner.toBase58() + + override fun toBase64() = inner.toBase64() + + override fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String) = inner.decryptV1(ephemeralKey, mac, ciphertext) + + override fun megolmV1PublicKey() = megolmV1Key + + private val megolmV1Key = object : IMegolmV1PublicKey { + override val publicKey: String + get() = inner.megolmV1PublicKey().publicKey + override val privateKeySalt: String? + get() = inner.megolmV1PublicKey().passphraseInfo?.privateKeySalt + override val privateKeyIterations: Int? + get() = inner.megolmV1PublicKey().passphraseInfo?.privateKeyIterations + + override val backupAlgorithm: String + get() = inner.megolmV1PublicKey().backupAlgorithm + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt similarity index 56% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt index 52b09be49..f3f4e23b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/BackupUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto.verification.qrcode +package org.matrix.android.sdk.api.session.crypto.keysbackup -import org.matrix.android.sdk.api.util.toBase64NoPadding -import java.security.SecureRandom - -internal fun generateSharedSecretV2(): String { - val secureRandom = SecureRandom() - - // 8 bytes long - val secretBytes = ByteArray(8) - secureRandom.nextBytes(secretBytes) - return secretBytes.toBase64NoPadding() +object BackupUtils { + fun recoveryKeyFromBase58(key: String): IBackupRecoveryKey = BackupRecoveryKey.fromBase58(key) + fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey = BackupRecoveryKey.newFromPassphrase(passphrase) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/IBackupRecoveryKey.kt similarity index 53% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/IBackupRecoveryKey.kt index db2ea72e8..4ee459af8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/IncomingSasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/IBackupRecoveryKey.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,24 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.verification +package org.matrix.android.sdk.api.session.crypto.keysbackup -interface IncomingSasVerificationTransaction : SasVerificationTransaction { - val uxState: UxState +interface IBackupRecoveryKey { - fun performAccept() + fun toBase58(): String - enum class UxState { - UNKNOWN, - SHOW_ACCEPT, - WAIT_FOR_KEY_AGREEMENT, - SHOW_SAS, - WAIT_FOR_VERIFICATION, - VERIFIED, - CANCELLED_BY_ME, - CANCELLED_BY_OTHER - } + fun toBase64(): String + + fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String): String + + fun megolmV1PublicKey(): IMegolmV1PublicKey +} + +interface IMegolmV1PublicKey { + val publicKey: String + + val privateKeySalt: String? + val privateKeyIterations: Int? + + val backupAlgorithm: String } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt index 8745003f9..9dbb7b554 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt @@ -16,87 +16,74 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.StepProgressListener import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult interface KeysBackupService { + /** * Retrieve the current version of the backup from the homeserver. * * It can be different than keysBackupVersion. - * @param callback Asynchronous callback */ - fun getCurrentVersion(callback: MatrixCallback) + suspend fun getCurrentVersion(): KeysBackupLastVersionResult? /** * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion]. * * @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion]. - * @param callback Asynchronous callback + * @return KeysVersion */ - fun createKeysBackupVersion( - keysBackupCreationInfo: MegolmBackupCreationInfo, - callback: MatrixCallback - ) + @Throws + suspend fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo): KeysVersion /** * Facility method to get the total number of locally stored keys. */ - fun getTotalNumbersOfKeys(): Int + suspend fun getTotalNumbersOfKeys(): Int /** * Facility method to get the number of backed up keys. */ - fun getTotalNumbersOfBackedUpKeys(): Int + suspend fun getTotalNumbersOfBackedUpKeys(): Int - /** - * Start to back up keys immediately. - * - * @param progressListener the callback to follow the progress - * @param callback the main callback - */ - fun backupAllGroupSessions( - progressListener: ProgressListener?, - callback: MatrixCallback? - ) +// /** +// * Start to back up keys immediately. +// * +// * @param progressListener the callback to follow the progress +// * @param callback the main callback +// */ +// fun backupAllGroupSessions(progressListener: ProgressListener?, +// callback: MatrixCallback?) /** * Check trust on a key backup version. * * @param keysBackupVersion the backup version to check. - * @param callback block called when the operations completes. */ - fun getKeysBackupTrust( - keysBackupVersion: KeysVersionResult, - callback: MatrixCallback - ) + suspend fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust /** * Return the current progress of the backup. */ - fun getBackupProgress(progressListener: ProgressListener) + suspend fun getBackupProgress(progressListener: ProgressListener) /** * Get information about a backup version defined on the homeserver. * * It can be different than keysBackupVersion. * @param version the backup version - * @param callback */ - fun getVersion( - version: String, - callback: MatrixCallback - ) + suspend fun getVersion(version: String): KeysVersionResult? /** * This method fetches the last backup version on the server, then compare to the currently backup version use. * If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version. * - * @param callback true if backup is already using the last version, and false if it is not the case + * @return true if backup is already using the last version, and false if it is not the case */ - fun forceUsingLastVersion(callback: MatrixCallback) + suspend fun forceUsingLastVersion(): Boolean /** * Check the server for an active key backup. @@ -104,7 +91,7 @@ interface KeysBackupService { * If one is present and has a valid signature from one of the user's verified * devices, start backing up to it. */ - fun checkAndStartKeysBackup() + suspend fun checkAndStartKeysBackup() fun addListener(listener: KeysBackupStateListener) @@ -120,31 +107,23 @@ interface KeysBackupService { * @param password an optional passphrase string that can be entered by the user * when restoring the backup as an alternative to entering the recovery key. * @param progressListener a progress listener, as generating private key from password may take a while - * @param callback Asynchronous callback */ - fun prepareKeysBackupVersion( - password: String?, - progressListener: ProgressListener?, - callback: MatrixCallback - ) + suspend fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?): MegolmBackupCreationInfo /** * Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself. * If we are backing up to this version. Backup will be stopped. * * @param version the backup version to delete. - * @param callback Asynchronous callback */ - fun deleteBackup( - version: String, - callback: MatrixCallback? - ) + @Throws + suspend fun deleteBackup(version: String) /** * Ask if the backup on the server contains keys that we may do not have locally. * This should be called when entering in the state READY_TO_BACKUP */ - fun canRestoreKeys(): Boolean + suspend fun canRestoreKeys(): Boolean /** * Set trust on a keys backup version. @@ -152,40 +131,31 @@ interface KeysBackupService { * * @param keysBackupVersion the backup version to check. * @param trust the trust to set to the keys backup. - * @param callback block called when the operations completes. */ - fun trustKeysBackupVersion( - keysBackupVersion: KeysVersionResult, - trust: Boolean, - callback: MatrixCallback - ) + @Throws + suspend fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean) /** * Set trust on a keys backup version. * * @param keysBackupVersion the backup version to check. * @param recoveryKey the recovery key to challenge with the key backup public key. - * @param callback block called when the operations completes. */ - fun trustKeysBackupVersionWithRecoveryKey( - keysBackupVersion: KeysVersionResult, - recoveryKey: String, - callback: MatrixCallback - ) + suspend fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: IBackupRecoveryKey) /** * Set trust on a keys backup version. * * @param keysBackupVersion the backup version to check. * @param password the pass phrase to challenge with the keyBackupVersion public key. - * @param callback block called when the operations completes. */ - fun trustKeysBackupVersionWithPassphrase( + suspend fun trustKeysBackupVersionWithPassphrase( keysBackupVersion: KeysVersionResult, - password: String, - callback: MatrixCallback + password: String ) + suspend fun onSecretKeyGossip(secret: String) + /** * Restore a backup with a recovery key from a given backup version stored on the homeserver. * @@ -194,16 +164,14 @@ interface KeysBackupService { * @param roomId the id of the room to get backup data from. * @param sessionId the id of the session to restore. * @param stepProgressListener the step progress listener - * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. */ - fun restoreKeysWithRecoveryKey( + suspend fun restoreKeysWithRecoveryKey( keysVersionResult: KeysVersionResult, - recoveryKey: String, + recoveryKey: IBackupRecoveryKey, roomId: String?, sessionId: String?, - stepProgressListener: StepProgressListener?, - callback: MatrixCallback - ) + stepProgressListener: StepProgressListener? + ): ImportRoomKeysResult /** * Restore a backup with a password from a given backup version stored on the homeserver. @@ -213,16 +181,14 @@ interface KeysBackupService { * @param roomId the id of the room to get backup data from. * @param sessionId the id of the session to restore. * @param stepProgressListener the step progress listener - * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. */ - fun restoreKeyBackupWithPassword( + suspend fun restoreKeyBackupWithPassword( keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, - stepProgressListener: StepProgressListener?, - callback: MatrixCallback - ) + stepProgressListener: StepProgressListener? + ): ImportRoomKeysResult val keysBackupVersion: KeysVersionResult? @@ -234,10 +200,10 @@ interface KeysBackupService { fun getState(): KeysBackupState // For gossiping - fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) - fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? + fun saveBackupRecoveryKey(recoveryKey: IBackupRecoveryKey?, version: String?) + suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? - fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) + suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: IBackupRecoveryKey): Boolean fun computePrivateKey( passphrase: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt index 0d708b8d7..2d4f36f9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt @@ -31,7 +31,7 @@ data class MegolmBackupCreationInfo( val authData: MegolmBackupAuthData, /** - * The Base58 recovery key. + * The recovery key. */ - val recoveryKey: String + val recoveryKey: IBackupRecoveryKey ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt index 7f90fea9a..897b527fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt @@ -17,6 +17,6 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup data class SavedKeyBackupKeyInfo( - val recoveryKey: String, + val recoveryKey: IBackupRecoveryKey, val version: String ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoRoomInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoRoomInfo.kt new file mode 100644 index 000000000..51cd81115 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoRoomInfo.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.model + +data class CryptoRoomInfo( + val algorithm: String, + val shouldEncryptForInvitedMembers: Boolean, + val blacklistUnverifiedDevices: Boolean, + // Determines whether or not room history should be shared on new member invites + val shouldShareHistory: Boolean, + // This is specific to megolm but not sure how to model it better + // a security to ensure that a room will never revert to not encrypted + // even if a new state event with empty encryption, or state is reset somehow + val wasEncryptedOnce: Boolean, + // How long the session should be used before changing it. 604800000 (a week) is the recommended default. + val rotationPeriodMs: Long, + // How many messages should be sent before changing the session. 100 is the recommended default. + val rotationPeriodMsgs: Long, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt index b55f0e874..b273215e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt @@ -18,5 +18,7 @@ package org.matrix.android.sdk.api.session.crypto.model data class ImportRoomKeysResult( val totalNumberOfKeys: Int, - val successfullyNumberOfImportedKeys: Int + val successfullyNumberOfImportedKeys: Int, + /* It's a map from room id to a map of the sender key to a list of session. */ + val importedSessionInfo: Map>> ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt index 66d7558fe..3d90c18f2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt @@ -18,6 +18,15 @@ package org.matrix.android.sdk.api.session.crypto.model import org.matrix.android.sdk.api.util.JsonDict +enum class MessageVerificationState { + VERIFIED, + SIGNED_DEVICE_OF_UNVERIFIED_USER, + UN_SIGNED_DEVICE_OF_VERIFIED_USER, + UN_SIGNED_DEVICE, + UNKNOWN_DEVICE, + UNSAFE_SOURCE, +} + /** * The result of a (successful) call to decryptEvent. */ @@ -45,5 +54,5 @@ data class MXEventDecryptionResult( */ val forwardingCurve25519KeyChain: List = emptyList(), - val isSafe: Boolean = false + val messageVerificationState: MessageVerificationState? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt index 736ae6b31..a23204a55 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.session.crypto.model class MXUsersDevicesMap { // A map of maps (userId -> (deviceId -> Object)). - val map = HashMap>() + val map = HashMap>() /** * @return the user Ids @@ -104,6 +104,10 @@ class MXUsersDevicesMap { map.clear() } + fun join(other: Map>) { + map.putAll(other.map { it.key to it.value.toMutableMap() }) + } + /** * Add entries from another MXUsersDevicesMap. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt index 6d57318f8..2f94fff11 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.util.JsonDict /** * This class represents the decryption result. + * It's serialized in eventEntity to remember the decryption result */ @JsonClass(generateAdapter = true) data class OlmDecryptionResult( @@ -50,4 +51,9 @@ data class OlmDecryptionResult( * True if the key used to decrypt is considered safe (trusted). */ @Json(name = "key_safety") val isSafe: Boolean? = null, + + /** + * Authenticity info for that message. + */ + @Json(name = "verification_state") val verificationState: MessageVerificationState? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt similarity index 62% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt index 38ee5dc7e..86a0ebf97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/OutgoingSasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/EVerificationState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,19 @@ package org.matrix.android.sdk.api.session.crypto.verification -interface OutgoingSasVerificationTransaction : SasVerificationTransaction { - val uxState: UxState +enum class EVerificationState { + // outgoing started request + WaitingForReady, - enum class UxState { - UNKNOWN, - WAIT_FOR_START, - WAIT_FOR_KEY_AGREEMENT, - SHOW_SAS, - WAIT_FOR_VERIFICATION, - VERIFIED, - CANCELLED_BY_ME, - CANCELLED_BY_OTHER - } + // for incoming + Requested, + + // both incoming/outgoing + Ready, + Started, + WeStarted, + WaitingForDone, + Done, + Cancelled, + HandledByOtherSession } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt index 7db450e86..5d30c847c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/PendingVerificationRequest.kt @@ -15,66 +15,33 @@ */ package org.matrix.android.sdk.api.session.crypto.verification -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import java.util.UUID - /** * Stores current pending verification requests. */ data class PendingVerificationRequest( val ageLocalTs: Long, + val state: EVerificationState, val isIncoming: Boolean = false, - val localId: String = UUID.randomUUID().toString(), +// val localId: String = UUID.randomUUID().toString(), val otherUserId: String, + val otherDeviceId: String?, + // in case of verification via room, it will be not null val roomId: String?, - val transactionId: String? = null, - val requestInfo: ValidVerificationInfoRequest? = null, - val readyInfo: ValidVerificationInfoReady? = null, + val transactionId: String, // ? = null, +// val requestInfo: ValidVerificationInfoRequest? = null, +// val readyInfo: ValidVerificationInfoReady? = null, val cancelConclusion: CancelCode? = null, - val isSuccessful: Boolean = false, + val isFinished: Boolean = false, val handledByOtherSession: Boolean = false, // In case of to device it is sent to a list of devices - val targetDevices: List? = null -) { - val isReady: Boolean = readyInfo != null - val isSent: Boolean = transactionId != null - - val isFinished: Boolean = isSuccessful || cancelConclusion != null - - /** - * SAS is supported if I support it and the other party support it. - */ - fun isSasSupported(): Boolean { - return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() - } - - /** - * Other can show QR code if I can scan QR code and other can show QR code. - */ - fun otherCanShowQrCode(): Boolean { - return if (isIncoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - } - } + val targetDevices: List? = null, + // if available store here the qr code to show + val qrCodeText: String? = null, + val isSasSupported: Boolean = false, + val weShouldShowScanOption: Boolean = false, + val weShouldDisplayQRCode: Boolean = false, - /** - * Other can scan QR code if I can show QR code and other can scan QR code. - */ - fun otherCanScanQrCode(): Boolean { - return if (isIncoming) { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() - } else { - requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() && - readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() - } - } + ) { +// val isReady: Boolean = readyInfo != null +// } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt index 06bac4109..2f7167d10 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/QrCodeVerificationTransaction.kt @@ -16,8 +16,22 @@ package org.matrix.android.sdk.api.session.crypto.verification +enum class QRCodeVerificationState { + // ie. we started + Reciprocated, + + // When started/scanned by other side and waiting for confirmation + // that was really scanned + WaitingForScanConfirmation, + WaitingForOtherDone, + Done, + Cancelled +} + interface QrCodeVerificationTransaction : VerificationTransaction { + fun state(): QRCodeVerificationState + /** * To use to display a qr code, for the other user to scan it. */ @@ -26,15 +40,17 @@ interface QrCodeVerificationTransaction : VerificationTransaction { /** * Call when you have scan the other user QR code. */ - fun userHasScannedOtherQrCode(otherQrCodeText: String) +// suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) /** * Call when you confirm that other user has scanned your QR code. */ - fun otherUserScannedMyQrCode() + suspend fun otherUserScannedMyQrCode() /** * Call when you do not confirm that other user has scanned your QR code. */ - fun otherUserDidNotScannedMyQrCode() + suspend fun otherUserDidNotScannedMyQrCode() + + override fun isSuccessful() = state() == QRCodeVerificationState.Done } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasTransactionState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasTransactionState.kt new file mode 100644 index 000000000..f13b11451 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasTransactionState.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +sealed class SasTransactionState { + + object None : SasTransactionState() + + // I wend a start + object SasStarted : SasTransactionState() + + // I received a start and it was accepted + object SasAccepted : SasTransactionState() + + // I received an accept and sent my key + object SasKeySent : SasTransactionState() + + // Keys exchanged and code ready to be shared + object SasShortCodeReady : SasTransactionState() + + // I received the other Mac, but might have not yet confirmed the short code + // at that time (other side already confirmed) + data class SasMacReceived(val codeConfirmed: Boolean) : SasTransactionState() + + // I confirmed the code and sent my mac + object SasMacSent : SasTransactionState() + + // I am done, waiting for other Done + data class Done(val otherDone: Boolean) : SasTransactionState() + + data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : SasTransactionState() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt index 095b4208f..99c3642b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/SasVerificationTransaction.kt @@ -18,19 +18,45 @@ package org.matrix.android.sdk.api.session.crypto.verification interface SasVerificationTransaction : VerificationTransaction { - fun supportsEmoji(): Boolean + companion object { + const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" + const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" - fun supportsDecimal(): Boolean + // Deprecated maybe removed later, use V2 + const val KEY_AGREEMENT_V1 = "curve25519" + const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256" + + // ordered by preferred order + val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1) + + // ordered by preferred order + val KNOWN_HASHES = listOf("sha256") + + // ordered by preferred order + val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) + + // older devices have limited support of emoji but SDK offers images for the 64 verification emojis + // so always send that we support EMOJI + val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL) + } + + fun state(): SasTransactionState + + override fun isSuccessful() = state() is SasTransactionState.Done + +// fun supportsEmoji(): Boolean fun getEmojiCodeRepresentation(): List - fun getDecimalCodeRepresentation(): String + fun getDecimalCodeRepresentation(): String? /** * To be called by the client when the user has verified that * both short codes do match. */ - fun userHasVerifiedShortCode() + suspend fun userHasVerifiedShortCode() + + suspend fun acceptVerification() - fun shortCodeDoesNotMatch() + suspend fun shortCodeDoesNotMatch() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt new file mode 100644 index 000000000..0f4ac1bdd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.verification + +sealed class VerificationEvent(val transactionId: String, val otherUserId: String) { + data class RequestAdded(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId) + data class RequestUpdated(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId) + data class TransactionAdded(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId) + data class TransactionUpdated(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId) +} + +fun VerificationEvent.getRequest(): PendingVerificationRequest? { + return when (this) { + is VerificationEvent.RequestAdded -> this.request + is VerificationEvent.RequestUpdated -> this.request + is VerificationEvent.TransactionAdded -> null + is VerificationEvent.TransactionUpdated -> null + } +} + +fun VerificationEvent.getTransaction(): VerificationTransaction? { + return when (this) { + is VerificationEvent.RequestAdded -> null + is VerificationEvent.RequestUpdated -> null + is VerificationEvent.TransactionAdded -> this.transaction + is VerificationEvent.TransactionUpdated -> this.transaction + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index ee93f1499..4a0c44287 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.crypto.verification +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.LocalEcho @@ -29,86 +30,85 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho */ interface VerificationService { - fun addListener(listener: Listener) +// fun addListener(listener: Listener) +// +// fun removeListener(listener: Listener) - fun removeListener(listener: Listener) + fun requestEventFlow(): Flow /** * Mark this device as verified manually. */ - fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) - fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? + suspend fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? - fun getExistingVerificationRequests(otherUserId: String): List + suspend fun getExistingVerificationRequests(otherUserId: String): List - fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? + suspend fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? - fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? + suspend fun getExistingVerificationRequestInRoom(roomId: String, tid: String): PendingVerificationRequest? - fun beginKeyVerification( - method: VerificationMethod, - otherUserId: String, - otherDeviceId: String, - transactionId: String? - ): String? + /** + * Request an interactive verification to begin + * + * This sends out a m.key.verification.request event over to-device messaging to + * to this device. + * + * If no specific device should be verified, but we would like to request + * verification from all our devices, use [requestSelfKeyVerification] instead. + */ + suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest /** * Request key verification with another user via room events (instead of the to-device API). */ - fun requestKeyVerificationInDMs( + @Throws + suspend fun requestKeyVerificationInDMs( methods: List, otherUserId: String, roomId: String, localId: String? = LocalEcho.createLocalEchoId() ): PendingVerificationRequest - fun cancelVerificationRequest(request: PendingVerificationRequest) + /** + * Request a self key verification using to-device API (instead of room events). + */ + @Throws + suspend fun requestSelfKeyVerification(methods: List): PendingVerificationRequest /** - * Request a key verification from another user using toDevice events. + * You should call this method after receiving a verification request. + * Accept the verification request advertising the given methods as supported + * Returns false if the request is unknown or transaction is not ready. */ - fun requestKeyVerification( + suspend fun readyPendingVerification( methods: List, otherUserId: String, - otherDevices: List? - ): PendingVerificationRequest + transactionId: String + ): Boolean - fun declineVerificationRequestInDMs( - otherUserId: String, - transactionId: String, - roomId: String - ) + suspend fun cancelVerificationRequest(request: PendingVerificationRequest) - // Only SAS method is supported for the moment - // TODO Parameter otherDeviceId should be removed in this case - fun beginKeyVerificationInDMs( + suspend fun cancelVerificationRequest(otherUserId: String, transactionId: String) + + suspend fun startKeyVerification( method: VerificationMethod, - transactionId: String, - roomId: String, otherUserId: String, - otherDeviceId: String - ): String + requestId: String + ): String? - /** - * Returns false if the request is unknown. - */ - fun readyPendingVerificationInDMs( - methods: List, + suspend fun reciprocateQRVerification( otherUserId: String, - roomId: String, - transactionId: String - ): Boolean + requestId: String, + scannedData: String + ): String? - /** - * Returns false if the request is unknown. - */ - fun readyPendingVerification( - methods: List, - otherUserId: String, - transactionId: String - ): Boolean +// suspend fun sasCodeMatch(theyMatch: Boolean, transactionId: String) + + // This starts the short SAS flow, the one that doesn't start with a request, deprecated + // using flow now? interface Listener { /** * Called when a verification request is created either by the user, or by the other user. @@ -151,5 +151,6 @@ interface VerificationService { } } - fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) + suspend fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) + suspend fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt index b68a82c60..a439cb916 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTransaction.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ package org.matrix.android.sdk.api.session.crypto.verification interface VerificationTransaction { - var state: VerificationTxState + val method: VerificationMethod val transactionId: String val otherUserId: String - var otherDeviceId: String? + val otherDeviceId: String? // TODO Not used. Remove? val isIncoming: Boolean @@ -30,9 +30,19 @@ interface VerificationTransaction { /** * User wants to cancel the transaction. */ - fun cancel() + suspend fun cancel() - fun cancel(code: CancelCode) + suspend fun cancel(code: CancelCode) fun isToDeviceTransport(): Boolean + + fun isSuccessful(): Boolean +} + +internal fun VerificationTransaction.dbgState(): String? { + return when (this) { + is SasVerificationTransaction -> "${this.state()}" + is QrCodeVerificationTransaction -> "${this.state()}" + else -> "??" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt deleted file mode 100644 index 30e4c6693..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationTxState.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.session.crypto.verification - -sealed class VerificationTxState { - /** - * Uninitialized state. - */ - object None : VerificationTxState() - - /** - * Specific for SAS. - */ - abstract class VerificationSasTxState : VerificationTxState() - - object SendingStart : VerificationSasTxState() - object Started : VerificationSasTxState() - object OnStarted : VerificationSasTxState() - object SendingAccept : VerificationSasTxState() - object Accepted : VerificationSasTxState() - object OnAccepted : VerificationSasTxState() - object SendingKey : VerificationSasTxState() - object KeySent : VerificationSasTxState() - object OnKeyReceived : VerificationSasTxState() - object ShortCodeReady : VerificationSasTxState() - object ShortCodeAccepted : VerificationSasTxState() - object SendingMac : VerificationSasTxState() - object MacSent : VerificationSasTxState() - object Verifying : VerificationSasTxState() - - /** - * Specific for QR code. - */ - abstract class VerificationQrTxState : VerificationTxState() - - /** - * Will be used to ask the user if the other user has correctly scanned. - */ - object QrScannedByOther : VerificationQrTxState() - object WaitingOtherReciprocateConfirm : VerificationQrTxState() - - /** - * Terminal states. - */ - abstract class TerminalTxState : VerificationTxState() - - object Verified : TerminalTxState() - - /** - * Cancelled by me or by other. - */ - data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState() -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index ae3e3a63c..bad8b3766 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -108,6 +108,9 @@ data class Event( @Transient var threadDetails: ThreadDetails? = null + @Transient + var verificationStateIsDirty: Boolean? = null + fun sendStateError(): MatrixError? { return sendStateDetails?.let { val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) @@ -139,6 +142,7 @@ data class Event( unsignedData: UnsignedData? = this.unsignedData, redacts: String? = this.redacts, mxDecryptionResult: OlmDecryptionResult? = this.mxDecryptionResult, + verificationStateIsDirty: Boolean? = this.verificationStateIsDirty, mCryptoError: MXCryptoError.ErrorType? = this.mCryptoError, mCryptoErrorReason: String? = this.mCryptoErrorReason, sendState: SendState = this.sendState, @@ -155,7 +159,7 @@ data class Event( stateKey = stateKey, roomId = roomId, unsignedData = unsignedData, - redacts = redacts + redacts = redacts, ).also { it.mxDecryptionResult = mxDecryptionResult it.mCryptoError = mCryptoError @@ -163,6 +167,7 @@ data class Event( it.sendState = sendState it.ageLocalTs = ageLocalTs it.threadDetails = threadDetails + it.verificationStateIsDirty = verificationStateIsDirty } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 013b452ce..9228f76db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -96,6 +96,7 @@ object EventType { const val SEND_SECRET = "m.secret.send" // Interactive key verification + const val KEY_VERIFICATION_REQUEST = "m.key.verification.request" const val KEY_VERIFICATION_START = "m.key.verification.start" const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" const val KEY_VERIFICATION_KEY = "m.key.verification.key" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 4968df775..ecd03288f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -77,14 +77,24 @@ data class HomeServerCapabilities( val canRemotelyTogglePushNotificationsOfDevices: Boolean = false, /** - * True if the home server supports event redaction with relations. + * True if the home server supports redaction of related events. */ - var canRedactEventWithRelations: Boolean = false, + var canRedactRelatedEvents: Boolean = false, /** * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. */ val externalAccountManagementUrl: String? = null, + + /** + * Authentication issuer for use with MSC3824 delegated OIDC, provided in Wellknown. + */ + val authenticationIssuer: String? = null, + + /** + * If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager, provided in Wellknown. + */ + val disableNetworkConstraint: Boolean? = null, ) { enum class RoomCapabilitySupport { @@ -141,6 +151,8 @@ data class HomeServerCapabilities( return cap?.preferred ?: cap?.support?.lastOrNull() } + val delegatedOidcAuthEnabled: Boolean = authenticationIssuer != null + companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L const val ROOM_CAP_KNOCK = "knock" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index 1788bf7bd..0733ac0bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -97,4 +97,15 @@ interface PermalinkService { * @return the created template */ fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String + + /** + * Check if the url is a permalink. It must be a matrix.to link + * or a link with host provided by the string-array `permalink_supported_hosts` in the config file + * + * @param supportedHosts the list of hosts supported for permalinks + * @param url the link to check, Ex: "https://matrix.to/#/@benoit:matrix.org" + * + * @return true when url is a permalink + */ + fun isPermalinkSupported(supportedHosts: Array, url: String): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/Action.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/Action.kt index 6122aae97..bbf65288c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/Action.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/Action.kt @@ -20,7 +20,6 @@ import timber.log.Timber sealed class Action { object Notify : Action() - object DoNotNotify : Action() data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action() data class Highlight(val highlight: Boolean) : Action() @@ -72,7 +71,6 @@ fun List.toJson(): List { return map { action -> when (action) { is Action.Notify -> Action.ACTION_NOTIFY - is Action.DoNotNotify -> Action.ACTION_DONT_NOTIFY is Action.Sound -> { mapOf( Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, @@ -95,7 +93,7 @@ fun PushRule.getActions(): List { actions.forEach { actionStrOrObj -> when (actionStrOrObj) { Action.ACTION_NOTIFY -> Action.Notify - Action.ACTION_DONT_NOTIFY -> Action.DoNotNotify + Action.ACTION_DONT_NOTIFY -> return@forEach is Map<*, *> -> { when (actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]) { Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushRule.kt index a11ffc0a9..31dbd8dd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushRule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/rest/PushRule.kt @@ -121,8 +121,6 @@ data class PushRule( if (notify) { mutableActions.add(Action.ACTION_NOTIFY) - } else { - mutableActions.add(Action.ACTION_DONT_NOTIFY) } return copy(actions = mutableActions) @@ -140,5 +138,5 @@ data class PushRule( * * @return true if the rule should not play sound */ - fun shouldNotNotify() = actions.contains(Action.ACTION_DONT_NOTIFY) + fun shouldNotNotify() = actions.isEmpty() || actions.contains(Action.ACTION_DONT_NOTIFY) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 65383f100..736d3ce35 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -244,6 +244,15 @@ interface RoomService { sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY ): LiveData> + /** + * Only notifies when this query has changes. + * It doesn't load any items in memory + */ + fun roomSummariesChangesLive( + queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY + ): LiveData> + /** * Get's a live paged list from a filter that can be dynamically updated. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt index db87f913b..f0a5dfd2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt @@ -24,6 +24,7 @@ interface UpdatableLivePageResult { val livePagedList: LiveData> val liveBoundaries: LiveData var queryParams: RoomSummaryQueryParams + var sortOrder: RoomSortOrder } data class ResultBoundaries( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index f6b7675d4..18daa579e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.room.model.message +import org.matrix.android.sdk.api.session.events.model.EventType + object MessageType { const val MSGTYPE_TEXT = "m.text" const val MSGTYPE_EMOTE = "m.emote" @@ -26,7 +28,7 @@ object MessageType { const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" - const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" + const val MSGTYPE_VERIFICATION_REQUEST = EventType.KEY_VERIFICATION_REQUEST // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt deleted file mode 100644 index 33f61648d..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationAcceptContent.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.api.session.room.model.message - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory - -@JsonClass(generateAdapter = true) -internal data class MessageVerificationAcceptContent( - @Json(name = "hash") override val hash: String?, - @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, - @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, - @Json(name = "commitment") override var commitment: String? = null -) : VerificationInfoAccept { - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoAcceptFactory { - - override fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept { - return MessageVerificationAcceptContent( - hash, - keyAgreementProtocol, - messageAuthenticationCode, - shortAuthenticationStrings, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ), - commitment - ) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt index 687e5362d..80e6206ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -17,34 +17,14 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel @JsonClass(generateAdapter = true) data class MessageVerificationCancelContent( - @Json(name = "code") override val code: String? = null, - @Json(name = "reason") override val reason: String? = null, + @Json(name = "code") val code: String? = null, + @Json(name = "reason") val reason: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoCancel { +) { - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object { - fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { - return MessageVerificationCancelContent( - reason.value, - reason.humanReadable, - RelationDefaultContent( - RelationType.REFERENCE, - transactionId - ) - ) - } - } + val transactionId: String? = relatesTo?.eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt index a7f05009b..ab60df22d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -17,30 +17,12 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo @JsonClass(generateAdapter = true) internal data class MessageVerificationDoneContent( @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfo { +) { - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent(): Content? = toContent() - - override fun asValidObject(): ValidVerificationDone? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationDone( - validTransactionId - ) - } + val transactionId: String? = relatesTo?.eventId } - -internal data class ValidVerificationDone( - val transactionId: String -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt index a6b36ce6c..ebbfb1743 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationKeyContent.kt @@ -17,36 +17,16 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory @JsonClass(generateAdapter = true) internal data class MessageVerificationKeyContent( /** * The device’s ephemeral public key, as an unpadded base64 string. */ - @Json(name = "key") override val key: String? = null, + @Json(name = "key") val key: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoKey { +) { - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoKeyFactory { - - override fun create(tid: String, pubKey: String): VerificationInfoKey { - return MessageVerificationKeyContent( - pubKey, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } + val transactionId: String? = relatesTo?.eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt index 3bb333849..317a9eb41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationMacContent.kt @@ -17,34 +17,14 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory @JsonClass(generateAdapter = true) internal data class MessageVerificationMacContent( - @Json(name = "mac") override val mac: Map? = null, - @Json(name = "keys") override val keys: String? = null, + @Json(name = "mac") val mac: Map? = null, + @Json(name = "keys") val keys: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoMac { +) { - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : VerificationInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { - return MessageVerificationMacContent( - mac, - keys, - RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } + val transactionId: String? = relatesTo?.eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt index 72bf6e6ff..9791bc7af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -17,34 +17,14 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady @JsonClass(generateAdapter = true) internal data class MessageVerificationReadyContent( - @Json(name = "from_device") override val fromDevice: String? = null, - @Json(name = "methods") override val methods: List? = null, + @Json(name = "from_device") val fromDevice: String? = null, + @Json(name = "methods") val methods: List? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerificationInfoReady { +) { - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() - - companion object : MessageVerificationReadyFactory { - override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { - return MessageVerificationReadyContent( - fromDevice = fromDevice, - methods = methods, - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - tid - ) - ) - } - } + val transactionId: String? = relatesTo?.eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt index a0699831f..7752fac1a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -18,25 +18,20 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest @JsonClass(generateAdapter = true) data class MessageVerificationRequestContent( @Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, @Json(name = "body") override val body: String, - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List, + @Json(name = "from_device") val fromDevice: String?, + @Json(name = "methods") val methods: List, @Json(name = "to") val toUserId: String, - @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "timestamp") val timestamp: Long?, @Json(name = "format") val format: String? = null, @Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, // Not parsed, but set after, using the eventId - override val transactionId: String? = null -) : MessageContent, VerificationInfoRequest { - - override fun toEventContent() = toContent() -} + val transactionId: String? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt index 8f23a9e15..f32008087 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVerificationStartContent.kt @@ -17,29 +17,19 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart -import org.matrix.android.sdk.internal.util.JsonCanonicalizer @JsonClass(generateAdapter = true) internal data class MessageVerificationStartContent( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "hashes") override val hashes: List?, - @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, - @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List?, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, - @Json(name = "method") override val method: String?, + @Json(name = "from_device") val fromDevice: String?, + @Json(name = "hashes") val hashes: List?, + @Json(name = "key_agreement_protocols") val keyAgreementProtocols: List?, + @Json(name = "message_authentication_codes") val messageAuthenticationCodes: List?, + @Json(name = "short_authentication_string") val shortAuthenticationStrings: List?, + @Json(name = "method") val method: String?, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, - @Json(name = "secret") override val sharedSecret: String? -) : VerificationInfoStart { + @Json(name = "secret") val sharedSecret: String? +) { - override fun toCanonicalJson(): String { - return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) - } - - override val transactionId: String? - get() = relatesTo?.eventId - - override fun toEventContent() = toContent() + val transactionId: String? = relatesTo?.eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index e7fcabf38..5a5cb466b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -117,7 +117,7 @@ interface RelationService { fun editReply( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, - newBodyText: String, + newBodyText: CharSequence, newFormattedBodyText: String? = null, compatibilityBodyText: String = "* $newBodyText" ): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 07036f4b6..9eb0fa409 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -158,10 +158,10 @@ interface SendService { * Redact (delete) the given event. * @param event the event to redact * @param reason optional reason string - * @param withRelations the list of relation types to redact with this event + * @param withRelTypes the list of relation types to redact with this event * @param additionalContent additional content to put in the event content */ - fun redactEvent(event: Event, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Cancelable + fun redactEvent(event: Event, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Cancelable /** * Schedule this message to be resent. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index a49c20ccb..ef6d103e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -37,13 +37,13 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent -import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.util.ContentUtils +import org.matrix.android.sdk.api.util.ContentUtils.ensureCorrectFormattedBodyInTextReply import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply /** @@ -160,37 +160,17 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? { fun TimelineEvent.getLastEditNewContent(): Content? { val lastContent = annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent - return if (isReply()) { - val previousFormattedBody = root.getClearContent().toModel()?.formattedBody - if (previousFormattedBody?.isNotEmpty() == true) { - val lastMessageContent = lastContent.toModel() - lastMessageContent?.let { ensureCorrectFormattedBodyInTextReply(it, previousFormattedBody) }?.toContent() ?: lastContent - } else { - lastContent - } - } else { - lastContent - } -} - -private const val MX_REPLY_END_TAG = "" - -/** - * Not every client sends a formatted body in the last edited event since this is not required in the - * [Matrix specification](https://spec.matrix.org/v1.4/client-server-api/#applying-mnew_content). - * We must ensure there is one so that it is still considered as a reply when rendering the message. - */ -private fun ensureCorrectFormattedBodyInTextReply(messageTextContent: MessageTextContent, previousFormattedBody: String): MessageTextContent { return when { - messageTextContent.formattedBody.isNullOrEmpty() && previousFormattedBody.contains(MX_REPLY_END_TAG) -> { - // take previous formatted body with the new body content - val newFormattedBody = previousFormattedBody.replaceAfterLast(MX_REPLY_END_TAG, messageTextContent.body) - messageTextContent.copy( - formattedBody = newFormattedBody, - format = MessageFormat.FORMAT_MATRIX_HTML, - ) + isReply() -> { + val originalFormattedBody = root.getClearContent().toModel()?.formattedBody + val lastMessageContent = lastContent.toModel() + if (lastMessageContent != null && originalFormattedBody?.isNotEmpty() == true) { + ensureCorrectFormattedBodyInTextReply(lastMessageContent, originalFormattedBody).toContent() + } else { + lastContent + } } - else -> messageTextContent + else -> lastContent } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt index bdbbd3ea8..783665f9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -135,5 +135,11 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult + @Deprecated("Requesting custom secrets not yet support by rust stack, prefer requestMissingSecrets") suspend fun requestSecret(name: String, myOtherDeviceId: String) + + /** + * Request the missing local secrets to other sessions. + */ + suspend fun requestMissingSecrets() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt index d64b2e6e9..fade51600 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt @@ -37,6 +37,7 @@ interface SignOutService { /** * Sign out, and release the session, clear all the session data, including crypto data. * @param signOutFromHomeserver true if the sign out request has to be done + * @param ignoreServerRequestError true to ignore server error if any */ - suspend fun signOut(signOutFromHomeserver: Boolean) + suspend fun signOut(signOutFromHomeserver: Boolean, ignoreServerRequestError: Boolean = false) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt index 382d8a174..3948acef6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt @@ -64,5 +64,12 @@ data class SyncResponse( * but that algorithm is not listed in device_unused_fallback_key_types, the client will upload a new key. */ @Json(name = "org.matrix.msc2732.device_unused_fallback_key_types") - val deviceUnusedFallbackKeyTypes: List? = null, -) + val devDeviceUnusedFallbackKeyTypes: List? = null, + @Json(name = "device_unused_fallback_key_types") + val stableDeviceUnusedFallbackKeyTypes: List? = null, + + ) { + + @Transient + val deviceUnusedFallbackKeyTypes: List? = stableDeviceUnusedFallbackKeyTypes ?: devDeviceUnusedFallbackKeyTypes +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt index e0596c132..6ffc82fc9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.util import android.util.Base64 +import timber.log.Timber fun ByteArray.toBase64NoPadding(): String { return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) @@ -25,3 +26,15 @@ fun ByteArray.toBase64NoPadding(): String { fun String.fromBase64(): ByteArray { return Base64.decode(this, Base64.DEFAULT) } + +/** + * Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source + */ +internal fun String.fromBase64Safe(): ByteArray? { + return try { + Base64.decode(this, Base64.DEFAULT) + } catch (throwable: Throwable) { + Timber.e(throwable, "Unable to decode base64 string") + null + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt index e453cb2df..ea4049bab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt @@ -15,6 +15,8 @@ */ package org.matrix.android.sdk.api.util +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.internal.util.unescapeHtml object ContentUtils { @@ -38,15 +40,36 @@ object ContentUtils { } fun extractUsefulTextFromHtmlReply(repliedBody: String): String { - if (repliedBody.startsWith("")) { - val closingTagIndex = repliedBody.lastIndexOf("") + if (repliedBody.startsWith(MX_REPLY_START_TAG)) { + val closingTagIndex = repliedBody.lastIndexOf(MX_REPLY_END_TAG) if (closingTagIndex != -1) { - return repliedBody.substring(closingTagIndex + "".length).trim() + return repliedBody.substring(closingTagIndex + MX_REPLY_END_TAG.length).trim() } } return repliedBody } + /** + * Not every client sends a formatted body in the last edited event since this is not required in the + * [Matrix specification](https://spec.matrix.org/v1.4/client-server-api/#applying-mnew_content). + * We must ensure there is one so that it is still considered as a reply when rendering the message. + */ + fun ensureCorrectFormattedBodyInTextReply(messageTextContent: MessageTextContent, originalFormattedBody: String): MessageTextContent { + return when { + messageTextContent.formattedBody != null && + !messageTextContent.formattedBody.contains(MX_REPLY_END_TAG) && + originalFormattedBody.contains(MX_REPLY_END_TAG) -> { + // take previous formatted body with the new body content + val newFormattedBody = originalFormattedBody.replaceAfterLast(MX_REPLY_END_TAG, messageTextContent.body) + messageTextContent.copy( + formattedBody = newFormattedBody, + format = MessageFormat.FORMAT_MATRIX_HTML, + ) + } + else -> messageTextContent + } + } + @Suppress("RegExpRedundantEscape") fun formatSpoilerTextFromHtml(formattedBody: String): String { // var reason = "", @@ -57,4 +80,6 @@ object ContentUtils { } private const val SPOILER_CHAR = "█" + private const val MX_REPLY_START_TAG = "" + private const val MX_REPLY_END_TAG = "" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index 5ec0dedad..af8ab71a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -30,6 +30,7 @@ object MimeTypes { const val BadJpg = "image/jpg" const val Jpeg = "image/jpeg" const val Gif = "image/gif" + const val Webp = "image/webp" const val Ogg = "audio/ogg" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt index b1f65194f..c43bef869 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -23,7 +23,6 @@ import dagger.Provides import io.realm.RealmConfiguration import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService -import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.internal.auth.db.AuthRealmMigration import org.matrix.android.sdk.internal.auth.db.AuthRealmModule import org.matrix.android.sdk.internal.auth.db.RealmPendingSessionStore @@ -34,7 +33,6 @@ import org.matrix.android.sdk.internal.auth.login.DirectLoginTask import org.matrix.android.sdk.internal.auth.login.QrLoginTokenTask import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.AuthDatabase -import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter import org.matrix.android.sdk.internal.wellknown.WellknownModule import java.io.File @@ -70,9 +68,6 @@ 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/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index d1dd0238b..e852c6118 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -298,9 +298,12 @@ internal class DefaultAuthenticationService @Inject constructor( } // If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow - val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true } + val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibility == true } val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows + val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null + + @Suppress("DEPRECATION") return LoginFlowResult( supportedLoginTypes = flows.orEmpty().mapNotNull { it.type }, ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, @@ -309,7 +312,7 @@ internal class DefaultAuthenticationService @Inject constructor( isOutdatedHomeserver = !versions.isSupportedBySdk(), hasOidcCompatibilityFlow = oidcCompatibilityFlow != null, isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), - isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), + isLoginWithQrSupported = supportsGetLoginTokenFlow || versions.doesServerSupportQrCodeLogin(), ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 971407388..2e52740ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -51,5 +51,13 @@ internal data class LoginFlow( * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) */ @Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") - val delegatedOidcCompatibilty: Boolean? = null + val delegatedOidcCompatibility: Boolean? = null, + + /** + * Whether a login flow of type m.login.token could accept a token issued using /login/get_token. + * + * See https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token + */ + @Json(name = "get_login_token") + val getLoginToken: Boolean? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 0a8c58de1..86341729c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.internal.auth.login -import android.util.Patterns import org.matrix.android.sdk.api.auth.LoginType import org.matrix.android.sdk.api.auth.login.LoginProfileInfo import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.extensions.isEmail import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.AuthAPI @@ -59,7 +59,7 @@ internal class DefaultLoginWizard( initialDeviceName: String, deviceId: String? ): Session { - val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) { + val loginParams = if (login.isEmail()) { PasswordLoginParams.thirdPartyIdentifier( medium = ThreePidMedium.EMAIL, address = login, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 4d8e90cf3..83186344b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -54,12 +54,12 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" +@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow") private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS = "org.matrix.msc3912" -private const val FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE = "org.matrix.msc3912.stable" +private const val FEATURE_REDACTION_OF_RELATED_EVENT = "org.matrix.msc3912" /** * Return true if the SDK supports this homeserver version. @@ -94,7 +94,9 @@ internal fun Versions.doesServerSupportThreadUnreadNotifications(): Boolean { return getMaxVersion() >= HomeServerVersion.v1_4_0 || (msc3771 && msc3773) } +@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow") internal fun Versions.doesServerSupportQrCodeLogin(): Boolean { + @Suppress("DEPRECATION") return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false } @@ -159,9 +161,8 @@ internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolea /** * Indicate if the server supports MSC3912: https://github.com/matrix-org/matrix-spec-proposals/pull/3912. * - * @return true if event redaction with relations is supported + * @return true if redaction of related events is supported */ -internal fun Versions.doesServerSupportRedactEventWithRelations(): Boolean { - return unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS).orFalse() || - unstableFeatures?.get(FEATURE_EVENT_REDACTION_WITH_RELATIONS_STABLE).orFalse() +internal fun Versions.doesServerSupportRedactionOfRelatedEvents(): Boolean { + return unstableFeatures?.get(FEATURE_REDACTION_OF_RELATED_EVENT).orFalse() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt new file mode 100644 index 000000000..90b6f1bed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/coroutines/builder/FlowBuilders.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.coroutines.builder + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.ProducerScope + +/** + * Use this with a flow builder like [kotlinx.coroutines.flow.channelFlow] to replace [kotlinx.coroutines.channels.awaitClose]. + * As awaitClose is at the end of the builder block, it can lead to the block being cancelled before it reaches the awaitClose. + * Example of usage: + * + * return channelFlow { + * val onClose = safeInvokeOnClose { + * // Do stuff on close + * } + * val data = getData() + * send(data) + * onClose.await() + * } + * + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal fun ProducerScope.safeInvokeOnClose(handler: (cause: Throwable?) -> Unit): CompletableDeferred { + val onClose = CompletableDeferred() + invokeOnClose { + handler(it) + onClose.complete(Unit) + } + return onClose +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt new file mode 100644 index 000000000..75575b14c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ComputeShieldForGroupUseCase.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.internal.di.UserId +import javax.inject.Inject + +internal class ComputeShieldForGroupUseCase @Inject constructor( + @UserId private val myUserId: String +) { + + suspend operator fun invoke(olmMachine: OlmMachine, userIds: List): RoomEncryptionTrustLevel { + val myIdentity = olmMachine.getIdentity(myUserId) + val allTrustedUserIds = userIds + .filter { userId -> + olmMachine.getIdentity(userId)?.verified() == true + } + + return if (allTrustedUserIds.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // If all devices of all verified users are trusted -> green + // else -> black + allTrustedUserIds + .map { userId -> + olmMachine.getUserDevices(userId) + } + .flatten() + .let { allDevices -> + if (myIdentity != null) { + allDevices.any { !it.toCryptoDeviceInfo().trustLevel?.crossSigningVerified.orFalse() } + } else { + // TODO check that if myIdentity is null ean + // Legacy method + allDevices.any { !it.toCryptoDeviceInfo().isVerified } + } + } + .let { hasWarning -> + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (userIds.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index c69a85901..cdc5973fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -22,12 +22,13 @@ import dagger.Provides import io.realm.RealmConfiguration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.crosssigning.ComputeTrustTask -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultComputeTrustTask -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask @@ -57,8 +58,8 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionD import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore +import org.matrix.android.sdk.internal.crypto.store.RustCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask @@ -68,7 +69,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask @@ -81,7 +81,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask @@ -89,6 +88,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask +import org.matrix.android.sdk.internal.crypto.verification.RustVerificationService import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.SessionFilesDirectory @@ -132,8 +132,8 @@ internal abstract class CryptoModule { @JvmStatic @Provides @SessionScope - fun providesCryptoCoroutineScope(): CoroutineScope { - return CoroutineScope(SupervisorJob()) + fun providesCryptoCoroutineScope(coroutineDispatchers: MatrixCoroutineDispatchers): CoroutineScope { + return CoroutineScope(SupervisorJob() + coroutineDispatchers.crypto) } @JvmStatic @@ -159,7 +159,7 @@ internal abstract class CryptoModule { } @Binds - abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService + abstract fun bindCryptoService(service: RustCryptoService): CryptoService @Binds abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask @@ -240,17 +240,17 @@ internal abstract class CryptoModule { abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask @Binds - abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService + abstract fun bindCrossSigningService(service: RustCrossSigningService): CrossSigningService @Binds - abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore + abstract fun bindVerificationService(service: RustVerificationService): VerificationService @Binds - abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask + abstract fun bindCryptoStore(store: RustCryptoStore): IMXCommonCryptoStore @Binds - abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask + abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask @Binds - abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask + abstract fun bindKeysBackupService(service: RustKeyBackupService): KeysBackupService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt index eee1ee70a..086d741ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -17,13 +17,22 @@ package org.matrix.android.sdk.internal.crypto import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.util.fetchCopied +import timber.log.Timber import javax.inject.Inject /** @@ -31,7 +40,8 @@ import javax.inject.Inject * in the session DB, this class encapsulate this functionality. */ internal class CryptoSessionInfoProvider @Inject constructor( - @SessionDatabase private val monarchy: Monarchy + @SessionDatabase private val monarchy: Monarchy, + @UserId private val myUserId: String ) { fun isRoomEncrypted(roomId: String): Boolean { @@ -60,4 +70,74 @@ internal class CryptoSessionInfoProvider @Inject constructor( } return userIds } + + fun getUserListForShieldComputation(roomId: String): List { + var userIds: List = emptyList() + monarchy.doWithRealm { realm -> + userIds = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + } + var isDirect = false + monarchy.doWithRealm { realm -> + isDirect = RoomSummaryEntity.where(realm, roomId = roomId).findFirst()?.isDirect == true + } + + return if (isDirect || userIds.size <= 2) { + userIds.filter { it != myUserId } + } else { + userIds + } + } + + fun getRoomsWhereUsersAreParticipating(userList: List): List { + if (userList.contains(myUserId)) { + // just take all + val roomIds: List? = null + monarchy.doWithRealm { sessionRealm -> + RoomSummaryEntity.where(sessionRealm) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findAll() + .map { it.roomId } + } + return roomIds.orEmpty() + } + var roomIds: List? = null + monarchy.doWithRealm { sessionRealm -> + roomIds = RoomSummaryEntity.where(sessionRealm) + .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships()) + .findAll() + .filter { it.otherMemberIds.any { it in userList } } + .map { it.roomId } +// roomIds = sessionRealm.where(RoomMemberSummaryEntity::class.java) +// .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) +// .distinct(RoomMemberSummaryEntityFields.ROOM_ID) +// .findAll() +// .map { it.roomId } +// .also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") } + } + return roomIds.orEmpty() + } + + fun markMessageVerificationStateAsDirty(userList: List) { + monarchy.writeAsync { sessionRealm -> + sessionRealm.where(EventEntity::class.java) + .`in`(EventEntityFields.SENDER, userList.toTypedArray()) + .equalTo(EventEntityFields.TYPE, EventType.ENCRYPTED) + .isNotNull(EventEntityFields.DECRYPTION_RESULT_JSON) +// // A bit annoying to have to do that like that and it could break :/ +// .contains(EventEntityFields.DECRYPTION_RESULT_JSON, "\"verification_state\":\"UNKNOWN_DEVICE\"") + .findAll() + .onEach { + it.isVerificationStateDirty = true + } + .map { EventMapper.map(it) } + .also { Timber.v("## VerificationState refresh - ... impacted events ${it.joinToString{ it.eventId.orEmpty() }}") } + } + } + + fun updateShieldForRoom(roomId: String, shield: RoomEncryptionTrustLevel?) { + monarchy.writeAsync { realm -> + val summary = RoomSummaryEntity.where(realm, roomId = roomId).findFirst() + summary?.roomEncryptionTrustLevel = shield + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt new file mode 100644 index 000000000..12255d078 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DecryptRoomEventUseCase.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.Event +import javax.inject.Inject + +internal class DecryptRoomEventUseCase @Inject constructor(private val olmMachine: OlmMachine) { + + suspend operator fun invoke(event: Event): MXEventDecryptionResult { + return olmMachine.decryptRoomEvent(event) + } + + suspend fun decryptAndSaveResult(event: Event) { + tryOrNull(message = "Unable to decrypt the event") { + invoke(event) + } + ?.let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + verificationState = result.messageVerificationState + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt deleted file mode 100755 index 50497e3a2..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ /dev/null @@ -1,1428 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import com.squareup.moshi.Types -import dagger.Lazy -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.NoOpMatrixCallback -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent -import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.api.session.room.model.shouldShareHistory -import org.matrix.android.sdk.api.session.sync.model.SyncResponse -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE -import org.matrix.android.sdk.internal.crypto.model.SessionInfo -import org.matrix.android.sdk.internal.crypto.model.toRest -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator -import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask -import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.TaskThread -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.task.launchToCallback -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmManager -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject -import kotlin.math.max - -/** - * A `CryptoService` class instance manages the end-to-end crypto for a session. - * - * - * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted - * before sending. - * In the other hand, received events goes through CryptoService for decrypting. - * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. - * Specially, it tracks all room membership changes events in order to do keys updates. - */ - -private val loggerTag = LoggerTag("DefaultCryptoService", LoggerTag.CRYPTO) - -@SessionScope -internal class DefaultCryptoService @Inject constructor( - // Olm Manager - private val olmManager: OlmManager, - @UserId - private val userId: String, - @DeviceId - private val deviceId: String?, - private val clock: Clock, - private val myDeviceInfoHolder: Lazy, - // the crypto store - private val cryptoStore: IMXCryptoStore, - // Room encryptors store - private val roomEncryptorsStore: RoomEncryptorsStore, - // Olm device - private val olmDevice: MXOlmDevice, - // Set of parameters used to configure/customize the end-to-end crypto. - private val mxCryptoConfig: MXCryptoConfig, - // Device list manager - private val deviceListManager: DeviceListManager, - // The key backup service. - private val keysBackupService: DefaultKeysBackupService, - // - private val objectSigner: ObjectSigner, - // - private val oneTimeKeysUploader: OneTimeKeysUploader, - // - private val roomDecryptorProvider: RoomDecryptorProvider, - // The verification service. - private val verificationService: DefaultVerificationService, - - private val crossSigningService: DefaultCrossSigningService, - // - private val incomingKeyRequestManager: IncomingKeyRequestManager, - private val secretShareManager: SecretShareManager, - // - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - // Actions - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val megolmSessionDataImporter: MegolmSessionDataImporter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - // Repository - private val megolmEncryptionFactory: MXMegolmEncryptionFactory, - private val olmEncryptionFactory: MXOlmEncryptionFactory, - // Tasks - private val deleteDeviceTask: DeleteDeviceTask, - private val getDevicesTask: GetDevicesTask, - private val getDeviceInfoTask: GetDeviceInfoTask, - private val setDeviceNameTask: SetDeviceNameTask, - private val uploadKeysTask: UploadKeysTask, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor, - private val cryptoCoroutineScope: CoroutineScope, - private val eventDecryptor: EventDecryptor, - private val verificationMessageProcessor: VerificationMessageProcessor, - private val liveEventManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, -) : CryptoService { - - private val isStarting = AtomicBoolean(false) - private val isStarted = AtomicBoolean(false) - - fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { - when (event.type) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) - } - } - - fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) { - // handle state events - if (event.isStateEvent()) { - when (event.type) { - EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) - EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) - } - } - - // handle verification - if (!isInitialSync) { - if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) { - cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { - verificationMessageProcessor.process(event) - } - } - } - } - -// val gossipingBuffer = mutableListOf() - - override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { - setDeviceNameTask - .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { - this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - // bg refresh of crypto device - downloadKeys(listOf(userId), true, NoOpMatrixCallback()) - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) - } - - override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { - deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) - } - - override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { - deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun getCryptoVersion(context: Context, longFormat: Boolean): String { - return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version - } - - override fun getMyDevice(): CryptoDeviceInfo { - return myDeviceInfoHolder.get().myDevice - } - - override fun fetchDevicesList(callback: MatrixCallback) { - getDevicesTask - .configureWith { - // this.executionThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - - override fun onSuccess(data: DevicesListResponse) { - // Save in local DB - cryptoStore.saveMyDevicesInfo(data.devices.orEmpty()) - callback.onSuccess(data) - } - } - } - .executeBy(taskExecutor) - } - - override fun getMyDevicesInfoLive(): LiveData> { - return cryptoStore.getLiveMyDevicesInfo() - } - - override fun getMyDevicesInfoLive(deviceId: String): LiveData> { - return cryptoStore.getLiveMyDevicesInfo(deviceId) - } - - override fun getMyDevicesInfo(): List { - return cryptoStore.getMyDevicesInfo() - } - - override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { - return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) - } - - /** - * Provides the tracking status. - * - * @param userId the user id - * @return the tracking status - */ - override fun getDeviceTrackingStatus(userId: String): Int { - return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED) - } - - /** - * Tell if the MXCrypto is started. - * - * @return true if the crypto is started - */ - fun isStarted(): Boolean { - return isStarted.get() - } - - /** - * Tells if the MXCrypto is starting. - * - * @return true if the crypto is starting - */ - fun isStarting(): Boolean { - return isStarting.get() - } - - /** - * Start the crypto module. - * Device keys will be uploaded, then one time keys if there are not enough on the homeserver - * and, then, if this is the first time, this new device will be announced to all other users - * devices. - * - */ - fun start() { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - internalStart() - } - // Just update - fetchDevicesList(NoOpMatrixCallback()) - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.tidyUpDataBase() - } - } - - fun ensureDevice() { - cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) { - // Open the store - cryptoStore.open() - - if (!cryptoStore.areDeviceKeysUploaded()) { - // Schedule upload of OTK - oneTimeKeysUploader.updateOneTimeKeyCount(0) - } - - // this can throw if no network - tryOrNull { - uploadDeviceKeys() - } - - oneTimeKeysUploader.maybeUploadOneTimeKeys() - // this can throw if no backup - tryOrNull { - keysBackupService.checkAndStartKeysBackup() - } - } - } - - fun onSyncWillProcess(isInitialSync: Boolean) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - if (isInitialSync) { - try { - // On initial sync, we start all our tracking from - // scratch, so mark everything as untracked. onCryptoEvent will - // be called for all e2e rooms during the processing of the sync, - // at which point we'll start tracking all the users of that room. - deviceListManager.invalidateAllDeviceLists() - // always track my devices? - deviceListManager.startTrackingDeviceList(listOf(userId)) - deviceListManager.refreshOutdatedDeviceLists() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "onSyncWillProcess ") - } - } - } - } - - private fun internalStart() { - if (isStarted.get() || isStarting.get()) { - return - } - isStarting.set(true) - - // Open the store - cryptoStore.open() - - isStarting.set(false) - isStarted.set(true) - } - - /** - * Close the crypto. - */ - fun close() = runBlocking(coroutineDispatchers.crypto) { - cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - incomingKeyRequestManager.close() - outgoingKeyRequestManager.close() - unrequestedForwardManager.close() - olmDevice.release() - cryptoStore.close() - } - - // Always enabled on Matrix Android SDK2 - override fun isCryptoEnabled() = true - - /** - * @return the Keys backup Service - */ - override fun keysBackupService() = keysBackupService - - /** - * @return the VerificationService - */ - override fun verificationService() = verificationService - - override fun crossSigningService() = crossSigningService - - /** - * A sync response has been received. - * - * @param syncResponse the syncResponse - * @param cryptoStoreAggregator data aggregated during the sync response treatment to store - */ - fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { - cryptoStore.storeData(cryptoStoreAggregator) - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - if (syncResponse.deviceLists != null) { - deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) - } - if (syncResponse.deviceOneTimeKeysCount != null) { - val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 - oneTimeKeysUploader.updateOneTimeKeyCount(currentCount) - } - - // unwedge if needed - try { - eventDecryptor.unwedgeDevicesIfNeeded() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed") - } - - // There is a limit of to_device events returned per sync. - // If we are in a case of such limited to_device sync we can't try to generate/upload - // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate - // the old otk too early. In this case we want to wait for the pending to_device before doing anything - // As per spec: - // If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response. - // 100 messages is recommended as a reasonable limit. - // The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure - // that there are no pending to_device - val toDevices = syncResponse.toDevice?.events.orEmpty() - if (isStarted() && toDevices.isEmpty()) { - // Make sure we process to-device messages before generating new one-time-keys #2782 - deviceListManager.refreshOutdatedDeviceLists() - // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. - // If there's no unused signed_curve25519 fallback key we need a new one. - if (syncResponse.deviceUnusedFallbackKeyTypes != null && - // Generate a fallback key only if the server does not already have an unused fallback key. - !syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE)) { - oneTimeKeysUploader.needsNewFallback() - } - - oneTimeKeysUploader.maybeUploadOneTimeKeys() - } - - // Process pending key requests - try { - if (toDevices.isEmpty()) { - // this is not blocking - outgoingKeyRequestManager.requireProcessAllPendingKeyRequests() - } else { - Timber.tag(loggerTag.value) - .w("Don't process key requests yet as there might be more to_device to catchup") - } - } catch (failure: Throwable) { - // just for safety but should not throw - Timber.tag(loggerTag.value).w("failed to process pending request") - } - - try { - incomingKeyRequestManager.processIncomingRequests() - } catch (failure: Throwable) { - // just for safety but should not throw - Timber.tag(loggerTag.value).w("failed to process incoming room key requests") - } - - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events -> - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - events.forEach { - onRoomKeyEvent(it, true) - } - } - } - } - } - } - - /** - * Find a device by curve25519 identity key. - * - * @param senderKey the curve25519 key to match. - * @param algorithm the encryption algorithm. - * @return the device info, or null if not found / unsupported algorithm / crypto released - */ - override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? { - return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { - // We only deal in olm keys - null - } else cryptoStore.deviceWithIdentityKey(senderKey) - } - - /** - * Provides the device information for a user id and a device Id. - * - * @param userId the user id - * @param deviceId the device id - */ - override fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { - return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { - cryptoStore.getUserDevice(userId, deviceId) - } else { - null - } - } - - override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback) { - getDeviceInfoTask - .configureWith(GetDeviceInfoTask.Params(deviceId)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun getCryptoDeviceInfo(userId: String): List { - return cryptoStore.getUserDeviceList(userId).orEmpty() - } - - override fun getLiveCryptoDeviceInfo(): LiveData> { - return cryptoStore.getLiveDeviceList() - } - - override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData> { - return cryptoStore.getLiveDeviceWithId(deviceId) - } - - override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { - return cryptoStore.getLiveDeviceList(userId) - } - - override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { - return cryptoStore.getLiveDeviceList(userIds) - } - - /** - * Set the devices as known. - * - * @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method. - * @param callback the asynchronous callback - */ - override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { - // build a devices map - val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId }) - - for ((userId, deviceIds) in devicesIdListByUserId) { - val storedDeviceIDs = cryptoStore.getUserDevices(userId) - - // sanity checks - if (null != storedDeviceIDs) { - var isUpdated = false - - deviceIds.forEach { deviceId -> - val device = storedDeviceIDs[deviceId] - - // assume if the device is either verified or blocked - // it means that the device is known - if (device?.isUnknown == true) { - device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.storeUserDevices(userId, storedDeviceIDs) - } - } - } - - callback?.onSuccess(Unit) - } - - /** - * Update the blocked/verified state of the given device. - * - * @param trustLevel the new trust level - * @param userId the owner of the device - * @param deviceId the unique identifier for the device. - */ - override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { - setDeviceVerificationAction.handle(trustLevel, userId, deviceId) - } - - /** - * Configure a room to use encryption. - * - * @param roomId the room id to enable encryption in. - * @param algorithm the encryption config for the room. - * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) - * @param membersId list of members to start tracking their devices - * @return true if the operation succeeds. - */ - private suspend fun setEncryptionInRoom( - roomId: String, - algorithm: String?, - inhibitDeviceQuery: Boolean, - membersId: List - ): Boolean { - // If we already have encryption in this room, we should ignore this event - // (for now at least. Maybe we should alert the user somehow?) - val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - - if (existingAlgorithm == algorithm) { - // ignore - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") - return false - } - - val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) - - // Always store even if not supported - cryptoStore.storeRoomAlgorithm(roomId, algorithm) - - if (!encryptingClass) { - Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") - return false - } - - val alg: IMXEncrypting? = when (algorithm) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) - else -> null - } - - if (alg != null) { - roomEncryptorsStore.put(roomId, alg) - } - - // if encryption was not previously enabled in this room, we will have been - // ignoring new device events for these users so far. We may well have - // up-to-date lists for some users, for instance if we were sharing other - // e2e rooms with them, so there is room for optimisation here, but for now - // we just invalidate everyone in the room. - if (null == existingAlgorithm) { - Timber.tag(loggerTag.value).d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") - - val userIds = ArrayList(membersId) - - deviceListManager.startTrackingDeviceList(userIds) - - if (!inhibitDeviceQuery) { - deviceListManager.refreshOutdatedDeviceLists() - } - } - - return true - } - - /** - * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM. - * - * @param roomId the room id - * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM - */ - override fun isRoomEncrypted(roomId: String): Boolean { - return cryptoSessionInfoProvider.isRoomEncrypted(roomId) - } - - /** - * @return the stored device keys for a user. - */ - override fun getUserDevices(userId: String): MutableList { - return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList() - } - - private fun isEncryptionEnabledForInvitedUser(): Boolean { - return mxCryptoConfig.enableEncryptionForInvitedMembers - } - - override fun getEncryptionAlgorithm(roomId: String): String? { - return cryptoStore.getRoomAlgorithm(roomId) - } - - /** - * Determine whether we should encrypt messages for invited users in this room. - *

- * Check here whether the invited members are allowed to read messages in the room history - * from the point they were invited onwards. - * - * @return true if we should encrypt messages for invited users. - */ - override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { - return cryptoStore.shouldEncryptForInvitedMembers(roomId) - } - - /** - * Encrypt an event content according to the configuration of the room. - * - * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param roomId the room identifier the event will be sent. - * @param callback the asynchronous callback - */ - override fun encryptEventContent( - eventContent: Content, - eventType: String, - roomId: String, - callback: MatrixCallback - ) { - // moved to crypto scope to have uptodate values - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val userIds = getRoomUserIds(roomId) - var alg = roomEncryptorsStore.get(roomId) - if (alg == null) { - val algorithm = getEncryptionAlgorithm(roomId) - if (algorithm != null) { - if (setEncryptionInRoom(roomId, algorithm, false, userIds)) { - alg = roomEncryptorsStore.get(roomId) - } - } - } - val safeAlgorithm = alg - if (safeAlgorithm != null) { - val t0 = clock.epochMillis() - Timber.tag(loggerTag.value).v("encryptEventContent() starts") - runCatching { - val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") - MXEncryptEventContentResult(content, EventType.ENCRYPTED) - }.foldToCallback(callback) - } else { - val algorithm = getEncryptionAlgorithm(roomId) - val reason = String.format( - MXCryptoError.UNABLE_TO_ENCRYPT_REASON, - algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON - ) - Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason") - callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) - } - } - } - - override fun discardOutboundSession(roomId: String) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val roomEncryptor = roomEncryptorsStore.get(roomId) - if (roomEncryptor is IMXGroupEncryption) { - roomEncryptor.discardSessionKey() - } else { - Timber.tag(loggerTag.value).e("discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption") - } - } - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or throw in case of error - */ - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return internalDecryptEvent(event, timeline) - } - - /** - * Decrypt an event asynchronously. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param callback the callback to return data or null - */ - override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - eventDecryptor.decryptEventAsync(event, timeline, callback) - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or null in case of error - */ - @Throws(MXCryptoError::class) - private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return eventDecryptor.decryptEvent(event, timeline) - } - - /** - * Reset replay attack data for the given timeline. - * - * @param timelineId the timeline id - */ - fun resetReplayAttackCheckInTimeline(timelineId: String) { - olmDevice.resetReplayAttackCheckInTimeline(timelineId) - } - - /** - * Handle the 'toDevice' event. - * - * @param event the event - */ - fun onToDeviceEvent(event: Event) { - // event have already been decrypted - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - when (event.getClearType()) { - EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - // Keys are imported directly, not waiting for end of sync - onRoomKeyEvent(event) - } - EventType.REQUEST_SECRET -> { - secretShareManager.handleSecretRequest(event) - } - EventType.ROOM_KEY_REQUEST -> { - event.getClearContent().toModel()?.let { req -> - // We'll always get these because we send room key requests to - // '*' (ie. 'all devices') which includes the sending device, - // so ignore requests from ourself because apart from it being - // very silly, it won't work because an Olm session cannot send - // messages to itself. - if (req.requestingDeviceId != deviceId) { // ignore self requests - event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) } - } - } - } - EventType.SEND_SECRET -> { - onSecretSendReceived(event) - } - in EventType.ROOM_KEY_WITHHELD.values -> { - onKeyWithHeldReceived(event) - } - else -> { - // ignore - } - } - } - liveEventManager.get().dispatchOnLiveToDevice(event) - } - - /** - * Handle a key event. - * - * @param event the key event. - * @param acceptUnrequested, if true it will force to accept unrequested keys. - */ - private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) { - val roomKeyContent = event.getDecryptedContent().toModel() ?: return - Timber.tag(loggerTag.value) - .i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>") - if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") - return - } - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) - if (alg == null) { - Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") - return - } - alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested) - } - - private fun onKeyWithHeldReceived(event: Event) { - val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") - } - val senderId = event.senderId ?: return Unit.also { - Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") - } - withHeldContent.sessionId ?: return - withHeldContent.algorithm ?: return - withHeldContent.roomId ?: return - withHeldContent.senderKey ?: return - outgoingKeyRequestManager.onRoomKeyWithHeld( - sessionId = withHeldContent.sessionId, - algorithm = withHeldContent.algorithm, - roomId = withHeldContent.roomId, - senderKey = withHeldContent.senderKey, - fromDevice = withHeldContent.fromDevice, - event = Event( - type = EventType.ROOM_KEY_WITHHELD.stable, - senderId = senderId, - content = event.getClearContent() - ) - ) - } - - private suspend fun onSecretSendReceived(event: Event) { - secretShareManager.onSecretSendReceived(event) { secretName, secretValue -> - handleSDKLevelGossip(secretName, secretValue) - } - } - - /** - * Returns true if handled by SDK, otherwise should be sent to application layer. - */ - private fun handleSDKLevelGossip( - secretName: String?, - secretValue: String - ): Boolean { - return when (secretName) { - MASTER_KEY_SSSS_NAME -> { - crossSigningService.onSecretMSKGossip(secretValue) - true - } - SELF_SIGNING_KEY_SSSS_NAME -> { - crossSigningService.onSecretSSKGossip(secretValue) - true - } - USER_SIGNING_KEY_SSSS_NAME -> { - crossSigningService.onSecretUSKGossip(secretValue) - true - } - KEYBACKUP_SECRET_SSSS_NAME -> { - keysBackupService.onSecretKeyGossip(secretValue) - true - } - else -> false - } - } - - /** - * Handle an m.room.encryption event. - * - * @param roomId the room Id - * @param event the encryption event. - */ - private fun onRoomEncryptionEvent(roomId: String, event: Event) { - if (!event.isStateEvent()) { - // Ignore - Timber.tag(loggerTag.value).w("Invalid encryption event") - return - } - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val userIds = getRoomUserIds(roomId) - setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) - } - } - - private fun getRoomUserIds(roomId: String): List { - val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() && - shouldEncryptForInvitedMembers(roomId) - return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers) - } - - /** - * Handle a change in the membership state of a member of a room. - * - * @param roomId the room Id - * @param event the membership event causing the change - */ - private fun onRoomMembershipEvent(roomId: String, event: Event) { - // because the encryption event can be after the join/invite in the same batch - event.stateKey?.let { _ -> - val roomMember: RoomMemberContent? = event.content.toModel() - val membership = roomMember?.membership - if (membership == Membership.INVITE) { - unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis()) - } - } - - roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return - - event.stateKey?.let { userId -> - val roomMember: RoomMemberContent? = event.content.toModel() - val membership = roomMember?.membership - if (membership == Membership.JOIN) { - // make sure we are tracking the deviceList for this user. - deviceListManager.startTrackingDeviceList(listOf(userId)) - } else if (membership == Membership.INVITE && - shouldEncryptForInvitedMembers(roomId) && - isEncryptionEnabledForInvitedUser()) { - // track the deviceList for this invited user. - // Caution: there's a big edge case here in that federated servers do not - // know what other servers are in the room at the time they've been invited. - // They therefore will not send device updates if a user logs in whilst - // their state is invite. - deviceListManager.startTrackingDeviceList(listOf(userId)) - } - } - } - - private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { - if (!event.isStateEvent()) return - val eventContent = event.content.toModel() - val historyVisibility = eventContent?.historyVisibility - if (historyVisibility == null) { - if (cryptoStoreAggregator != null) { - cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false - } else { - // Store immediately - cryptoStore.setShouldShareHistory(roomId, false) - } - } else { - if (cryptoStoreAggregator != null) { - cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED - cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory() - } else { - // Store immediately - cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) - cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) - } - } - } - - /** - * Upload my user's device keys. - */ - private suspend fun uploadDeviceKeys() { - if (cryptoStore.areDeviceKeysUploaded()) { - Timber.tag(loggerTag.value).d("Keys already uploaded, nothing to do") - return - } - // Prepare the device keys data to send - // Sign it - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) - var rest = getMyDevice().toRest() - - rest = rest.copy( - signatures = objectSigner.signObject(canonicalJson) - ) - - val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null, null) - uploadKeysTask.execute(uploadDeviceKeysParams) - - cryptoStore.setDeviceKeysUploaded(true) - } - - /** - * Export the crypto keys. - * - * @param password the password - * @return the exported keys - */ - override suspend fun exportRoomKeys(password: String): ByteArray { - return exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) - } - - /** - * Export the crypto keys. - * - * @param password the password - * @param anIterationCount the encryption iteration count (0 means no encryption) - */ - private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray { - return withContext(coroutineDispatchers.crypto) { - val iterationCount = max(0, anIterationCount) - - val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() } - - val adapter = MoshiProvider.providesMoshi() - .adapter(List::class.java) - - MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) - } - } - - /** - * Import the room keys. - * - * @param roomKeysAsArray the room keys as array. - * @param password the password - * @param progressListener the progress listener - * @return the result ImportRoomKeysResult - */ - override suspend fun importRoomKeys( - roomKeysAsArray: ByteArray, - password: String, - progressListener: ProgressListener? - ): ImportRoomKeysResult { - return withContext(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).v("importRoomKeys starts") - - val t0 = clock.epochMillis() - val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) - val t1 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") - - val importedSessions = MoshiProvider.providesMoshi() - .adapter>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) - .fromJson(roomKeys) - - val t2 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms") - - if (importedSessions == null) { - throw Exception("Error") - } - - megolmSessionDataImporter.handle( - megolmSessionsData = importedSessions, - fromBackup = false, - progressListener = progressListener - ) - } - } - - /** - * Update the warn status when some unknown devices are detected. - * - * @param warn true to warn when some unknown devices are detected. - */ - override fun setWarnOnUnknownDevices(warn: Boolean) { - warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn) - } - - /** - * Check if the user ids list have some unknown devices. - * A success means there is no unknown devices. - * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered. - * - * @param userIds the user ids list - * @param callback the asynchronous callback. - */ - fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { - // force the refresh to ensure that the devices list is up-to-date - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - val keys = deviceListManager.downloadKeys(userIds, true) - val unknownDevices = getUnknownDevices(keys) - if (unknownDevices.map.isNotEmpty()) { - // trigger an an unknown devices exception - throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)) - } - }.foldToCallback(callback) - } - } - - /** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. - * If false, it can still be overridden per-room. - * If true, it overrides the per-room settings. - * - * @param block true to unilaterally blacklist all - */ - override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - cryptoStore.setGlobalBlacklistUnverifiedDevices(block) - } - - override fun enableKeyGossiping(enable: Boolean) { - cryptoStore.enableKeyGossiping(enable) - } - - override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() - - override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled() - - override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable) - - /** - * Tells whether the client should ever send encrypted messages to unverified devices. - * The default value is false. - * This function must be called in the getEncryptingThreadHandler() thread. - * - * @return true to unilaterally blacklist all unverified devices. - */ - override fun getGlobalBlacklistUnverifiedDevices(): Boolean { - return cryptoStore.getGlobalBlacklistUnverifiedDevices() - } - - override fun getLiveGlobalCryptoConfig(): LiveData { - return cryptoStore.getLiveGlobalCryptoConfig() - } - - /** - * Tells whether the client should encrypt messages only for the verified devices - * in this room. - * The default value is false. - * - * @param roomId the room id - * @return true if the client should encrypt messages only for the verified devices. - */ - override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { - return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) } - ?: false - } - - /** - * A live status regarding sharing keys for unverified devices in this room. - * - * @return Live status - */ - override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { - return cryptoStore.getLiveBlockUnverifiedDevices(roomId) - } - - /** - * Add this room to the ones which don't encrypt messages to unverified devices. - * - * @param roomId the room id - * @param block if true will block sending keys to unverified devices - */ - override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { - cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) - } - - /** - * Remove this room to the ones which don't encrypt messages to unverified devices. - * - * @param roomId the room id - */ - override fun setRoomUnBlockUnverifiedDevices(roomId: String) { - setRoomBlockUnverifiedDevices(roomId, false) - } - - /** - * Re request the encryption keys required to decrypt an event. - * - * @param event the event to decrypt again. - */ - override fun reRequestRoomKeyForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, true) - } - - override fun requestRoomKeyForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, false) - } - - /** - * Add a GossipingRequestListener listener. - * - * @param listener listener - */ - override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingKeyRequestManager.addRoomKeysRequestListener(listener) - secretShareManager.addListener(listener) - } - - /** - * Add a GossipingRequestListener listener. - * - * @param listener listener - */ - override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingKeyRequestManager.removeRoomKeysRequestListener(listener) - secretShareManager.removeListener(listener) - } - - /** - * Provides the list of unknown devices. - * - * @param devicesInRoom the devices map - * @return the unknown devices map - */ - private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { - val unknownDevices = MXUsersDevicesMap() - val userIds = devicesInRoom.userIds - for (userId in userIds) { - devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> - devicesInRoom.getObject(userId, deviceId) - ?.takeIf { it.isUnknown } - ?.let { - unknownDevices.setObject(userId, deviceId, it) - } - } - } - - return unknownDevices - } - - override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - deviceListManager.downloadKeys(userIds, forceDownload) - }.foldToCallback(callback) - } - } - - override fun addNewSessionListener(newSessionListener: NewSessionListener) { - roomDecryptorProvider.addNewSessionListener(newSessionListener) - } - - override fun removeSessionListener(listener: NewSessionListener) { - roomDecryptorProvider.removeSessionListener(listener) - } -/* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ - - override fun toString(): String { - return "DefaultCryptoService of $userId ($deviceId)" - } - - override fun getOutgoingRoomKeyRequests(): List { - return cryptoStore.getOutgoingRoomKeyRequests() - } - - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getOutgoingRoomKeyRequestsPaged() - } - - override fun getIncomingRoomKeyRequests(): List { - return cryptoStore.getGossipingEvents() - .mapNotNull { - IncomingRoomKeyRequest.fromEvent(it) - } - } - - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) { - IncomingRoomKeyRequest.fromEvent(it) - ?: IncomingRoomKeyRequest(localCreationTimestamp = 0L) - } - } - - /** - * If you registered a `GossipingRequestListener`, you will be notified of key request - * that was not accepted by the SDK. You can call back this manually to accept anyhow. - */ - override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { - incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request) - } - - override fun getGossipingEventsTrail(): LiveData> { - return cryptoStore.getGossipingEventsTrail() - } - - override fun getGossipingEvents(): List { - return cryptoStore.getGossipingEvents() - } - - override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { - return cryptoStore.getSharedWithInfo(roomId, sessionId) - } - - override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { - return cryptoStore.getWithHeldMegolmSession(roomId, sessionId) - } - - override fun prepareToEncrypt(roomId: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") - // Ensure to load all room members - try { - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") - // we probably shouldn't block sending on that (but questionable) - // but some members won't be able to decrypt - } - - val userIds = getRoomUserIds(roomId) - val alg = roomEncryptorsStore.get(roomId) - ?: getEncryptionAlgorithm(roomId) - ?.let { setEncryptionInRoom(roomId, it, false, userIds) } - ?.let { roomEncryptorsStore.get(roomId) } - - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") - callback.onFailure(IllegalArgumentException("Missing algorithm")) - return@launch - } - - runCatching { - (alg as? IMXGroupEncryption)?.preshareKey(userIds) - }.fold( - { callback.onSuccess(Unit) }, - { - Timber.tag(loggerTag.value).e(it, "prepareToEncrypt() failed.") - callback.onFailure(it) - } - ) - } - } - - override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) { - deviceListManager.downloadKeys(listOf(userId), false) - val userDevices = cryptoStore.getUserDeviceList(userId) - val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo -> - // Get inbound session from sessionId and sessionKey - withContext(coroutineDispatchers.crypto) { - olmDevice.getInboundGroupSession( - sessionId = sessionInfo.sessionId, - senderKey = sessionInfo.senderKey, - roomId = roomId - ).takeIf { it.wrapper.sessionData.sharedHistory } - } - } - - userDevices?.forEach { deviceInfo -> - // Lets share the provided inbound sessions for every user device - sessionToShare.forEach { inboundGroupSession -> - val encryptor = roomEncryptorsStore.get(roomId) - encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo) - Timber.i("## CRYPTO | Sharing inbound session") - } - } - } - - /* ========================================================================================== - * For test only - * ========================================================================================== */ - - @VisibleForTesting - val cryptoStoreForTesting = cryptoStore - - @VisibleForTesting - val olmDeviceForTest = olmDevice - - companion object { - const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt new file mode 100644 index 000000000..0bd6ed06d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/Device.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.SasVerification +import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest +import org.matrix.android.sdk.internal.crypto.verification.prepareMethods +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import org.matrix.rustcomponents.sdk.crypto.LocalTrust +import org.matrix.rustcomponents.sdk.crypto.SignatureException +import org.matrix.rustcomponents.sdk.crypto.Device as InnerDevice + +/** Class representing a device that supports E2EE in the Matrix world + * + * This class can be used to directly start a verification flow with the device + * or to manually verify the device. + */ +internal class Device @AssistedInject constructor( + @Assisted private var innerDevice: InnerDevice, + olmMachine: OlmMachine, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationRequestFactory: VerificationRequest.Factory, + private val sasVerificationFactory: SasVerification.Factory +) { + + @AssistedFactory + interface Factory { + fun create(innerDevice: InnerDevice): Device + } + + private val innerMachine = olmMachine.inner() + + @Throws(CryptoStoreException::class) + private suspend fun refreshData() { + val device = withContext(coroutineDispatchers.io) { + innerMachine.getDevice(innerDevice.userId, innerDevice.deviceId, 30u) + } + + if (device != null) { + innerDevice = device + } + } + + /** + * Request an interactive verification to begin. + * + * This sends out a m.key.verification.request event over to-device messaging to + * to this device. + * + * If no specific device should be verified, but we would like to request + * verification from all our devices, the + * [org.matrix.android.sdk.internal.crypto.OwnUserIdentity.requestVerification] + * method can be used instead. + */ + @Throws(CryptoStoreException::class) + suspend fun requestVerification(methods: List): VerificationRequest? { + val stringMethods = prepareMethods(methods) + val result = withContext(coroutineDispatchers.io) { + innerMachine.requestVerificationWithDevice(innerDevice.userId, innerDevice.deviceId, stringMethods) + } + return if (result != null) { + try { + requestSender.sendVerificationRequest(result.request) + verificationRequestFactory.create(result.verification) + } catch (failure: Throwable) { + // innerMachine.cancelVerification(result.verification.otherUserId, result.verification.flowId, CancelCode.UserError.value) + null + } + } else { + null + } + } + + /** + * Start an interactive verification with this device. + * + * This sends out a m.key.verification.start event with the method set to + * m.sas.v1 to this device using to-device messaging. + * + * This method will soon be deprecated by [MSC3122](https://github.com/matrix-org/matrix-doc/pull/3122). + * The [requestVerification] method should be used instead. + * + */ + @Throws(CryptoStoreException::class) + suspend fun startVerification(): SasVerification? { + val result = withContext(coroutineDispatchers.io) { + innerMachine.startSasWithDevice(innerDevice.userId, innerDevice.deviceId) + } + return if (result != null) { + try { + requestSender.sendVerificationRequest(result.request) + sasVerificationFactory.create(result.sas) + } catch (failure: Throwable) { + result.sas.cancel(CancelCode.UserError.value) +// innerMachine.cancelVerification(result.sas.otherUserId, result.sas.flowId, CancelCode.UserError.value) + null + } + } else { + null + } + } + + /** + * Mark this device as locally trusted. + * + * This won't upload any signatures, it will only mark the device as trusted + * in the local database. + */ + @Throws(CryptoStoreException::class) + suspend fun markAsTrusted() { + withContext(coroutineDispatchers.io) { + innerMachine.setLocalTrust(innerDevice.userId, innerDevice.deviceId, LocalTrust.VERIFIED) + } + } + + /** + * Manually verify this device. + * + * This will sign the device with our self-signing key and upload the signatures + * to the server. + * + * This will fail if the device doesn't belong to use or if we don't have the + * private part of our self-signing key. + */ + @Throws(SignatureException::class) + suspend fun verify(): Boolean { + val request = withContext(coroutineDispatchers.io) { + innerMachine.verifyDevice(innerDevice.userId, innerDevice.deviceId) + } + requestSender.sendSignatureUpload(request) + return true + } + + /** + * Get the DeviceTrustLevel of this device. + */ + @Throws(CryptoStoreException::class) + suspend fun trustLevel(): DeviceTrustLevel { + refreshData() + return DeviceTrustLevel(crossSigningVerified = innerDevice.crossSigningTrusted, locallyVerified = innerDevice.locallyTrusted) + } + + /** + * Convert this device to a CryptoDeviceInfo. + * + * This will not fetch out fresh data from the Rust side. + **/ + internal fun toCryptoDeviceInfo(): CryptoDeviceInfo { +// val keys = innerDevice.keys.map { (keyId, key) -> keyId to key }.toMap() + + return CryptoDeviceInfo( + deviceId = innerDevice.deviceId, + userId = innerDevice.userId, + algorithms = innerDevice.algorithms, + keys = innerDevice.keys, + // The Kotlin side doesn't need to care about signatures, + // so we're not filling this out + signatures = mapOf(), + unsigned = UnsignedDeviceInfo(innerDevice.displayName), + trustLevel = DeviceTrustLevel( + crossSigningVerified = innerDevice.crossSigningTrusted, + locallyVerified = innerDevice.locallyTrusted + ), + isBlocked = innerDevice.isBlocked, + firstTimeSeenLocalTs = innerDevice.firstTimeSeenTs.toLong() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt deleted file mode 100755 index 364d77f7a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixConfiguration -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.extensions.measureMetric -import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper -import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.UserDataToStore -import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.sync.SyncTokenStore -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -// Legacy name: MXDeviceList -@SessionScope -internal class DeviceListManager @Inject constructor( - private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val syncTokenStore: SyncTokenStore, - private val credentials: Credentials, - private val downloadKeysForUsersTask: DownloadKeysForUsersTask, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor, - private val clock: Clock, - matrixConfiguration: MatrixConfiguration -) { - - private val metricPlugins = matrixConfiguration.metricPlugins - - interface UserDevicesUpdateListener { - fun onUsersDeviceUpdate(userIds: List) - } - - private val deviceChangeListeners = mutableListOf() - - fun addListener(listener: UserDevicesUpdateListener) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.add(listener) - } - } - - fun removeListener(listener: UserDevicesUpdateListener) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.remove(listener) - } - } - - private fun dispatchDeviceChange(users: List) { - synchronized(deviceChangeListeners) { - deviceChangeListeners.forEach { - try { - it.onUsersDeviceUpdate(users) - } catch (failure: Throwable) { - Timber.e(failure, "Failed to dispatch device change") - } - } - } - } - - // HS not ready for retry - private val notReadyToRetryHS = mutableSetOf() - - private val cryptoCoroutineContext = coroutineDispatchers.crypto - - init { - taskExecutor.executorScope.launch(cryptoCoroutineContext) { - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - for ((userId, status) in deviceTrackingStatuses) { - if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { - // if a download was in progress when we got shut down, it isn't any more. - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - } - - /** - * Tells if the key downloads should be tried. - * - * @param userId the userId - * @return true if the keys download can be retrieved - */ - private fun canRetryKeysDownload(userId: String): Boolean { - var res = false - - if (':' in userId) { - try { - synchronized(notReadyToRetryHS) { - res = !notReadyToRetryHS.contains(userId.substringAfter(':')) - } - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed") - } - } - - return res - } - - /** - * Clear the unavailable server lists. - */ - private fun clearUnavailableServersList() { - synchronized(notReadyToRetryHS) { - notReadyToRetryHS.clear() - } - } - - fun onRoomMembersLoadedFor(roomId: String) { - taskExecutor.executorScope.launch(cryptoCoroutineContext) { - if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) { - // It's OK to track also device for invited users - val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true) - startTrackingDeviceList(userIds) - refreshOutdatedDeviceLists() - } - } - } - - /** - * Mark the cached device list for the given user outdated - * flag the given user for device-list tracking, if they are not already. - * - * @param userIds the user ids list - */ - fun startTrackingDeviceList(userIds: List) { - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - for (userId in userIds) { - if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { - Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - - /** - * Update the devices list statuses. - * - * @param changed the user ids list which have new devices - * @param left the user ids list which left a room - */ - fun handleDeviceListsChanges(changed: Collection, left: Collection) { - Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}") - var isUpdated = false - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - if (changed.isNotEmpty() || left.isNotEmpty()) { - clearUnavailableServersList() - } - - for (userId in changed) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - isUpdated = true - } - } - - for (userId in left) { - if (deviceTrackingStatuses.containsKey(userId)) { - Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId") - deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED - isUpdated = true - } - } - - if (isUpdated) { - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - } - - /** - * This will flag each user whose devices we are tracking as in need of an update. - */ - fun invalidateAllDeviceLists() { - handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList()) - } - - /** - * The keys download failed. - * - * @param userIds the user ids list - */ - private fun onKeysDownloadFailed(userIds: List) { - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD } - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - } - - /** - * The keys download succeeded. - * - * @param userIds the userIds list - * @param failures the failure map. - */ - private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { - if (failures != null) { - for ((k, value) in failures) { - val statusCode = when (val status = value["status"]) { - is Double -> status.toInt() - is Int -> status.toInt() - else -> 0 - } - if (statusCode == 503) { - synchronized(notReadyToRetryHS) { - notReadyToRetryHS.add(k) - } - } - } - } - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - val usersDevicesInfoMap = MXUsersDevicesMap() - for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId) - if (null == devices) { - if (canRetryKeysDownload(userId)) { - deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD - Timber.e("failed to retry the devices of $userId : retry later") - } else { - if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { - deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER - Timber.e("failed to retry the devices of $userId : the HS is not available") - } - } - } else { - if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { - // we didn't get any new invalidations since this download started: - // this user's device list is now up to date. - deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE - Timber.v("Device list for $userId now up to date") - } - // And the response result - usersDevicesInfoMap.setObjects(userId, devices) - } - } - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - - dispatchDeviceChange(userIds) - return usersDevicesInfoMap - } - - /** - * Download the device keys for a list of users and stores the keys in the MXStore. - * It must be called in getEncryptingThreadHandler() thread. - * - * @param userIds The users to fetch. - * @param forceDownload Always download the keys even if cached. - */ - suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { - Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds") - // Map from userId -> deviceId -> MXDeviceInfo - val stored = MXUsersDevicesMap() - - // List of user ids we need to download keys for - val downloadUsers = ArrayList() - if (null != userIds) { - if (forceDownload) { - downloadUsers.addAll(userIds) - } else { - for (userId in userIds) { - val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED) - // downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys - // not yet retrieved - if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) { - downloadUsers.add(userId) - } else { - val devices = cryptoStore.getUserDevices(userId) - // should always be true - if (devices != null) { - stored.setObjects(userId, devices) - } else { - downloadUsers.add(userId) - } - } - } - } - } - return if (downloadUsers.isEmpty()) { - Timber.v("## CRYPTO | downloadKeys() : no new user device") - stored - } else { - Timber.v("## CRYPTO | downloadKeys() : starts") - val t0 = clock.epochMillis() - try { - val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") - result.also { - it.addEntriesFromMap(stored) - } - } catch (failure: Throwable) { - Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms") - if (forceDownload) { - throw failure - } else { - stored - } - } - } - } - - /** - * Download the devices keys for a set of users. - * - * @param downloadUsers the user ids list - */ - private suspend fun doKeyDownloadForUsers(downloadUsers: List): MXUsersDevicesMap { - Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}") - // get the user ids which did not already trigger a keys download - val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } - if (filteredUsers.isEmpty()) { - // trigger nothing - return MXUsersDevicesMap() - } - val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken()) - val relevantPlugins = metricPlugins.filterIsInstance() - - val response: KeysQueryResponse - relevantPlugins.measureMetric { - response = try { - downloadKeysForUsersTask.execute(params) - } catch (throwable: Throwable) { - Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") - if (throwable is CancellationException) { - // the crypto module is getting closed, so we cannot access the DB anymore - Timber.w("The crypto module is closed, ignoring this error") - } else { - onKeysDownloadFailed(filteredUsers) - } - throw throwable - } - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") - } - - val userDataToStore = UserDataToStore() - - for (userId in filteredUsers) { - // al devices = - val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } - - Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models") - if (!models.isNullOrEmpty()) { - val workingCopy = models.toMutableMap() - for ((deviceId, deviceInfo) in models) { - // Get the potential previously store device keys for this device - val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId) - - // in some race conditions (like unit tests) - // the self device must be seen as verified - if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { - deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true) - } - // Validate received keys - if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { - // New device keys are not valid. Do not store them - workingCopy.remove(deviceId) - if (null != previouslyStoredDeviceKeys) { - // But keep old validated ones if any - workingCopy[deviceId] = previouslyStoredDeviceKeys - } - } else if (null != previouslyStoredDeviceKeys) { - // The verified status is not sync'ed with hs. - // This is a client side information, valid only for this client. - // So, transfer its previous value - workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel - } - } - // Update the store - // Note that devices which aren't in the response will be removed from the stores - userDataToStore.userDevices[userId] = workingCopy - } - - val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") - } - val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") - } - val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") - } - userDataToStore.userIdentities[userId] = UserIdentity( - masterKey = masterKey, - selfSigningKey = selfSigningKey, - userSigningKey = userSigningKey - ) - } - - cryptoStore.storeData(userDataToStore) - - // Update devices trust for these users - // dispatchDeviceChange(downloadUsers) - - return onKeysDownloadSucceed(filteredUsers, response.failures) - } - - /** - * Validate device keys. - * This method must called on getEncryptingThreadHandler() thread. - * - * @param deviceKeys the device keys to validate. - * @param userId the id of the user of the device. - * @param deviceId the id of the device. - * @param previouslyStoredDeviceKeys the device keys we received before for this device - * @return true if succeeds - */ - private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { - if (null == deviceKeys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") - return false - } - - if (null == deviceKeys.keys) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") - return false - } - - if (null == deviceKeys.signatures) { - Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") - return false - } - - // Check that the user_id and device_id in the received deviceKeys are correct - if (deviceKeys.userId != userId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") - return false - } - - if (deviceKeys.deviceId != deviceId) { - Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") - return false - } - - val signKeyId = "ed25519:" + deviceKeys.deviceId - val signKey = deviceKeys.keys[signKeyId] - - if (null == signKey) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") - return false - } - - val signatureMap = deviceKeys.signatures[userId] - - if (null == signatureMap) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") - return false - } - - val signature = signatureMap[signKeyId] - - if (null == signature) { - Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") - return false - } - - var isVerified = false - var errorMessage: String? = null - - try { - olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature) - isVerified = true - } catch (e: Exception) { - errorMessage = e.message - } - - if (!isVerified) { - Timber.e( - "## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + - deviceKeys.deviceId + " with error " + errorMessage - ) - return false - } - - if (null != previouslyStoredDeviceKeys) { - if (previouslyStoredDeviceKeys.fingerprint() != signKey) { - // This should only happen if the list has been MITMed; we are - // best off sticking with the original keys. - // - // Should we warn the user about it somehow? - Timber.e( - "## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + - deviceKeys.deviceId + " has changed : " + - previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey - ) - - Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") - Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") - - return false - } - } - - return true - } - - /** - * Start device queries for any users who sent us an m.new_device recently - * This method must be called on getEncryptingThreadHandler() thread. - */ - suspend fun refreshOutdatedDeviceLists() { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists()") - val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - - val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId -> - TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId] - } - - if (users.isEmpty()) { - return - } - - // update the statuses - users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS } - - cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) - runCatching { - doKeyDownloadForUsers(users) - }.fold( - { - Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done") - }, - { - Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") - } - ) - } - - companion object { - - /** - * State transition diagram for DeviceList.deviceTrackingStatus. - *

-         *
-         *                                   |
-         *        stopTrackingDeviceList     V
-         *      +---------------------> NOT_TRACKED
-         *      |                            |
-         *      +<--------------------+      | startTrackingDeviceList
-         *      |                     |      V
-         *      |   +-------------> PENDING_DOWNLOAD <--------------------+-+
-         *      |   |                      ^ |                            | |
-         *      |   | restart     download | |  start download            | | invalidateUserDeviceList
-         *      |   | client        failed | |                            | |
-         *      |   |                      | V                            | |
-         *      |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
-         *      |                    |       |                              |
-         *      +<-------------------+       |  download successful         |
-         *      ^                            V                              |
-         *      +----------------------- UP_TO_DATE ------------------------+
-         *
-         * 
- */ - - const val TRACKING_STATUS_NOT_TRACKED = -1 - const val TRACKING_STATUS_PENDING_DOWNLOAD = 1 - const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2 - const val TRACKING_STATUS_UP_TO_DATE = 3 - const val TRACKING_STATUS_UNREACHABLE_SERVER = 4 - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt new file mode 100644 index 000000000..ed53b8a28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("EncryptEventContentUseCase", LoggerTag.CRYPTO) + +internal class EncryptEventContentUseCase @Inject constructor( + private val olmMachine: OlmMachine, + private val prepareToEncrypt: PrepareToEncryptUseCase, + private val clock: Clock) { + + suspend operator fun invoke( + eventContent: Content, + eventType: String, + roomId: String): MXEncryptEventContentResult { + val t0 = clock.epochMillis() + + /** + * When using in-room messages and the room has encryption enabled, + * clients should ensure that encryption does not hinder the verification. + * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s + * unverified devices receive the keys necessary to decrypt the messages, + * even if they would normally not be given the keys to decrypt messages in the room. + */ + val shouldSendToUnverified = isVerificationEvent(eventType, eventContent) + + prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false, forceDistributeToUnverified = shouldSendToUnverified) + val content = olmMachine.encrypt(roomId, eventType, eventContent) + Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms") + return MXEncryptEventContentResult(content, EventType.ENCRYPTED) + } + + private fun isVerificationEvent(eventType: String, eventContent: Content) = + EventType.isVerificationEvent(eventType) || + (eventType == EventType.MESSAGE && + eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt new file mode 100644 index 000000000..5d87d8701 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EnsureUsersKeysUseCase.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.RequestType +import java.util.UUID +import javax.inject.Inject +import javax.inject.Provider + +internal class EnsureUsersKeysUseCase @Inject constructor( + private val olmMachine: Provider, + private val outgoingRequestsProcessor: OutgoingRequestsProcessor, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + suspend operator fun invoke(userIds: List, forceDownload: Boolean) { + val olmMachine = olmMachine.get() + if (forceDownload) { + tryOrNull("Failed to download keys for $userIds") { + forceKeyDownload(olmMachine, userIds) + } + } else { + userIds.filter { userId -> + !olmMachine.isUserTracked(userId) + }.also { untrackedUserIds -> + olmMachine.updateTrackedUsers(untrackedUserIds) + } + outgoingRequestsProcessor.processOutgoingRequests(olmMachine) { + it is Request.KeysQuery && it.users.intersect(userIds.toSet()).isNotEmpty() + } + } + } + + @Throws + private suspend fun forceKeyDownload(olmMachine: OlmMachine, userIds: List) { + withContext(coroutineDispatchers.io) { + val requestId = UUID.randomUUID().toString() + val response = requestSender.queryKeys(Request.KeysQuery(requestId, userIds)) + olmMachine.markRequestAsSent(requestId, RequestType.KEYS_QUERY, response) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index ac9c61a32..fe57cf553 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,268 +16,21 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber import javax.inject.Inject -private const val SEND_TO_DEVICE_RETRY_COUNT = 3 +internal class EventDecryptor @Inject constructor(val decryptRoomEventUseCase: DecryptRoomEventUseCase) { -private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO) - -@SessionScope -internal class EventDecryptor @Inject constructor( - private val cryptoCoroutineScope: CoroutineScope, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val clock: Clock, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val messageEncrypter: MessageEncrypter, - private val sendToDeviceTask: SendToDeviceTask, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, -) { - - /** - * Rate limit unwedge attempt, should we persist that? - */ - private val lastNewSessionForcedDates = mutableMapOf() - - data class WedgedDeviceInfo( - val userId: String, - val senderKey: String? - ) - - private val wedgedMutex = Mutex() - private val wedgedDevices = mutableListOf() - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or throw in case of error - */ @Throws(MXCryptoError::class) + @Suppress("UNUSED_PARAMETER") suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return internalDecryptEvent(event, timeline) + return decryptRoomEventUseCase.invoke(event) } - /** - * Decrypt an event and save the result in the given event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - */ + @Suppress("UNUSED_PARAMETER") suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { - // event is not encrypted or already decrypted - if (event.getClearType() != EventType.ENCRYPTED) return - - tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") { - decryptEvent(event, timeline) - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } - } - - /** - * Decrypt an event asynchronously. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param callback the callback to return data or null - */ - fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - // is it needed to do that on the crypto scope?? - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - runCatching { - internalDecryptEvent(event, timeline) - }.foldToCallback(callback) - } - } - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the MXEventDecryptionResult data, or null in case of error - */ - @Throws(MXCryptoError::class) - private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val eventContent = event.content - if (eventContent == null) { - Timber.tag(loggerTag.value).e("decryptEvent : empty event content") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } else if (event.isRedacted()) { - // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm - return MXEventDecryptionResult( - clearEvent = mapOf( - "room_id" to event.roomId.orEmpty(), - "type" to EventType.MESSAGE, - "content" to emptyMap(), - "unsigned" to event.unsignedData.toContent() - ) - ) - } else { - val algorithm = eventContent["algorithm"]?.toString() - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) - Timber.tag(loggerTag.value).e("decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) - } else { - try { - return alg.decryptEvent(event, timeline) - } catch (mxCryptoError: MXCryptoError) { - Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") - if (algorithm == MXCRYPTO_ALGORITHM_OLM) { - if (mxCryptoError is MXCryptoError.Base && - mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { - // need to find sending device - val olmContent = event.content.toModel() - if (event.senderId != null && olmContent?.senderKey != null) { - markOlmSessionForUnwedging(event.senderId, olmContent.senderKey) - } else { - Timber.tag(loggerTag.value).d("Can't mark as wedge malformed") - } - } - } - throw mxCryptoError - } - } - } - } - - private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) { - wedgedMutex.withLock { - val info = WedgedDeviceInfo(senderId, senderKey) - if (!wedgedDevices.contains(info)) { - Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged") - wedgedDevices.add(info) - } - } - } - - // coroutineDispatchers.crypto scope - suspend fun unwedgeDevicesIfNeeded() { - // handle wedged devices - // Some olm decryption have failed and some device are wedged - // we should force start a new session for those - Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged") - // get the one that should be retried according to rate limit - val now = clock.epochMillis() - val toUnwedge = wedgedMutex.withLock { - wedgedDevices.filter { - val lastForcedDate = lastNewSessionForcedDates[it] ?: 0 - if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate") - return@filter false - } - // let's already mark that we tried now - lastNewSessionForcedDates[it] = now - true - } - } - - if (toUnwedge.isEmpty()) { - Timber.tag(loggerTag.value).v("Nothing to unwedge") - return - } - Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices") - - toUnwedge - .chunked(100) // safer to chunk if we ever have lots of wedged devices - .forEach { wedgedList -> - val groupedByUserId = wedgedList.groupBy { it.userId } - // lets download keys if needed - withContext(coroutineDispatchers.io) { - deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false) - } - - // find the matching devices - groupedByUserId - .map { groupedByUser -> - val userId = groupedByUser.key - val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey } - val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - userId to wedgeSenderKeysForUser.mapNotNull { senderKey -> - knownDevices.firstOrNull { it.identityKey() == senderKey } - } - } - .toMap() - .let { deviceList -> - try { - // force creating new outbound session and mark them as most recent to - // be used for next encryption (dummy) - val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true) - Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to") - - // Now send a dummy message on that session so the other side knows about it. - val payloadJson = mapOf( - "type" to EventType.DUMMY - ) - val sendToDeviceMap = MXUsersDevicesMap() - sessionToUse.map.values - .flatMap { it.values } - .map { it.deviceInfo } - .forEach { deviceInfo -> - Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}") - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload) - } - - // now let's send that - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT) - } - - deviceList.values.flatten().forEach { deviceInfo -> - wedgedMutex.withLock { - wedgedDevices.removeAll { - it.senderKey == deviceInfo.identityKey() && - it.userId == deviceInfo.userId - } - } - } - } catch (failure: Throwable) { - deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let { - Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}") - } - } - } - } + return decryptRoomEventUseCase.decryptAndSaveResult(event) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt new file mode 100644 index 000000000..391c0a2ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/FlowCollectors.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.util.Optional + +internal data class UserIdentityCollector(val userId: String, val collector: SendChannel>) : + SendChannel> by collector + +internal data class DevicesCollector(val userIds: List, val collector: SendChannel>) : + SendChannel> by collector + +private typealias PrivateKeysCollector = SendChannel> + +internal class FlowCollectors { + private val userIdentityCollectors = mutableListOf() + private val privateKeyCollectors = mutableListOf() + private val deviceCollectors = ArrayList() + + private val identityLock = Mutex() + private val keysLock = Mutex() + private val deviceLock = Mutex() + + suspend fun addIdentityCollector(collector: UserIdentityCollector) { + identityLock.withLock { + userIdentityCollectors.add(collector) + } + } + + fun removeIdentityCollector(collector: UserIdentityCollector) { + // Annoying but it's called when the channel is closed and can't call + // something suspendable there :/ + runBlocking { + identityLock.withLock { + userIdentityCollectors.remove(collector) + } + } + } + + suspend fun forEachIdentityCollector(block: suspend ((UserIdentityCollector) -> Unit)) { + val safeCopy = identityLock.withLock { + userIdentityCollectors.toList() + } + safeCopy.onEach { block(it) } + } + + suspend fun addPrivateKeysCollector(collector: PrivateKeysCollector) { + keysLock.withLock { + privateKeyCollectors.add(collector) + } + } + + fun removePrivateKeysCollector(collector: PrivateKeysCollector) { + // Annoying but it's called when the channel is closed and can't call + // something suspendable there :/ + runBlocking { + keysLock.withLock { + privateKeyCollectors.remove(collector) + } + } + } + + suspend fun forEachPrivateKeysCollector(block: suspend ((PrivateKeysCollector) -> Unit)) { + val safeCopy = keysLock.withLock { + privateKeyCollectors.toList() + } + safeCopy.onEach { block(it) } + } + + suspend fun addDevicesCollector(collector: DevicesCollector) { + deviceLock.withLock { + deviceCollectors.add(collector) + } + } + + fun removeDevicesCollector(collector: DevicesCollector) { + // Annoying but it's called when the channel is closed and can't call + // something suspendable there :/ + runBlocking { + deviceLock.withLock { + deviceCollectors.remove(collector) + } + } + } + + suspend fun forEachDevicesCollector(block: suspend ((DevicesCollector) -> Unit)) { + val safeCopy = deviceLock.withLock { + deviceCollectors.toList() + } + safeCopy.onEach { block(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt new file mode 100644 index 000000000..a12bf2eb8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import javax.inject.Inject + +internal class GetRoomUserIdsUseCase @Inject constructor(private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider) { + + operator fun invoke(roomId: String): List { + return cryptoSessionInfoProvider.getRoomUserIds(roomId, shouldEncryptForInvitedMembers(roomId)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt new file mode 100644 index 000000000..0725edbc8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetUserIdentityUseCase.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.squareup.moshi.Moshi +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider +import org.matrix.rustcomponents.sdk.crypto.UserIdentity as InnerUserIdentity + +internal class GetUserIdentityUseCase @Inject constructor( + private val olmMachine: Provider, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val moshi: Moshi, + private val verificationRequestFactory: VerificationRequest.Factory +) { + + @Throws(CryptoStoreException::class) + suspend operator fun invoke(userId: String): UserIdentities? { + val innerMachine = olmMachine.get().inner() + val identity = try { + withContext(coroutineDispatchers.io) { + innerMachine.getIdentity(userId, 30u) + } + } catch (error: CryptoStoreException) { + Timber.w(error, "Failed to get identity for user $userId") + return null + } + val adapter = moshi.adapter(RestKeyInfo::class.java) + + return when (identity) { + is InnerUserIdentity.Other -> { + val verified = innerMachine.isIdentityVerified(userId) + val masterKey = adapter.fromJson(identity.masterKey)!!.toCryptoModel().apply { + trustLevel = DeviceTrustLevel(verified, verified) + } + val selfSigningKey = adapter.fromJson(identity.selfSigningKey)!!.toCryptoModel().apply { + trustLevel = DeviceTrustLevel(verified, verified) + } + UserIdentity( + userId = identity.userId, + masterKey = masterKey, + selfSigningKey = selfSigningKey, + innerMachine = innerMachine, + requestSender = requestSender, + coroutineDispatchers = coroutineDispatchers, + verificationRequestFactory = verificationRequestFactory + ) + } + is InnerUserIdentity.Own -> { + val verified = innerMachine.isIdentityVerified(userId) + + val masterKey = adapter.fromJson(identity.masterKey)!!.toCryptoModel().apply { + trustLevel = DeviceTrustLevel(verified, verified) + } + val selfSigningKey = adapter.fromJson(identity.selfSigningKey)!!.toCryptoModel().apply { + trustLevel = DeviceTrustLevel(verified, verified) + } + val userSigningKey = adapter.fromJson(identity.userSigningKey)!!.toCryptoModel() + + OwnUserIdentity( + userId = identity.userId, + masterKey = masterKey, + selfSigningKey = selfSigningKey, + userSigningKey = userSigningKey, + trustsOurOwnDevice = identity.trustsOurOwnDevice, + innerMachine = innerMachine, + requestSender = requestSender, + coroutineDispatchers = coroutineDispatchers, + verificationRequestFactory = verificationRequestFactory + ) + } + null -> null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt deleted file mode 100644 index 6d197a09e..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.util.LruCache -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber -import javax.inject.Inject - -internal data class InboundGroupSessionHolder( - val wrapper: MXInboundMegolmSessionWrapper, - val mutex: Mutex = Mutex() -) - -private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO) - -/** - * Allows to cache and batch store operations on inbound group session store. - * Because it is used in the decrypt flow, that can be called quite rapidly - */ -internal class InboundGroupSessionStore @Inject constructor( - private val store: IMXCryptoStore, - private val cryptoCoroutineScope: CoroutineScope, - private val coroutineDispatchers: MatrixCoroutineDispatchers -) { - - private data class CacheKey( - val sessionId: String, - val senderKey: String - ) - - private val sessionCache = object : LruCache(100) { - override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) { - if (oldValue != null) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}") - // store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper }) - oldValue.wrapper.session.releaseSession() - } - } - } - } - - @Synchronized - fun clear() { - sessionCache.evictAll() - } - - @Synchronized - fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? { - val known = sessionCache[CacheKey(sessionId, senderKey)] - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}") - return known - ?: store.getInboundGroupSession(sessionId, senderKey)?.also { - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}") - sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it)) - }?.let { - InboundGroupSessionHolder(it) - } - } - - @Synchronized - fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - store.removeInboundGroupSession(sessionId, senderKey) - sessionCache.remove(CacheKey(sessionId, senderKey)) - - // release removed session - old.wrapper.session.releaseSession() - - internalStoreGroupSession(new, sessionId, senderKey) - } - - @Synchronized - fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}") - - store.storeInboundGroupSessions( - listOf( - old.wrapper.copy( - sessionData = old.wrapper.sessionData.copy(trusted = true) - ) - ) - ) - // will release it :/ - sessionCache.remove(CacheKey(sessionId, senderKey)) - } - - @Synchronized - fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - internalStoreGroupSession(holder, sessionId, senderKey) - } - - private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { - Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}") - - if (sessionCache[CacheKey(sessionId, senderKey)] == null) { - // first time seen, put it in memory cache while waiting for batch insert - // If it's already known, no need to update cache it's already there - sessionCache.put(CacheKey(sessionId, senderKey), holder) - } - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - store.storeInboundGroupSessions(listOf(holder.wrapper)) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt deleted file mode 100644 index 729b4481e..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.system.measureTimeMillis - -private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO) - -@SessionScope -internal class IncomingKeyRequestManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val olmDevice: MXOlmDevice, - private val cryptoConfig: MXCryptoConfig, - private val messageEncrypter: MessageEncrypter, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sendToDeviceTask: SendToDeviceTask, - private val clock: Clock, -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) - val sequencer = SemaphoreCoroutineSequencer() - - private val incomingRequestBuffer = mutableListOf() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - enum class MegolmRequestAction { - Request, Cancel - } - - data class ValidMegolmRequestBody( - val requestId: String, - val requestingUserId: String, - val requestingDeviceId: String, - val roomId: String, - val senderKey: String, - val sessionId: String, - val action: MegolmRequestAction - ) { - fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId" - } - - private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? { - val deviceId = requestingDeviceId ?: return null - val body = body ?: return null - val roomId = body.roomId ?: return null - val sessionId = body.sessionId ?: return null - val senderKey = body.senderKey ?: return null - val requestId = this.requestId ?: return null - if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - val action = when (this.action) { - "request" -> MegolmRequestAction.Request - "request_cancellation" -> MegolmRequestAction.Cancel - else -> null - } ?: return null - return ValidMegolmRequestBody( - requestId = requestId, - requestingUserId = senderId, - requestingDeviceId = deviceId, - roomId = roomId, - senderKey = senderKey, - sessionId = sessionId, - action = action - ) - } - - fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) { - if (!cryptoStore.isKeyGossipingEnabled()) { - Timber.tag(loggerTag.value) - .i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}") - return - } - outgoingRequestScope.launch { - // It is important to handle requests in order - sequencer.post { - val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also { - Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}") - } - - // is there already one like that? - val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest } - if (existing == null) { - when (validMegolmRequest.action) { - MegolmRequestAction.Request -> { - // just add to the buffer - incomingRequestBuffer.add(validMegolmRequest) - } - MegolmRequestAction.Cancel -> { - // ignore, we can't cancel as it's not known (probably already processed) - // still notify app layer if it was passed up previously - IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - listenersCopy.onEach { - tryOrNull { - withContext(coroutineDispatchers.main) { - it.onRequestCancelled(iReq) - } - } - } - } - } - } - } - } else { - when (validMegolmRequest.action) { - MegolmRequestAction.Request -> { - // it's already in buffer, nop keep existing - } - MegolmRequestAction.Cancel -> { - // discard the request in buffer - incomingRequestBuffer.remove(existing) - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - listenersCopy.onEach { - IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> - withContext(coroutineDispatchers.main) { - tryOrNull { it.onRequestCancelled(iReq) } - } - } - } - } - } - } - } - } - } - } - - fun processIncomingRequests() { - outgoingRequestScope.launch { - sequencer.post { - measureTimeMillis { - Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process") - incomingRequestBuffer.forEach { - // should not happen, we only store requests - if (it.action != MegolmRequestAction.Request) return@forEach - try { - handleIncomingRequest(it) - } catch (failure: Throwable) { - // ignore and continue, should not happen - Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it") - } - } - incomingRequestBuffer.clear() - }.let { duration -> - Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms") - } - } - } - } - - private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) { - // We don't want to download keys, if we don't know the device yet we won't share any how? - val requestingDevice = - cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}") - } - - cryptoStore.saveIncomingKeyRequestAuditTrail( - request.requestId, - request.roomId, - request.sessionId, - request.senderKey, - MXCRYPTO_ALGORITHM_MEGOLM, - request.requestingUserId, - request.requestingDeviceId - ) - - val roomAlgorithm = // withContext(coroutineDispatchers.crypto) { - cryptoStore.getRoomAlgorithm(request.roomId) -// } - if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { - // strange we received a request for a room that is not encrypted - // maybe a broken state? - Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}") - return - } - - // Is it for one of our sessions? - if (request.requestingUserId == credentials.userId) { - Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}") - - if (request.requestingDeviceId == credentials.deviceId) { - // ignore it's a remote echo - return - } - // If it's verified we share from the early index we know - // if not we check if it was originaly shared or not - if (requestingDevice.isVerified) { - // we share from the earliest known chain index - shareMegolmKey(request, requestingDevice, null) - } else { - shareIfItWasPreviouslyShared(request, requestingDevice) - } - } else { - if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { - Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}") - return - } - Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}") - if (requestingDevice.isBlocked) { - // it's blocked, so send a withheld code - sendWithheldForRequest(request, WithHeldCode.BLACKLISTED) - } else { - shareIfItWasPreviouslyShared(request, requestingDevice) - } - } - } - - private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) { - // we don't reshare unless it was previously shared with - val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) { - cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice) - } - if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) { - // we share from the index it was previously shared with - shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong()) - } else { - val isOwnDevice = requestingDevice.userId == credentials.userId - sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED) - // if it's our device we could delegate to the app layer to decide - if (isOwnDevice) { - outgoingRequestScope.launch(coroutineDispatchers.computation) { - val listenersCopy = synchronized(gossipingRequestListeners) { - gossipingRequestListeners.toList() - } - val iReq = IncomingRoomKeyRequest( - userId = requestingDevice.userId, - deviceId = requestingDevice.deviceId, - requestId = request.requestId, - requestBody = RoomKeyRequestBody( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - senderKey = request.senderKey, - sessionId = request.sessionId, - roomId = request.roomId - ), - localCreationTimestamp = clock.epochMillis() - ) - listenersCopy.onEach { - withContext(coroutineDispatchers.main) { - tryOrNull { it.onRoomKeyRequest(iReq) } - } - } - } - } - } - } - - private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) { - Timber.tag(loggerTag.value) - .w("Send withheld $code for req: ${request.shortDbgString()}") - val withHeldContent = RoomKeyWithHeldContent( - roomId = request.roomId, - senderKey = request.senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - sessionId = request.sessionId, - codeString = code.value, - fromDevice = credentials.deviceId - ) - - val params = SendToDeviceTask.Params( - EventType.ROOM_KEY_WITHHELD.stable, - MXUsersDevicesMap().apply { - setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent) - } - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - Timber.tag(loggerTag.value) - .d("Send withheld $code req: ${request.shortDbgString()}") - } - - cryptoStore.saveWithheldAuditTrail( - roomId = request.roomId, - sessionId = request.sessionId, - senderKey = request.senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - code = code, - userId = request.requestingUserId, - deviceId = request.requestingDeviceId - ) - } catch (failure: Throwable) { - // Ignore it's not that important? - // do we want to fallback to a worker? - Timber.tag(loggerTag.value) - .w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}") - } - } - - suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { - request.requestId ?: return - request.deviceId ?: return - request.userId ?: return - request.requestBody?.roomId ?: return - request.requestBody.senderKey ?: return - request.requestBody.sessionId ?: return - val validReq = ValidMegolmRequestBody( - requestId = request.requestId, - requestingDeviceId = request.deviceId, - requestingUserId = request.userId, - roomId = request.requestBody.roomId, - senderKey = request.requestBody.senderKey, - sessionId = request.requestBody.sessionId, - action = MegolmRequestAction.Request - ) - val requestingDevice = - cryptoStore.getUserDevice(request.userId, request.deviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}") - } - - shareMegolmKey(validReq, requestingDevice, null) - } - - private suspend fun shareMegolmKey( - validRequest: ValidMegolmRequestBody, - requestingDevice: CryptoDeviceInfo, - chainIndex: Long? - ): Boolean { - Timber.tag(loggerTag.value) - .d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}") - - val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to establish olm session") - sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) - return false - } - - val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value) - .w("reshareKey: no session with this device, probably because there were no one-time keys") - sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) - return false - } - val sessionHolder = try { - olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}") - // It's unavailable - sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE) - return false - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys(chainIndex) - } ?: return false.also { - Timber.tag(loggerTag.value) - .e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload) - Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - return try { - sendToDeviceTask.execute(sendToDeviceParams) - Timber.tag(loggerTag.value) - .i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - cryptoStore.saveForwardKeyAuditTrail( - validRequest.roomId, - validRequest.sessionId, - validRequest.senderKey, - MXCRYPTO_ALGORITHM_MEGOLM, - requestingDevice.userId, - requestingDevice.deviceId, - chainIndex - ) - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") - false - } - } - - fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - fun close() { - try { - outgoingRequestScope.cancel("User Terminate") - incomingRequestBuffer.clear() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("Failed to shutDown request manager") - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt deleted file mode 100755 index faadf339e..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ /dev/null @@ -1,963 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper -import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertFromUTF8 -import org.matrix.android.sdk.internal.util.convertToUTF8 -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmException -import org.matrix.olm.OlmInboundGroupSession -import org.matrix.olm.OlmMessage -import org.matrix.olm.OlmOutboundGroupSession -import org.matrix.olm.OlmSession -import org.matrix.olm.OlmUtility -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) - -// The libolm wrapper. -@SessionScope -internal class MXOlmDevice @Inject constructor( - /** - * The store where crypto data is saved. - */ - private val store: IMXCryptoStore, - private val olmSessionStore: OlmSessionStore, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val clock: Clock, -) { - - val mutex = Mutex() - - /** - * @return the Curve25519 key for the account. - */ - var deviceCurve25519Key: String? = null - private set - - /** - * @return the Ed25519 key for the account. - */ - var deviceEd25519Key: String? = null - private set - - // The OLM lib utility instance. - private var olmUtility: OlmUtility? = null - - private data class GroupSessionCacheItem( - val groupId: String, - val groupSession: OlmOutboundGroupSession - ) - - // The outbound group session. - // Caches active outbound session to avoid to sync with DB before read - // The key is the session id, the value the . - private val outboundGroupSessionCache: MutableMap = HashMap() - - // Store a set of decrypted message indexes for each group session. - // This partially mitigates a replay attack where a MITM resends a group - // message into the room. - // - // The Matrix SDK exposes events through MXEventTimelines. A developer can open several - // timelines from a same room so that a message can be decrypted several times but from - // a different timeline. - // So, store these message indexes per timeline id. - // - // The first level keys are timeline ids. - // The second level values is a Map that represents: - // "|||" --> eventId - private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() - - init { - // Retrieve the account from the store - try { - store.getOrCreateOlmAccount() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount") - } - - try { - olmUtility = OlmUtility() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error") - olmUtility = null - } - - try { - deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") - } - - try { - deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") - } - } - - /** - * @return The current (unused, unpublished) one-time keys for this account. - */ - fun getOneTimeKeys(): Map>? { - try { - return store.doWithOlmAccount { it.oneTimeKeys() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") - } - - return null - } - - /** - * @return The maximum number of one-time keys the olm account can store. - */ - fun getMaxNumberOfOneTimeKeys(): Long { - return store.doWithOlmAccount { it.maxOneTimeKeys() } - } - - /** - * Returns an unpublished fallback key. - * A call to markKeysAsPublished will mark it as published and this - * call will return null (until a call to generateFallbackKey is made). - */ - fun getFallbackKey(): MutableMap>? { - try { - return store.doWithOlmAccount { it.fallbackKey() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") - } - return null - } - - /** - * Generates a new fallback key if there is not already - * an unpublished one. - * @return true if a new key was generated - */ - fun generateFallbackKeyIfNeeded(): Boolean { - try { - if (!hasUnpublishedFallbackKey()) { - store.doWithOlmAccount { - it.generateFallbackKey() - store.saveOlmAccount() - } - return true - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed") - } - return false - } - - internal fun hasUnpublishedFallbackKey(): Boolean { - return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty() - } - - fun forgetFallbackKey() { - try { - store.doWithOlmAccount { - it.forgetFallbackKey() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed") - } - } - - /** - * Release the instance. - */ - fun release() { - olmUtility?.releaseUtility() - outboundGroupSessionCache.values.forEach { - it.groupSession.releaseSession() - } - outboundGroupSessionCache.clear() - inboundGroupSessionStore.clear() - olmSessionStore.clear() - } - - /** - * Signs a message with the ed25519 key for this account. - * - * @param message the message to be signed. - * @return the base64-encoded signature. - */ - fun signMessage(message: String): String? { - try { - return store.doWithOlmAccount { it.signMessage(message) } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") - } - - return null - } - - /** - * Marks all of the one-time keys as published. - */ - fun markKeysAsPublished() { - try { - store.doWithOlmAccount { - it.markOneTimeKeysAsPublished() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed") - } - } - - /** - * Generate some new one-time keys. - * - * @param numKeys number of keys to generate - */ - fun generateOneTimeKeys(numKeys: Int) { - try { - store.doWithOlmAccount { - it.generateOneTimeKeys(numKeys) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed") - } - } - - /** - * Generate a new outbound session. - * The new session will be stored in the MXStore. - * - * @param theirIdentityKey the remote user's Curve25519 identity key - * @param theirOneTimeKey the remote user's one-time Curve25519 key - * @return the session id for the outbound session. - */ - fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { - Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") - var olmSession: OlmSession? = null - - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) - } - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - - // Pretend we've received a message at this point, otherwise - // if we try to send a message to the device, it won't use - // this session - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - - olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) - - val sessionIdentifier = olmSession.sessionIdentifier() - - Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") - return sessionIdentifier - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Generate a new inbound session, given an incoming message. - * - * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. - * @param messageType the message_type field from the received message (must be 0). - * @param ciphertext base64-encoded body from the received message. - * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. - */ - fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { - Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") - - var olmSession: OlmSession? = null - - try { - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed") - return null - } - - Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") - - try { - store.doWithOlmAccount { olmAccount -> - olmAccount.removeOneTimeKeys(olmSession) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") - } - - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - var payloadString: String? = null - - try { - payloadString = olmSession.decryptMessage(olmMessage) - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - // This counts as a received message: set last received message time to now - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed") - } - - val res = HashMap() - - if (!payloadString.isNullOrEmpty()) { - res["payload"] = payloadString - } - - val sessionIdentifier = olmSession.sessionIdentifier() - - if (!sessionIdentifier.isNullOrEmpty()) { - res["session_id"] = sessionIdentifier - } - - return res - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Get a list of known session IDs for the given device. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return a list of known session ids for the device. - */ - fun getSessionIds(theirDeviceIdentityKey: String): List { - return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey) - } - - /** - * Get the right olm session id for encrypting messages to the given identity key. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return the session id, or null if no established session. - */ - fun getSessionId(theirDeviceIdentityKey: String): String? { - return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey) - } - - /** - * Encrypt an outgoing message using an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session - * @param payloadString the payload to be encrypted and sent - * @return the cipher text - */ - suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (olmSessionWrapper != null) { - try { - Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") - - val olmMessage = olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.encryptMessage(payloadString) - } - return mapOf( - "body" to olmMessage.mCipherText, - "type" to olmMessage.mType, - ).also { - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") - return null - } - } else { - Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId") - return null - } - } - - /** - * Decrypt an incoming message using an existing session. - * - * @param ciphertext the base64-encoded body from the received message. - * @param messageType message_type field from the received message. - * @param sessionId the id of the active session. - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return the decrypted payload. - */ - @kotlin.jvm.Throws - suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { - var payloadString: String? = null - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (null != olmSessionWrapper) { - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - payloadString = - olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived(clock.epochMillis()) - } - } - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - - return payloadString - } - - /** - * Determine if an incoming messages is a prekey message matching an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. - * @param messageType message_type field from the received message. - * @param ciphertext the base64-encoded body from the received message. - * @return YES if the received message is a prekey message which matchesthe given session. - */ - fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { - if (messageType != 0) { - return false - } - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) - } - - // Outbound group session - - /** - * Generate a new outbound group session. - * - * @return the session id for the outbound session. - */ - fun createOutboundGroupSessionForRoom(roomId: String): String? { - var session: OlmOutboundGroupSession? = null - try { - session = OlmOutboundGroupSession() - outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session) - store.storeCurrentOutboundGroupSessionForRoom(roomId, session) - return session.sessionIdentifier() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession") - - session?.releaseSession() - } - - return null - } - - fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) { - outboundGroupSessionCache[sessionId]?.let { - store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession) - } - } - - fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? { - val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId) - if (restoredOutboundGroupSession != null) { - val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier() - // cache it - outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession) - - return MXOutboundSessionInfo( - sessionId = sessionId, - sharedWithHelper = SharedWithHelper(roomId, sessionId, store), - clock = clock, - creationTime = restoredOutboundGroupSession.creationTime, - sharedHistory = restoredOutboundGroupSession.sharedHistory - ) - } - return null - } - - fun discardOutboundGroupSessionForRoom(roomId: String) { - val toDiscard = outboundGroupSessionCache.filter { - it.value.groupId == roomId - } - toDiscard.forEach { (sessionId, cacheItem) -> - cacheItem.groupSession.releaseSession() - outboundGroupSessionCache.remove(sessionId) - } - store.storeCurrentOutboundGroupSessionForRoom(roomId, null) - } - - /** - * Get the current session key of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the base64-encoded secret key. - */ - fun getSessionKey(sessionId: String): String? { - if (sessionId.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed") - } - } - return null - } - - /** - * Get the current message index of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the current chain index. - */ - fun getMessageIndex(sessionId: String): Int { - return if (sessionId.isNotEmpty()) { - outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex() - } else 0 - } - - /** - * Encrypt an outgoing message with an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @param payloadString the payload to be encrypted and sent. - * @return ciphertext - */ - fun encryptGroupMessage(sessionId: String, payloadString: String): String? { - if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed") - } - } - return null - } - - // Inbound group session - - sealed interface AddSessionResult { - data class Imported(val ratchetIndex: Int) : AddSessionResult - abstract class Failure : AddSessionResult - object NotImported : Failure() - data class NotImportedHigherIndex(val newIndex: Int) : Failure() - } - - /** - * Add an inbound group session to the session store. - * - * @param sessionId the session identifier. - * @param sessionKey base64-encoded secret key. - * @param roomId the id of the room in which this session will be used. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. - * @param keysClaimed Other keys the sender claims. - * @param exportFormat true if the megolm keys are in export format - * @param sharedHistory MSC3061, this key is sharable on invite - * @param trusted True if the key is coming from a trusted source - * @return true if the operation succeeds. - */ - fun addInboundGroupSession( - sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean, - sharedHistory: Boolean, - trusted: Boolean - ): AddSessionResult { - val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { - if (exportFormat) { - OlmInboundGroupSession.importSession(sessionKey) - } else { - OlmInboundGroupSession(sessionKey) - } - } ?: return AddSessionResult.NotImported.also { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId") - } - - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - // If we have an existing one we should check if the new one is not better - if (existingSession != null) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") - try { - val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { - // This is quite unexpected, could throw if native was released? - Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") - candidateSession.releaseSession() - // Probably should discard it? - } - val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex } - ?: return AddSessionResult.NotImported.also { - candidateSession.releaseSession() - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index") - } - - val keyConnects = existingSession.session.connects(candidateSession) - if (!keyConnects) { - Timber.tag(loggerTag.value) - .e("## addInboundGroupSession() Unconnected key") - if (!trusted) { - // Ignore the not connecting unsafe, keep existing - Timber.tag(loggerTag.value) - .e("## addInboundGroupSession() Received unsafe unconnected key") - return AddSessionResult.NotImported - } - // else if the new one is safe and does not connect with existing, import the new one - } else { - // If our existing session is better we keep it - if (existingFirstKnown <= newKnownFirstIndex) { - val shouldUpdateTrust = trusted && (existingSession.sessionData.trusted != true) - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : updateTrust for $sessionId") - if (shouldUpdateTrust) { - // the existing as a better index but the new one is trusted so update trust - inboundGroupSessionStore.updateToSafe(existingSessionHolder, sessionId, senderKey) - } - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession.releaseSession() - return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) - } - } - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession.releaseSession() - return AddSessionResult.NotImported - } - } - - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - - try { - if (candidateSession.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateSession.releaseSession() - return AddSessionResult.NotImported - } - } catch (e: Throwable) { - candidateSession.releaseSession() - Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") - return AddSessionResult.NotImported - } - - val candidateSessionData = InboundGroupSessionData( - senderKey = senderKey, - roomId = roomId, - keysClaimed = keysClaimed, - forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, - sharedHistory = sharedHistory, - trusted = trusted - ) - - val wrapper = MXInboundMegolmSessionWrapper( - candidateSession, - candidateSessionData - ) - if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey) - } else { - inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey) - } - - return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) - } - - fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean { - return try { - val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex) - this.export(lowestCommonIndex) == other.export(lowestCommonIndex) - } catch (failure: Throwable) { - // native error? key disposed? - false - } - } - - /** - * Import an inbound group sessions to the session store. - * - * @param megolmSessionsData the megolm sessions data - * @return the successfully imported sessions. - */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) - - for (megolmSessionData in megolmSessionsData) { - val sessionId = megolmSessionData.sessionId ?: continue - val senderKey = megolmSessionData.senderKey ?: continue - val roomId = megolmSessionData.roomId - - val candidateSessionToImport = try { - MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") - continue - } - - val candidateOlmInboundGroupSession = candidateSessionToImport.session - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - - if (existingSession == null) { - // Session does not already exist, add it - Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") - sessions.add(candidateSessionToImport) - } else { - Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } - val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex } - - if (existingFirstKnown == null || candidateFirstKnownIndex == null) { - // should not happen? - candidateSessionToImport.session.releaseSession() - Timber.tag(loggerTag.value) - .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") - } else { - if (existingFirstKnown <= candidateFirstKnownIndex) { - // Ignore this, keep existing - candidateOlmInboundGroupSession.releaseSession() - } else { - // update cache with better session - inboundGroupSessionStore.replaceGroupSession( - existingSessionHolder, - InboundGroupSessionHolder(candidateSessionToImport), - sessionId, - senderKey - ) - sessions.add(candidateSessionToImport) - } - } - } - } - - store.storeInboundGroupSessions(sessions) - - return sessions - } - - /** - * Decrypt a received message with an inbound group session. - * - * @param body the base64-encoded body of the encrypted message. - * @param roomId the room in which the message was received. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param eventId the eventId of the message that will be decrypted - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return the decrypting result. Null if the sessionId is unknown. - */ - @Throws(MXCryptoError::class) - suspend fun decryptGroupMessage( - body: String, - roomId: String, - timeline: String?, - eventId: String, - sessionId: String, - senderKey: String - ): OlmDecryptionResult { - val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) - val wrapper = sessionHolder.wrapper - val inboundGroupSession = wrapper.session - if (roomId != wrapper.roomId) { - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) - } - val decryptResult = try { - sessionHolder.mutex.withLock { - inboundGroupSession.decryptMessage(body) - } - } catch (e: OlmException) { - Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") - throw MXCryptoError.OlmError(e) - } - - val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex - Timber.tag(loggerTag.value).v("##########################################################") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId") - Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}") - - if (timeline?.isNotBlank() == true) { - val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() } - if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) { - val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) - } - replayAttackMap[messageIndexKey] = eventId - } - val payload = try { - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) - adapter.fromJson(payloadString) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - return OlmDecryptionResult( - payload, - wrapper.sessionData.keysClaimed, - senderKey, - wrapper.sessionData.forwardingCurve25519KeyChain, - isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse() - ) - } - - /** - * Reset replay attack data for the given timeline. - * - * @param timeline the id of the timeline. - */ - fun resetReplayAttackCheckInTimeline(timeline: String?) { - if (null != timeline) { - inboundGroupSessionMessageIndexes.remove(timeline) - } - } - -// Utilities - - /** - * Verify an ed25519 signature on a JSON object. - * - * @param key the ed25519 key. - * @param jsonDictionary the JSON object which was signed. - * @param signature the base64-encoded signature to be checked. - * @throws Exception the exception - */ - @Throws(Exception::class) - fun verifySignature(key: String, jsonDictionary: Map, signature: String) { - // Check signature on the canonical version of the JSON - olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) - } - - /** - * Calculate the SHA-256 hash of the input and encodes it as base64. - * - * @param message the message to hash. - * @return the base64-encoded hash value. - */ - fun sha256(message: String): String { - return olmUtility!!.sha256(convertToUTF8(message)) - } - - /** - * Search an OlmSession. - * - * @param theirDeviceIdentityKey the device key - * @param sessionId the session Id - * @return the olm session - */ - private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { - // sanity check - return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { - olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) - } - } - - /** - * Extract an InboundGroupSession from the session store and do some check. - * inboundGroupSessionWithIdError describes the failure reason. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param roomId the room where the session is used. - * @return the inbound group session. - */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { - if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) - } - - val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) - val session = holder?.wrapper - - if (session != null) { - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId != session.roomId) { - val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) - Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) - } else { - return holder - } - } else { - Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) - } - } - - /** - * Determine if we have the keys for a given megolm session. - * - * @param roomId room in which the message was received - * @param senderKey base64-encoded curve25519 key of the sender - * @param sessionId session identifier - * @return true if the unbound session keys are known. - */ - fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { - return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess - } - - @VisibleForTesting - fun clearOlmSessionCache() { - olmSessionStore.clear() - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionImportManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionImportManager.kt new file mode 100644 index 000000000..b73bb96a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionImportManager.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +/** + * Helper that allows listeners to be notified when a new megolm session + * has been added to the crypto layer (could be via room keys or forward keys via sync + * or after importing keys from key backup or manual import). + * Can be used to refresh display when the keys are received after the message + */ +@SessionScope +internal class MegolmSessionImportManager @Inject constructor( + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope +) { + + private val newSessionsListeners = mutableListOf() + + fun addListener(listener: NewSessionListener) { + synchronized(newSessionsListeners) { + if (!newSessionsListeners.contains(listener)) { + newSessionsListeners.add(listener) + } + } + } + + fun removeListener(listener: NewSessionListener) { + synchronized(newSessionsListeners) { + newSessionsListeners.remove(listener) + } + } + + fun dispatchNewSession(roomId: String?, sessionId: String) { + val copy = synchronized(newSessionsListeners) { + newSessionsListeners.toList() + } + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { + copy.forEach { + tryOrNull("Failed to dispatch new session import") { + it.onNewSession(roomId, sessionId) + } + } + } + } + + fun dispatchKeyImportResults(result: ImportRoomKeysResult) { + result.importedSessionInfo.forEach { (roomId, senderToSessionIdMap) -> + senderToSessionIdMap.values.forEach { sessionList -> + sessionList.forEach { sessionId -> + dispatchNewSession(roomId, sessionId) + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt deleted file mode 100644 index 3d09c0469..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import javax.inject.Inject - -@SessionScope -internal class MyDeviceInfoHolder @Inject constructor( - // The credentials, - credentials: Credentials, - // the crypto store - cryptoStore: IMXCryptoStore, - // Olm device - olmDevice: MXOlmDevice -) { - // Our device keys - /** - * my device info. - */ - val myDevice: CryptoDeviceInfo - - init { - - val keys = HashMap() - -// TODO it's a bit strange, why not load from DB? - if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { - keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! - } - - if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) { - keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! - } - -// myDevice.keys = keys -// -// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() - - // TODO hwo to really check cross signed status? - // - val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false -// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true) - - myDevice = CryptoDeviceInfo( - credentials.deviceId!!, - credentials.userId, - keys = keys, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - trustLevel = DeviceTrustLevel(crossSigned, true) - ) - - // Add our own deviceinfo to the store - val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) - - val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap() - - myDevices[myDevice.deviceId] = myDevice - - cryptoStore.storeUserDevices(credentials.userId, myDevices) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt deleted file mode 100644 index 3f4b633ea..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ObjectSigner.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.auth.data.Credentials -import javax.inject.Inject - -internal class ObjectSigner @Inject constructor( - private val credentials: Credentials, - private val olmDevice: MXOlmDevice -) { - - /** - * Sign Object. - * - * Example: - *
-     *     {
-     *         "[MY_USER_ID]": {
-     *             "ed25519:[MY_DEVICE_ID]": "sign(str)"
-     *         }
-     *     }
-     * 
- * - * @param strToSign the String to sign and to include in the Map - * @return a Map (see example) - */ - fun signObject(strToSign: String): Map> { - val result = HashMap>() - - val content = HashMap() - - content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign) - ?: "" // null reported by rageshake if happens during logout - - result[credentials.userId] = content - - return result - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt new file mode 100644 index 000000000..f90ae4a34 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -0,0 +1,957 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import com.squareup.moshi.Moshi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState +import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse +import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.coroutines.builder.safeInvokeOnClose +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.DefaultKeysAlgorithmAndData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.SasVerification +import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest +import org.matrix.android.sdk.internal.crypto.verification.VerificationsProvider +import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.SessionRustFilesDirectory +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.rustcomponents.sdk.crypto.BackupKeys +import org.matrix.rustcomponents.sdk.crypto.BackupRecoveryKey +import org.matrix.rustcomponents.sdk.crypto.CrossSigningKeyExport +import org.matrix.rustcomponents.sdk.crypto.CrossSigningStatus +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import org.matrix.rustcomponents.sdk.crypto.DecryptionException +import org.matrix.rustcomponents.sdk.crypto.DeviceLists +import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings +import org.matrix.rustcomponents.sdk.crypto.KeyRequestPair +import org.matrix.rustcomponents.sdk.crypto.KeysImportResult +import org.matrix.rustcomponents.sdk.crypto.LocalTrust +import org.matrix.rustcomponents.sdk.crypto.Logger +import org.matrix.rustcomponents.sdk.crypto.MegolmV1BackupKey +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.RequestType +import org.matrix.rustcomponents.sdk.crypto.RoomKeyCounts +import org.matrix.rustcomponents.sdk.crypto.ShieldColor +import org.matrix.rustcomponents.sdk.crypto.ShieldState +import org.matrix.rustcomponents.sdk.crypto.SignatureVerification +import org.matrix.rustcomponents.sdk.crypto.setLogger +import timber.log.Timber +import java.io.File +import java.nio.charset.Charset +import javax.inject.Inject +import org.matrix.rustcomponents.sdk.crypto.OlmMachine as InnerMachine +import org.matrix.rustcomponents.sdk.crypto.ProgressListener as RustProgressListener + +class CryptoLogger : Logger { + override fun log(logLine: String) { + Timber.d(logLine) + } +} + +private class CryptoProgressListener(private val listener: ProgressListener?) : RustProgressListener { + override fun onProgress(progress: Int, total: Int) { + listener?.onProgress(progress, total) + } +} + +fun setRustLogger() { + setLogger(CryptoLogger() as Logger) +} + +@SessionScope +internal class OlmMachine @Inject constructor( + @UserId userId: String, + @DeviceId deviceId: String, + @SessionRustFilesDirectory path: File, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + baseMoshi: Moshi, + private val verificationsProvider: VerificationsProvider, + private val deviceFactory: Device.Factory, + private val getUserIdentity: GetUserIdentityUseCase, + private val ensureUsersKeys: EnsureUsersKeysUseCase, + private val matrixConfiguration: MatrixConfiguration, + private val megolmSessionImportManager: MegolmSessionImportManager, + rustEncryptionConfiguration: RustEncryptionConfiguration, +) { + + private val inner: InnerMachine + + init { + inner = InnerMachine(userId, deviceId, path.toString(), rustEncryptionConfiguration.getDatabasePassphrase()) + } + + private val flowCollectors = FlowCollectors() + + private val moshi = baseMoshi.newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + + /** Get our own user ID. */ + fun userId(): String { + return inner.userId() + } + + /** Get our own device ID. */ + fun deviceId(): String { + return inner.deviceId() + } + + /** Get our own public identity keys ID. */ + fun identityKeys(): Map { + return inner.identityKeys() + } + + fun inner(): InnerMachine { + return inner + } + + private suspend fun updateLiveDevices() { + flowCollectors.forEachDevicesCollector { + val devices = getCryptoDeviceInfo(it.userIds) + it.trySend(devices) + } + } + + private suspend fun updateLiveUserIdentities() { + flowCollectors.forEachIdentityCollector { + val identity = getIdentity(it.userId)?.toMxCrossSigningInfo().toOptional() + it.trySend(identity) + } + } + + private suspend fun updateLivePrivateKeys() { + val keys = exportCrossSigningKeys().toOptional() + flowCollectors.forEachPrivateKeysCollector { + it.trySend(keys) + } + } + + /** + * Get our own device info as [CryptoDeviceInfo]. + */ + suspend fun ownDevice(): CryptoDeviceInfo { + val deviceId = deviceId() + + val keys = identityKeys().map { (keyId, key) -> "$keyId:$deviceId" to key }.toMap() + + val crossSigningVerified = when (val ownIdentity = getIdentity(userId())) { + is OwnUserIdentity -> ownIdentity.trustsOurOwnDevice() + else -> false + } + + return CryptoDeviceInfo( + deviceId(), + userId(), + // TODO pass the algorithms here. + listOf(), + keys, + mapOf(), + UnsignedDeviceInfo(), + DeviceTrustLevel(crossSigningVerified, locallyVerified = true), + false, + null + ) + } + + /** + * Get the list of outgoing requests that need to be sent to the homeserver. + * + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the [markRequestAsSent] method. + * + * @return the list of requests that needs to be sent to the homeserver + */ + suspend fun outgoingRequests(): List = + withContext(coroutineDispatchers.io) { inner.outgoingRequests() } + + /** + * Mark a request that was sent to the server as sent. + * + * @param requestId The unique ID of the request that was sent out. This needs to be an UUID. + * + * @param requestType The type of the request that was sent out. + * + * @param responseBody The body of the response that was received. + */ + @Throws(CryptoStoreException::class) + suspend fun markRequestAsSent( + requestId: String, + requestType: RequestType, + responseBody: String + ) = + withContext(coroutineDispatchers.io) { + inner.markRequestAsSent(requestId, requestType, responseBody) + if (requestType == RequestType.KEYS_QUERY) { + updateLiveDevices() + updateLiveUserIdentities() + } + } + + /** + * Let the state machine know about E2EE related sync changes that we received from the server. + * + * This needs to be called after every sync, ideally before processing any other sync changes. + * + * @param toDevice A serialized array of to-device events we received in the current sync + * response. + * + * @param deviceChanges The list of devices that have changed in some way since the previous + * sync. + * + * @param keyCounts The map of uploaded one-time key types and counts. + * + * @param deviceUnusedFallbackKeyTypes The key algorithms for which the server has an unused fallback key for the device. + * + * @param nextBatch The batch token to pass in the next sync request. + * + * @return The handled events, decrypted if needed (secrets are zeroised). + */ + @Throws(CryptoStoreException::class) + suspend fun receiveSyncChanges( + toDevice: ToDeviceSyncResponse?, + deviceChanges: DeviceListResponse?, + keyCounts: DeviceOneTimeKeysCountSyncResponse?, + deviceUnusedFallbackKeyTypes: List?, + nextBatch: String? + ): ToDeviceSyncResponse { + val response = withContext(coroutineDispatchers.io) { + val counts: MutableMap = mutableMapOf() + + if (keyCounts?.signedCurve25519 != null) { + counts["signed_curve25519"] = keyCounts.signedCurve25519 + } + + val devices = + DeviceLists(deviceChanges?.changed.orEmpty(), deviceChanges?.left.orEmpty()) + + val adapter = MoshiProvider.providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + .adapter(ToDeviceSyncResponse::class.java) + val events = adapter.toJson(toDevice ?: ToDeviceSyncResponse()) + + // field pass in the list of unused fallback keys here + val receiveSyncChanges = inner.receiveSyncChanges(events, devices, counts, deviceUnusedFallbackKeyTypes, nextBatch ?: "") + + val outAdapter = moshi.adapter(Event::class.java) + + // we don't need to use `roomKeyInfos` as for now we are manually + // checking the returned to devices to check for room keys. + // XXX Anyhow there is now proper signaling we should soon stop parsing them manually + receiveSyncChanges.toDeviceEvents.map { + outAdapter.fromJson(it) ?: Event() + } + } + + // We may get cross signing keys over a to-device event, update our listeners. + updateLivePrivateKeys() + + return ToDeviceSyncResponse(events = response) + } +// +// suspend fun receiveUnencryptedVerificationEvent(roomId: String, event: Event) = withContext(coroutineDispatchers.io) { +// val adapter = moshi +// .adapter(Event::class.java) +// val serializedEvent = adapter.toJson(event) +// inner.receiveUnencryptedVerificationEvent(serializedEvent, roomId) +// } + + suspend fun receiveVerificationEvent(roomId: String, event: Event) = withContext(coroutineDispatchers.io) { + val adapter = moshi + .adapter(Event::class.java) + val serializedEvent = adapter.toJson(event) + inner.receiveVerificationEvent(serializedEvent, roomId) + } + + /** + * Used for lazy migration of inboundGroupSession from EA to ER. + */ + suspend fun importRoomKey(inbound: MXInboundMegolmSessionWrapper): Result { + Timber.v("Migration:: Tentative lazy migration") + return withContext(coroutineDispatchers.io) { + val export = inbound.exportKeys() + ?: return@withContext Result.failure(Exception("Failed to export key")) + val result = importDecryptedKeys(listOf(export), null).also { + Timber.v("Migration:: Tentative lazy migration result: ${it.totalNumberOfKeys}") + } + if (result.totalNumberOfKeys == 1) return@withContext Result.success(Unit) + return@withContext Result.failure(Exception("Import failed")) + } + } + + /** + * Mark the given list of users to be tracked, triggering a key query request for them. + * + * *Note*: Only users that aren't already tracked will be considered for an update. It's safe to + * call this with already tracked users, it won't result in excessive keys query requests. + * + * @param users The users that should be queued up for a key query. + */ + suspend fun updateTrackedUsers(users: List) = + withContext(coroutineDispatchers.io) { inner.updateTrackedUsers(users) } + + /** + * Check if the given user is considered to be tracked. + * A user can be marked for tracking using the + * [OlmMachine.updateTrackedUsers] method. + */ + @Throws(CryptoStoreException::class) + fun isUserTracked(userId: String): Boolean { + return inner.isUserTracked(userId) + } + + /** + * Generate one-time key claiming requests for all the users we are missing sessions for. + * + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the [markRequestAsSent] method. + * + * This method should be called every time before a call to [shareRoomKey] is made. + * + * @param users The list of users for which we would like to establish 1:1 Olm sessions for. + * + * @return A [Request.KeysClaim] request that needs to be sent out to the server. + */ + @Throws(CryptoStoreException::class) + suspend fun getMissingSessions(users: List): Request? = + withContext(coroutineDispatchers.io) { inner.getMissingSessions(users) } + + /** + * Share a room key with the given list of users for the given room. + * + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the markRequestAsSent() method. + * + * This method should be called every time before a call to `encrypt()` with the given `room_id` + * is made. + * + * @param roomId The unique id of the room, note that this doesn't strictly need to be a Matrix + * room, it just needs to be an unique identifier for the group that will participate in the + * conversation. + * + * @param users The list of users which are considered to be members of the room and should + * receive the room key. + * + * @param settings The encryption settings for that room. + * + * @return The list of [Request.ToDevice] that need to be sent out. + */ + @Throws(CryptoStoreException::class) + suspend fun shareRoomKey(roomId: String, users: List, settings: EncryptionSettings): List = + withContext(coroutineDispatchers.io) { + inner.shareRoomKey(roomId, users, settings) + } + + /** + * Encrypt the given event with the given type and content for the given room. + * + * **Note**: A room key needs to be shared with the group of users that are members + * in the given room. If this is not done this method will panic. + * + * The usual flow to encrypt an event using this state machine is as follows: + * + * 1. Get the one-time key claim request to establish 1:1 Olm sessions for + * the room members of the room we wish to participate in. This is done + * using the [getMissingSessions] method. This method call should be locked per call. + * + * 2. Share a room key with all the room members using the [shareRoomKey]. + * This method call should be locked per room. + * + * 3. Encrypt the event using this method. + * + * 4. Send the encrypted event to the server. + * + * After the room key is shared steps 1 and 2 will become no-ops, unless there's some changes in + * the room membership or in the list of devices a member has. + * + * @param roomId the ID of the room where the encrypted event will be sent to + * + * @param eventType the type of the event + * + * @param content the JSON content of the event + * + * @return The encrypted version of the [Content] + */ + @Throws(CryptoStoreException::class) + suspend fun encrypt(roomId: String, eventType: String, content: Content): Content = + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Map::class.java) + val contentString = adapter.toJson(content) + val encrypted = inner.encrypt(roomId, eventType, contentString) + adapter.fromJson(encrypted)!! + } + + /** + * Decrypt the given event that was sent in the given room. + * + * # Arguments + * + * @param event The serialized encrypted version of the event. + * + * @return the decrypted version of the event as a [MXEventDecryptionResult]. + */ + @Throws(MXCryptoError::class) + suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult = + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Event::class.java) + try { + if (event.roomId.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + } + if (event.isRedacted()) { + // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm + // Workaround until https://github.com/matrix-org/matrix-rust-sdk/issues/1642 + return@withContext MXEventDecryptionResult( + clearEvent = mapOf( + "room_id" to event.roomId, + "type" to EventType.MESSAGE, + "content" to emptyMap(), + "unsigned" to event.unsignedData.toContent() + ) + ) + } + + val serializedEvent = adapter.toJson(event) + val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId, false, false) + + val deserializationAdapter = + moshi.adapter(Map::class.java) + val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent) + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) + + MXEventDecryptionResult( + clearEvent = clearEvent, + senderCurve25519Key = decrypted.senderCurve25519Key, + claimedEd25519Key = decrypted.claimedEd25519Key, + forwardingCurve25519KeyChain = decrypted.forwardingCurve25519Chain, + messageVerificationState = decrypted.shieldState.toVerificationState(), + ) + } catch (throwable: Throwable) { + val reThrow = when (throwable) { + is DecryptionException.MissingRoomKey -> { + if (throwable.withheldCode != null) { + MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD, throwable.withheldCode!!) + } else { + MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, throwable.error) + } + } + is DecryptionException.Megolm -> { + // TODO check if it's the correct binding? + // Could encapsulate more than that, need to update sdk + MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, throwable.error) + } + is DecryptionException.Identifier -> { + MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) + } + else -> { + val reason = String.format( + MXCryptoError.UNABLE_TO_DECRYPT_REASON, + throwable.message, + "m.megolm.v1.aes-sha2" + ) + MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } + } + matrixConfiguration.cryptoAnalyticsPlugin?.onFailedToDecryptRoomMessage( + reThrow, + (event.content?.get("session_id") as? String) ?: "" + ) + throw reThrow + } + } + + private fun ShieldState.toVerificationState(): MessageVerificationState? { + return when (this.color) { + ShieldColor.NONE -> MessageVerificationState.VERIFIED + ShieldColor.RED -> { + when (this.message) { + "Encrypted by an unverified device." -> MessageVerificationState.UN_SIGNED_DEVICE + "Encrypted by a device not verified by its owner." -> MessageVerificationState.UN_SIGNED_DEVICE + "Encrypted by an unknown or deleted device." -> MessageVerificationState.UNKNOWN_DEVICE + else -> MessageVerificationState.UN_SIGNED_DEVICE + } + } + ShieldColor.GREY -> { + MessageVerificationState.UNSAFE_SOURCE + } + } + } + + /** + * Request the room key that was used to encrypt the given undecrypted event. + * + * @param event The that we're not able to decrypt and want to request a room key for. + * + * @return a key request pair, consisting of an optional key request cancellation and the key + * request itself. The cancellation *must* be sent out before the request, otherwise devices + * will ignore the key request. + */ + @Throws(DecryptionException::class) + suspend fun requestRoomKey(event: Event): KeyRequestPair = + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(Event::class.java) + val serializedEvent = adapter.toJson(event) + + inner.requestRoomKey(serializedEvent, event.roomId!!) + } + + /** + * Export all of our room keys. + * + * @param passphrase The passphrase that should be used to encrypt the key export. + * + * @param rounds The number of rounds that should be used when expanding the passphrase into an + * key. + * + * @return the encrypted key export as a bytearray. + */ + @Throws(CryptoStoreException::class) + suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray = + withContext(coroutineDispatchers.io) { + inner.exportRoomKeys(passphrase, rounds).toByteArray() + } + + private fun KeysImportResult.fromOlm(): ImportRoomKeysResult { + return ImportRoomKeysResult( + this.total.toInt(), + this.imported.toInt(), + this.keys + ) + } + + /** + * Import room keys from the given serialized key export. + * + * @param keys The serialized version of the key export. + * + * @param passphrase The passphrase that was used to encrypt the key export. + * + * @param listener A callback that can be used to introspect the progress of the key import. + */ + @Throws(CryptoStoreException::class) + suspend fun importKeys( + keys: ByteArray, + passphrase: String, + listener: ProgressListener? + ): ImportRoomKeysResult = + withContext(coroutineDispatchers.io) { + val decodedKeys = String(keys, Charset.defaultCharset()) + + val rustListener = CryptoProgressListener(listener) + + val result = inner.importRoomKeys(decodedKeys, passphrase, rustListener) + + result.fromOlm() + } + + @Throws(CryptoStoreException::class) + suspend fun importDecryptedKeys( + keys: List, + listener: ProgressListener? + ): ImportRoomKeysResult = + withContext(coroutineDispatchers.io) { + val adapter = moshi.adapter(List::class.java) + + // If the key backup is too big we take the risk of causing OOM + // when serializing to json + // so let's chunk to avoid it + var totalImported = 0L + var accTotal = 0L + val details = mutableMapOf>>() + keys.chunked(500) + .forEach { keysSlice -> + val encodedKeys = adapter.toJson(keysSlice) + val rustListener = object : RustProgressListener { + override fun onProgress(progress: Int, total: Int) { + val accProgress = (accTotal + progress).toInt() + listener?.onProgress(accProgress, keys.size) + } + } + + inner.importDecryptedRoomKeys(encodedKeys, rustListener).let { + totalImported += it.imported + accTotal += it.total + details.putAll(it.keys) + } + } + ImportRoomKeysResult(totalImported.toInt(), accTotal.toInt(), details).also { + megolmSessionImportManager.dispatchKeyImportResults(it) + } + } + + @Throws(CryptoStoreException::class) + suspend fun getIdentity(userId: String): UserIdentities? = getUserIdentity(userId) + + /** + * Get a `Device` from the store. + * + * This method returns our own device as well. + * + * @param userId The id of the device owner. + * + * @param deviceId The id of the device itself. + * + * @return The Device if it found one. + */ + @Throws(CryptoStoreException::class) + suspend fun getCryptoDeviceInfo(userId: String, deviceId: String): CryptoDeviceInfo? { + return getDevice(userId, deviceId)?.toCryptoDeviceInfo() + } + + @Throws(CryptoStoreException::class) + suspend fun getDevice(userId: String, deviceId: String): Device? { + val innerDevice = withContext(coroutineDispatchers.io) { + inner.getDevice(userId, deviceId, 30u) + } ?: return null + return deviceFactory.create(innerDevice) + } + + suspend fun getUserDevices(userId: String): List { + return withContext(coroutineDispatchers.io) { + inner.getUserDevices(userId, 30u).map(deviceFactory::create) + } + } + + /** + * Get all devices of an user. + * + * @param userId The id of the device owner. + * + * @return The list of Devices or an empty list if there aren't any. + */ + @Throws(CryptoStoreException::class) + suspend fun getCryptoDeviceInfo(userId: String): List { + return getUserDevices(userId).map { it.toCryptoDeviceInfo() } + } + + /** + * Get all the devices of multiple users. + * + * @param userIds The ids of the device owners. + * + * @return The list of Devices or an empty list if there aren't any. + */ + private suspend fun getCryptoDeviceInfo(userIds: List): List { + val plainDevices: ArrayList = arrayListOf() + + for (user in userIds) { + val devices = getCryptoDeviceInfo(user) + plainDevices.addAll(devices) + } + + return plainDevices + } + + private suspend fun getUserDevicesMap(userIds: List): MXUsersDevicesMap { + val userMap = MXUsersDevicesMap() + + for (user in userIds) { + val devices = getCryptoDeviceInfo(user) + + for (device in devices) { + userMap.setObject(user, device.deviceId, device) + } + } + + return userMap + } + + /** + * If the user is untracked or forceDownload is set to true, a key query request will be made. + * It will suspend until query response, and the device list will be returned. + * + * The key query request will be retried a few time in case of shaky connection, but could fail. + */ + suspend fun ensureUserDevicesMap(userIds: List, forceDownload: Boolean = false): MXUsersDevicesMap { + ensureUsersKeys(userIds, forceDownload) + return getUserDevicesMap(userIds) + } + + /** + * If the user is untracked or forceDownload is set to true, a key query request will be made. + * It will suspend until query response. + * + * The key query request will be retried a few time in case of shaky connection, but could fail. + */ + suspend fun ensureUsersKeys(userIds: List, forceDownload: Boolean = false) { + ensureUsersKeys.invoke(userIds, forceDownload) + } + + private fun getUserIdentityFlow(userId: String): Flow> { + return channelFlow { + val userIdentityCollector = UserIdentityCollector(userId, this) + val onClose = safeInvokeOnClose { + flowCollectors.removeIdentityCollector(userIdentityCollector) + } + flowCollectors.addIdentityCollector(userIdentityCollector) + val identity = getIdentity(userId)?.toMxCrossSigningInfo().toOptional() + send(identity) + onClose.await() + } + } + + fun getLiveUserIdentity(userId: String): LiveData> { + return getUserIdentityFlow(userId).asLiveData(coroutineDispatchers.io) + } + + fun getLivePrivateCrossSigningKeys(): LiveData> { + return getPrivateCrossSigningKeysFlow().asLiveData(coroutineDispatchers.io) + } + + fun getPrivateCrossSigningKeysFlow(): Flow> { + return channelFlow { + val onClose = safeInvokeOnClose { + flowCollectors.removePrivateKeysCollector(this) + } + flowCollectors.addPrivateKeysCollector(this) + val keys = this@OlmMachine.exportCrossSigningKeys().toOptional() + send(keys) + onClose.await() + } + } + + /** + * Get all the devices of multiple users as a live version. + * + * The live version will update the list of devices if some of the data changes, or if new + * devices arrive for a certain user. + * + * @param userIds The ids of the device owners. + * + * @return The list of Devices or an empty list if there aren't any as a Flow. + */ + fun getLiveDevices(userIds: List): LiveData> { + return getDevicesFlow(userIds).asLiveData(coroutineDispatchers.io) + } + + fun getDevicesFlow(userIds: List): Flow> { + return channelFlow { + val devicesCollector = DevicesCollector(userIds, this) + val onClose = safeInvokeOnClose { + flowCollectors.removeDevicesCollector(devicesCollector) + } + flowCollectors.addDevicesCollector(devicesCollector) + val devices = getCryptoDeviceInfo(userIds) + send(devices) + onClose.await() + } + } + + /** Discard the currently active room key for the given room if there is one. */ + @Throws(CryptoStoreException::class) + fun discardRoomKey(roomId: String) { + runBlocking { inner.discardRoomKey(roomId) } + } + + /** + * Get all the verification requests we have with the given user. + * + * @param userId The ID of the user for which we would like to fetch the + * verification requests + * + * @return The list of [VerificationRequest] that we share with the given user + */ + fun getVerificationRequests(userId: String): List { + return verificationsProvider.getVerificationRequests(userId) + } + + /** Get a verification request for the given user with the given flow ID. */ + fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { + return verificationsProvider.getVerificationRequest(userId, flowId) + } + + /** Get an active verification for the given user and given flow ID. + * + * @return Either a [SasVerification] verification or a [QrCodeVerification] + * verification. + */ + fun getVerification(userId: String, flowId: String): VerificationTransaction? { + return verificationsProvider.getVerification(userId, flowId) + } + + suspend fun bootstrapCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { + val requests = withContext(coroutineDispatchers.io) { + inner.bootstrapCrossSigning() + } + requestSender.uploadCrossSigningKeys(requests.uploadSigningKeysRequest, uiaInterceptor) + requestSender.sendSignatureUpload(requests.signatureRequest) + } + + /** + * Get the status of our private cross signing keys, i.e. which private keys do we have stored locally. + */ + fun crossSigningStatus(): CrossSigningStatus { + return inner.crossSigningStatus() + } + + suspend fun exportCrossSigningKeys(): PrivateKeysInfo? { + val export = withContext(coroutineDispatchers.io) { + inner.exportCrossSigningKeys() + } ?: return null + + return PrivateKeysInfo(export.masterKey, export.selfSigningKey, export.userSigningKey) + } + + suspend fun importCrossSigningKeys(export: PrivateKeysInfo): UserTrustResult { + val rustExport = CrossSigningKeyExport(export.master, export.selfSigned, export.user) + + var result: UserTrustResult + withContext(coroutineDispatchers.io) { + result = try { + inner.importCrossSigningKeys(rustExport) + + // Sign the cross signing keys with our device + // Fail silently if signature upload fails?? + try { + getIdentity(userId())?.verify() + } catch (failure: Throwable) { + Timber.e(failure, "Failed to sign x-keys with own device") + } + UserTrustResult.Success + } catch (failure: Exception) { + // KeyImportError? + UserTrustResult.Failure(failure.localizedMessage ?: "Unknown Error") + } + } + withContext(coroutineDispatchers.main) { + this@OlmMachine.updateLivePrivateKeys() + } + return result + } + + suspend fun sign(message: String): Map> { + return withContext(coroutineDispatchers.computation) { + inner.sign(message) + } + } + + suspend fun requestMissingSecretsFromOtherSessions(): Boolean { + return withContext(coroutineDispatchers.io) { + inner.queryMissingSecretsFromOtherSessions() + } + } + @Throws(CryptoStoreException::class) + suspend fun enableBackupV1(key: String, version: String) { + return withContext(coroutineDispatchers.computation) { + val backupKey = MegolmV1BackupKey(key, mapOf(), null, MXCRYPTO_ALGORITHM_MEGOLM_BACKUP) + inner.enableBackupV1(backupKey, version) + } + } + + @Throws(CryptoStoreException::class) + fun disableBackup() { + inner.disableBackup() + } + + fun backupEnabled(): Boolean { + return inner.backupEnabled() + } + + @Throws(CryptoStoreException::class) + suspend fun roomKeyCounts(): RoomKeyCounts { + return withContext(coroutineDispatchers.computation) { + inner.roomKeyCounts() + } + } + + @Throws(CryptoStoreException::class) + suspend fun getBackupKeys(): BackupKeys? { + return withContext(coroutineDispatchers.computation) { + inner.getBackupKeys() + } + } + + @Throws(CryptoStoreException::class) + suspend fun saveRecoveryKey(key: BackupRecoveryKey?, version: String?) { + withContext(coroutineDispatchers.computation) { + inner.saveRecoveryKey(key, version) + } + } + + @Throws(CryptoStoreException::class) + suspend fun backupRoomKeys(): Request? { + return withContext(coroutineDispatchers.computation) { + Timber.d("BACKUP CREATING REQUEST") + val request = inner.backupRoomKeys() + Timber.d("BACKUP CREATED REQUEST: $request") + request + } + } + + @Throws(CryptoStoreException::class) + suspend fun checkAuthDataSignature(authData: KeysAlgorithmAndData): SignatureVerification { + return withContext(coroutineDispatchers.computation) { + val adapter = moshi + .newBuilder() + .build() + .adapter(DefaultKeysAlgorithmAndData::class.java) + val serializedAuthData = adapter.toJson( + DefaultKeysAlgorithmAndData( + algorithm = authData.algorithm, + authData = authData.authData + ) + ) + + inner.verifyBackup(serializedAuthData) + } + } + + @Throws(CryptoStoreException::class) + suspend fun setDeviceLocalTrust(userId: String, deviceId: String, trusted: Boolean) { + withContext(coroutineDispatchers.io) { + inner.setLocalTrust(userId, deviceId, if (trusted) LocalTrust.VERIFIED else LocalTrust.UNSET) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt deleted file mode 100644 index 4401a0719..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.olm.OlmSession -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO) - -/** - * Keep the used olm session in memory and load them from the data layer when needed. - * Access is synchronized for thread safety. - */ -internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) { - /** - * Map of device key to list of olm sessions (it is possible to have several active sessions with a device). - */ - private val olmSessions = HashMap>() - - /** - * Store a session between our own device and another device. - * This will be called after the session has been created but also every time it has been used - * in order to persist the correct state for next run - * @param olmSessionWrapper the end-to-end session. - * @param deviceKey the public key of the other device. - */ - @Synchronized - fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { - // This could be a newly created session or one that was just created - // Anyhow we should persist ratchet state for future app lifecycle - addNewSessionInCache(olmSessionWrapper, deviceKey) - store.storeSession(olmSessionWrapper, deviceKey) - } - - /** - * Get all the Olm Sessions we are sharing with the given device. - * - * @param deviceKey the public key of the other device. - * @return A set of sessionId, or empty if device is not known - */ - @Synchronized - fun getDeviceSessionIds(deviceKey: String): List { - // we need to get the persisted ids first - val persistedKnownSessions = store.getDeviceSessionIds(deviceKey) - .orEmpty() - .toMutableList() - // Do we have some in cache not yet persisted? - olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached -> - getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId -> - if (!persistedKnownSessions.contains(cachedSessionId)) { - // as it's in cache put in on top - persistedKnownSessions.add(0, cachedSessionId) - } - } - } - return persistedKnownSessions - } - - /** - * Retrieve an end-to-end session between our own device and another - * device. - * - * @param sessionId the session Id. - * @param deviceKey the public key of the other device. - * @return the session wrapper if found - */ - @Synchronized - fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - // get from cache or load and add to cache - return internalGetSession(sessionId, deviceKey) - } - - /** - * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist. - * - * @param deviceKey the public key of the other device. - * @return last used sessionId, or null if not found - */ - @Synchronized - fun getLastUsedSessionId(deviceKey: String): String? { - // We want to avoid to load in memory old session if possible - val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey) - var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) } - // we should check if we have one in cache with a higher last message received? - olmSessions[deviceKey].orEmpty().forEach { inCache -> - if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) { - candidate = inCache - } - } - - return candidate?.olmSession?.sessionIdentifier() - } - - /** - * Release all sessions and clear cache. - */ - @Synchronized - fun clear() { - olmSessions.entries.onEach { entry -> - entry.value.onEach { it.olmSession.releaseSession() } - } - olmSessions.clear() - } - - private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - return getSessionInCache(sessionId, deviceKey) - ?: // deserialize from store - return store.getDeviceSession(sessionId, deviceKey)?.also { - addNewSessionInCache(it, deviceKey) - } - } - - private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? { - return olmSessions[deviceKey]?.firstOrNull { - getSafeSessionIdentifier(it.olmSession) == sessionId - } - } - - private fun getSafeSessionIdentifier(session: OlmSession): String? { - return try { - session.sessionIdentifier() - } catch (throwable: Throwable) { - Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session") - null - } - } - - private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) { - val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return - olmSessions.getOrPut(deviceKey) { mutableListOf() }.let { - val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId } - it.add(session) - // remove and release if was there but with different instance - if (existing != null && existing.olmSession != session.olmSession) { - // mm not sure when this could happen - // anyhow we should remove and release the one known - it.remove(existing) - existing.olmSession.releaseSession() - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt deleted file mode 100644 index 8143e3689..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import android.content.Context -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.internal.crypto.model.MXKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse -import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import timber.log.Timber -import javax.inject.Inject -import kotlin.math.floor -import kotlin.math.min - -// The spec recommend a 5mn delay, but due to federation -// or server downtime we give it a bit more time (1 hour) -private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L - -@SessionScope -internal class OneTimeKeysUploader @Inject constructor( - private val olmDevice: MXOlmDevice, - private val objectSigner: ObjectSigner, - private val uploadKeysTask: UploadKeysTask, - private val clock: Clock, - context: Context -) { - // tell if there is a OTK check in progress - private var oneTimeKeyCheckInProgress = false - - // last OTK check timestamp - private var lastOneTimeKeyCheck: Long = 0 - private var oneTimeKeyCount: Int? = null - - // Simple storage to remember when was uploaded the last fallback key - private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE) - - /** - * Stores the current one_time_key count which will be handled later (in a call of - * _onSyncCompleted). The count is e.g. coming from a /sync response. - * - * @param currentCount the new count - */ - fun updateOneTimeKeyCount(currentCount: Int) { - oneTimeKeyCount = currentCount - } - - fun needsNewFallback() { - if (olmDevice.generateFallbackKeyIfNeeded()) { - // As we generated a new one, it's already forgetting one - // so we can clear the last publish time - // (in case the network calls fails after to avoid calling forgetKey) - saveLastFallbackKeyPublishTime(0L) - } - } - - /** - * Check if the OTK must be uploaded. - */ - suspend fun maybeUploadOneTimeKeys() { - if (oneTimeKeyCheckInProgress) { - Timber.v("maybeUploadOneTimeKeys: already in progress") - return - } - if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { - // we've done a key upload recently. - Timber.v("maybeUploadOneTimeKeys: executed too recently") - return - } - - oneTimeKeyCheckInProgress = true - - val oneTimeKeyCountFromSync = oneTimeKeyCount - ?: fetchOtkCount() // we don't have count from sync so get from server - ?: return Unit.also { - oneTimeKeyCheckInProgress = false - Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server") - } - - Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}") - - lastOneTimeKeyCheck = clock.epochMillis() - - // We then check how many keys we can store in the Account object. - val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() - - // Try to keep at most half that number on the server. This leaves the - // rest of the slots free to hold keys that have been claimed from the - // server but we haven't received a message for. - // If we run out of slots when generating new keys then olm will - // discard the oldest private keys first. This will eventually clean - // out stale private keys that won't receive a message. - val keyLimit = floor(maxOneTimeKeys / 2.0).toInt() - - // We need to keep a pool of one time public keys on the server so that - // other devices can start conversations with us. But we can only store - // a finite number of private keys in the olm Account object. - // To complicate things further then can be a delay between a device - // claiming a public one time key from the server and it sending us a - // message. We need to keep the corresponding private key locally until - // we receive the message. - // But that message might never arrive leaving us stuck with duff - // private keys clogging up our local storage. - // So we need some kind of engineering compromise to balance all of - // these factors. - tryOrNull("Unable to upload OTK") { - val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) - Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") - } - oneTimeKeyCheckInProgress = false - - // Check if we need to forget a fallback key - val latestPublishedTime = getLastFallbackKeyPublishTime() - if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) { - // This should be called once you are reasonably certain that you will not receive any more messages - // that use the old fallback key - Timber.d("## forgetFallbackKey()") - olmDevice.forgetFallbackKey() - } - } - - private suspend fun fetchOtkCount(): Int? { - return tryOrNull("Unable to get OTK count") { - val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null, null)) - result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) - } - } - - /** - * Upload some the OTKs. - * - * @param keyCount the key count - * @param keyLimit the limit - * @return the number of uploaded keys - */ - private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { - if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) { - // If we don't need to generate any more keys then we are done. - return 0 - } - var keysThisLoop = 0 - if (keyLimit > keyCount) { - // Creating keys can be an expensive operation so we limit the - // number we generate in one go to avoid blocking the application - // for too long. - keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) - olmDevice.generateOneTimeKeys(keysThisLoop) - } - - // We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed - val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey() - val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) - olmDevice.markKeysAsPublished() - if (hadUnpublishedFallbackKey) { - // It had an unpublished fallback key that was published just now - saveLastFallbackKeyPublishTime(clock.epochMillis()) - } - - if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { - // Maybe upload other keys - return keysThisLoop + - uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + - (if (hadUnpublishedFallbackKey) 1 else 0) - } else { - Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") - throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") - } - } - - private fun saveLastFallbackKeyPublishTime(timeMillis: Long) { - storage.edit().putLong("last_fb_key_publish", timeMillis).apply() - } - - private fun getLastFallbackKeyPublishTime(): Long { - return storage.getLong("last_fb_key_publish", 0) - } - - /** - * Upload curve25519 one time keys. - */ - private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { - val oneTimeJson = mutableMapOf() - - val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() - - curve25519Map.forEach { (key_id, value) -> - val k = mutableMapOf() - k["key"] = value - - // the key is also signed - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) - - k["signatures"] = objectSigner.signObject(canonicalJson) - - oneTimeJson["signed_curve25519:$key_id"] = k - } - - val fallbackJson = mutableMapOf() - val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() - fallbackCurve25519Map.forEach { (key_id, key) -> - val k = mutableMapOf() - k["key"] = key - k["fallback"] = true - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) - k["signatures"] = objectSigner.signObject(canonicalJson) - - fallbackJson["signed_curve25519:$key_id"] = k - } - - // For now, we set the device id explicitly, as we may not be using the - // same one as used in login. - val uploadParams = UploadKeysTask.Params( - deviceKeys = null, - oneTimeKeys = oneTimeJson, - fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() } - ) - return uploadKeysTask.executeRetry(uploadParams, 3) - } - - companion object { - // max number of keys to upload at once - // Creating keys can be an expensive operation so we limit the - // number we generate in one go to avoid blocking the application - // for too long. - private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5 - - // frequency with which to check & upload one-time keys - private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt deleted file mode 100755 index 810699d93..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import timber.log.Timber -import java.util.Stack -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.system.measureTimeMillis - -private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) - -/** - * This class is responsible for sending key requests to other devices when a message failed to decrypt. - * It's lifecycle is based on the sync pulse: - * - You can post queries for session, or report when you got a session - * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices - * If a request failed it will be retried at the end of the next sync - */ -@SessionScope -internal class OutgoingKeyRequestManager @Inject constructor( - @SessionId private val sessionId: String, - @UserId private val myUserId: String, - private val cryptoStore: IMXCryptoStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoConfig: MXCryptoConfig, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val sendToDeviceTask: SendToDeviceTask, - private val deviceListManager: DeviceListManager, - private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - // We only have one active key request per session, so we don't request if it's already requested - // But it could make sense to check more the backup, as it's evolving. - // We keep a stack as we consider that the key requested last is more likely to be on screen? - private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() - - fun requestKeyForEvent(event: Event, force: Boolean) { - val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return - val index = ratchetIndexForMessage(event) ?: 0 - postRoomKeyRequest(body, targets, index, force) - } - - private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { - val sender = event.senderId ?: return null - val encryptedEventContent = event.content.toModel() ?: return null.also { - Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") - } - if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - - val senderDevice = encryptedEventContent.deviceId - val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { - mapOf( - myUserId to listOf("*") - ) - } else { - if (event.senderId == myUserId) { - mapOf( - myUserId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - myUserId to listOf("*"), - - // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 - // so in this case query to all - sender to listOf(senderDevice ?: "*") - ) - } - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - return recipients to requestBody - } - - private fun ratchetIndexForMessage(event: Event): Int? { - val encryptedContent = event.content.toModel() ?: return null - if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { - tryOrNull { - val megolmVersion = it.read() - if (megolmVersion != 3) return@tryOrNull null - /** Int tag */ - if (it.read() != 8) return@tryOrNull null - it.read() - } - } - } - - fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueRequest(requestBody, recipients, fromIndex, force) - } - } - } - - /** - * Typically called when we the session as been imported or received meanwhile. - */ - fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) - } - } - } - - fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { - if (newTrust) { - // we were previously not cross signed, but we are now - // so there is now more chances to get better replies for existing request - // Let's forget about sent request so that next time we try to decrypt we will resend requests - // We don't resend all because we don't want to generate a bulk of traffic - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) - } - - sequencer.post { - delay(1000) - perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) - } - } - } - } - - fun onRoomKeyForwarded( - sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - fromIndex: Int, - event: Event - ) { - Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - // strip out encrypted stuff as it's just a trail? - event = event.copy( - type = event.getClearType(), - content = mapOf( - "chain_index" to fromIndex - ) - ) - ) - } - } - } - - fun onRoomKeyWithHeld( - sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - event: Event - ) { - outgoingRequestScope.launch { - sequencer.post { - Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") - Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") - - // We want to store withheld code from the sender of the message (owner of the megolm session), not from - // other devices that might gossip the key. If not the initial reason might be overridden - // by a request to one of our session. - event.getClearContent().toModel()?.let { withheld -> - withContext(coroutineDispatchers.crypto) { - tryOrNull { - deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) - } - cryptoStore.getUserDeviceList(event.senderId ?: "") - .also { devices -> - Timber.tag(loggerTag.value) - .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") - } - ?.firstOrNull { - it.identityKey() == senderKey - } - }.also { - Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") - }?.let { - if (it.userId == event.senderId) { - if (fromDevice != null) { - if (it.deviceId == fromDevice) { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } else { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } - } - } - - // Here we store the replies from a given request - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - event = event - ) - } - } - } - - /** - * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those). - */ - fun requireProcessAllPendingKeyRequests() { - outgoingRequestScope.launch { - sequencer.post { - internalProcessPendingKeyRequests() - } - } - } - - private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { - // do we have known requests for that session?? - Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") - val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey - ) - if (knownRequest.isEmpty()) return Unit.also { - Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") - } - if (knownRequest.size > 1) { - // It's worth logging, there should be only one - Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") - } - knownRequest.forEach { request -> - when (request.state) { - OutgoingRoomKeyRequestState.UNSENT -> { - if (request.fromIndex >= localKnownChainIndex) { - // we have a good index we can cancel - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - } - } - OutgoingRoomKeyRequestState.SENT -> { - // It was already sent, and index satisfied we can cancel - if (request.fromIndex >= localKnownChainIndex) { - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // It is already marked to be cancelled - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - if (request.fromIndex >= localKnownChainIndex) { - // we just want to cancel now - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - // was already canceled - // if we need a better index, should we resend? - } - } - } - } - - fun close() { - try { - outgoingRequestScope.cancel("User Terminate") - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("Failed to shutDown request manager") - } - } - - private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { - if (!cryptoStore.isKeyGossipingEnabled()) { - // we might want to try backup? - if (requestBody.roomId != null && requestBody.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) - } - Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") - return - } - - Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") - val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) - Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") - when (existing?.state) { - null -> { - // create a new one - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - OutgoingRoomKeyRequestState.UNSENT -> { - // nothing it's new or not yet handled - } - OutgoingRoomKeyRequestState.SENT -> { - // it was already requested - Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") - if (force) { - // update to UNSENT - Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } else { - if (existing.roomId != null && existing.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) - } - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // request is canceled only if I got the keys so what to do here... - if (force) { - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - // It's already going to resend - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - if (force) { - cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - } - } - - if (existing != null && existing.fromIndex >= fromIndex) { - // update the required index - cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) - } - } - - private suspend fun internalProcessPendingKeyRequests() { - val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) - Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") - - measureTimeMillis { - toProcess.forEach { - when (it.state) { - OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, - OutgoingRoomKeyRequestState.SENT -> { - // these are filtered out - } - } - } - }.let { - Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") - } - - val maxBackupCallsBySync = 60 - var currentCalls = 0 - measureTimeMillis { - while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> - // we want to rate limit that somehow :/ - perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) - } - currentCalls++ - } - }.let { - Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") - } - } - - private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { - // In order to avoid generating to_device traffic, we can first check if the key is backed up - Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") - val sessionId = request.sessionId ?: return - val roomId = request.roomId ?: return - if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { - // let's see what's the index - val knownIndex = tryOrNull { - inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "") - ?.wrapper - ?.session - ?.firstKnownIndex - } - if (knownIndex != null && knownIndex <= request.fromIndex) { - // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request - Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - return - } - } - - // we need to send the request - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - body = request.requestBody - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") - // The request was sent, so update state - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") - } - } - - private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { - Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") - // we have to cancel this - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - return try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - // The request cancellation was sent, we don't delete yet because we want - // to keep trace of the sent replies - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") - false - } - } - - private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { - if (handleRequestToCancel(request)) { - // this will create a new unsent request with no replies that will be process in the following call - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt index 5f62e7be9..8321c6713 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt @@ -20,12 +20,9 @@ import dagger.Lazy import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.util.awaitCallback -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -40,8 +37,7 @@ private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.C */ internal class PerSessionBackupQueryRateLimiter @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val keysBackupService: Lazy, - private val cryptoStore: IMXCryptoStore, + private val keysBackupService: Lazy, private val clock: Clock, ) { @@ -70,11 +66,11 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor( var backupWasCheckedFromServer: Boolean = false val now = clock.epochMillis() - fun refreshBackupInfoIfNeeded(force: Boolean = false) { + suspend fun refreshBackupInfoIfNeeded(force: Boolean = false) { if (backupWasCheckedFromServer && !force) return Timber.tag(loggerTag.value).v("Checking if can access a backup") backupWasCheckedFromServer = true - val knownBackupSecret = cryptoStore.getKeyBackupRecoveryKeyInfo() + val knownBackupSecret = keysBackupService.get().getKeyBackupRecoveryKeyInfo() ?: return Unit.also { Timber.tag(loggerTag.value).v("We don't have the backup secret!") } @@ -101,19 +97,17 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor( (now - lastTry.timestamp) > MIN_TRY_BACKUP_PERIOD_MILLIS if (!shouldQuery) return false - + val recoveryKey = savedKeyBackupKeyInfo?.recoveryKey ?: return false val successfullyImported = withContext(coroutineDispatchers.io) { try { - awaitCallback { keysBackupService.get().restoreKeysWithRecoveryKey( currentVersion, - savedKeyBackupKeyInfo?.recoveryKey ?: "", + recoveryKey, roomId, sessionId, null, - it ) - }.successfullyNumberOfImportedKeys + .successfullyNumberOfImportedKeys } catch (failure: Throwable) { // Fail silently Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt new file mode 100644 index 000000000..e4c0469c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask +import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings +import org.matrix.rustcomponents.sdk.crypto.EventEncryptionAlgorithm +import org.matrix.rustcomponents.sdk.crypto.HistoryVisibility +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.RequestType +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("PrepareToEncryptUseCase", LoggerTag.CRYPTO) + +@SessionScope +internal class PrepareToEncryptUseCase @Inject constructor( + private val olmMachine: OlmMachine, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoStore: IMXCommonCryptoStore, + private val getRoomUserIds: GetRoomUserIdsUseCase, + private val requestSender: RequestSender, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val keysBackupService: RustKeyBackupService, + private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase, +) { + + private val keyClaimLock: Mutex = Mutex() + private val roomKeyShareLocks: ConcurrentHashMap = ConcurrentHashMap() + + suspend operator fun invoke(roomId: String, ensureAllMembersAreLoaded: Boolean, forceDistributeToUnverified: Boolean = false) { + withContext(coroutineDispatchers.crypto) { + Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") + // Ensure to load all room members + if (ensureAllMembersAreLoaded) { + measureTimeMillis { + try { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") + throw failure + } + }.also { + Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId load room members took: $it ms") + } + } + val userIds = getRoomUserIds(roomId) + val algorithm = getEncryptionAlgorithm(roomId) + if (algorithm == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) + Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") + throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)) + } + preshareRoomKey(roomId, userIds, forceDistributeToUnverified) + } + } + + private fun getEncryptionAlgorithm(roomId: String): String? { + return cryptoStore.getRoomAlgorithm(roomId) + } + + private suspend fun preshareRoomKey(roomId: String, roomMembers: List, forceDistributeToUnverified: Boolean) { + measureTimeMillis { + claimMissingKeys(roomMembers) + }.also { + Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId claimMissingKeys took: $it ms") + } + val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() } + var sharedKey = false + + val info = cryptoStore.getRoomCryptoInfo(roomId) + ?: throw java.lang.UnsupportedOperationException("Encryption not configured in this room") + // how to react if this is null?? + if (info.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + throw java.lang.UnsupportedOperationException("Unsupported algorithm ${info.algorithm}") + } + val settings = EncryptionSettings( + algorithm = EventEncryptionAlgorithm.MEGOLM_V1_AES_SHA2, + onlyAllowTrustedDevices = if (forceDistributeToUnverified) { + false + } else { + cryptoStore.getGlobalBlacklistUnverifiedDevices() || + info.blacklistUnverifiedDevices + }, + rotationPeriod = info.rotationPeriodMs.toULong(), + rotationPeriodMsgs = info.rotationPeriodMsgs.toULong(), + historyVisibility = if (info.shouldShareHistory) { + HistoryVisibility.SHARED + } else if (shouldEncryptForInvitedMembers.invoke(roomId)) { + HistoryVisibility.INVITED + } else { + HistoryVisibility.JOINED + } + ) + measureTimeMillis { + keyShareLock.withLock { + coroutineScope { + olmMachine.shareRoomKey(roomId, roomMembers, settings).map { + when (it) { + is Request.ToDevice -> { + sharedKey = true + async { + sendToDevice(olmMachine, it) + } + } + else -> { + // This request can only be a to-device request but + // we need to handle all our cases and put this + // async block for our joinAll to work. + async {} + } + } + }.joinAll() + } + } + }.also { + Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId shareRoomKeys took: $it ms") + } + + // If we sent out a room key over to-device messages it's likely that we created a new one + // Try to back the key up + if (sharedKey) { + keysBackupService.maybeBackupKeys() + } + } + + private suspend fun claimMissingKeys(roomMembers: List) = keyClaimLock.withLock { + val request = olmMachine.getMissingSessions(roomMembers) + // This request can only be a keys claim request. + when (request) { + is Request.KeysClaim -> { + claimKeys(request) + } + else -> { + } + } + } + + private suspend fun sendToDevice(olmMachine: OlmMachine, request: Request.ToDevice) { + try { + requestSender.sendToDevice(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}") + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO sendToDevice(): error") + } + } + + private suspend fun claimKeys(request: Request.KeysClaim) { + try { + val response = requestSender.claimKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## CRYPTO claimKeys(): error") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt deleted file mode 100644 index d37e60d28..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -@SessionScope -internal class RoomDecryptorProvider @Inject constructor( - private val olmDecryptionFactory: MXOlmDecryptionFactory, - private val megolmDecryptionFactory: MXMegolmDecryptionFactory -) { - - // A map from algorithm to MXDecrypting instance, for each room - private val roomDecryptors: MutableMap> = HashMap() - - private val newSessionListeners = ArrayList() - - fun addNewSessionListener(listener: NewSessionListener) { - if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) - } - - fun removeSessionListener(listener: NewSessionListener) { - newSessionListeners.remove(listener) - } - - /** - * Get a decryptor for a given room and algorithm. - * If we already have a decryptor for the given room and algorithm, return - * it. Otherwise try to instantiate it. - * - * @param roomId the room id - * @param algorithm the crypto algorithm - * @return the decryptor - * // TODO Create another method for the case of roomId is null - */ - fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { - // sanity check - if (algorithm.isNullOrEmpty()) { - Timber.e("## getRoomDecryptor() : null algorithm") - return null - } - if (roomId != null && roomId.isNotEmpty()) { - synchronized(roomDecryptors) { - val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() } - val alg = decryptors[algorithm] - if (alg != null) { - return alg - } - } - } - val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm) - if (decryptingClass) { - val alg = when (algorithm) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply { - this.newSessionListener = object : NewSessionListener { - override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { - // PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor - newSessionListeners.toList().forEach { - try { - it.onNewSession(roomId, senderKey, sessionId) - } catch (ignore: Throwable) { - } - } - } - } - } - else -> olmDecryptionFactory.create() - } - if (!roomId.isNullOrEmpty()) { - synchronized(roomDecryptors) { - roomDecryptors[roomId]?.put(algorithm, alg) - } - } - return alg - } - return null - } - - fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { - if (roomId == null || algorithm == null) { - return null - } - return roomDecryptors[roomId]?.get(algorithm) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt deleted file mode 100644 index 9f6714cc4..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import javax.inject.Inject - -@SessionScope -internal class RoomEncryptorsStore @Inject constructor( - private val cryptoStore: IMXCryptoStore, - private val megolmEncryptionFactory: MXMegolmEncryptionFactory, - private val olmEncryptionFactory: MXOlmEncryptionFactory, -) { - - // MXEncrypting instance for each room. - private val roomEncryptors = mutableMapOf() - - fun put(roomId: String, alg: IMXEncrypting) { - synchronized(roomEncryptors) { - roomEncryptors.put(roomId, alg) - } - } - - fun get(roomId: String): IMXEncrypting? { - return synchronized(roomEncryptors) { - val cache = roomEncryptors[roomId] - if (cache != null) { - return@synchronized cache - } else { - val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) { - MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) - MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) - else -> null - } - alg?.let { roomEncryptors.put(roomId, it) } - return@synchronized alg - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt new file mode 100644 index 000000000..e2def5af8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import androidx.lifecycle.LiveData +import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult +import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor +import org.matrix.rustcomponents.sdk.crypto.Request +import javax.inject.Inject + +internal class RustCrossSigningService @Inject constructor( + private val olmMachine: OlmMachine, + private val outgoingRequestsProcessor: OutgoingRequestsProcessor, + private val computeShieldForGroup: ComputeShieldForGroupUseCase +) : CrossSigningService { + + /** + * Is our own identity trusted. + */ + override suspend fun isCrossSigningVerified(): Boolean { + return when (val identity = olmMachine.getIdentity(olmMachine.userId())) { + is OwnUserIdentity -> identity.verified() + else -> false + } + } + + override suspend fun isUserTrusted(otherUserId: String): Boolean { + // This seems to be used only in tests. + return checkUserTrust(otherUserId).isVerified() + } + + /** + * Will not force a download of the key, but will verify signatures trust chain. + * Checks that my trusted user key has signed the other user UserKey + */ + override suspend fun checkUserTrust(otherUserId: String): UserTrustResult { + val identity = olmMachine.getIdentity(olmMachine.userId()) + + // While UserTrustResult has many different states, they are by the callers + // converted to a boolean value immediately, thus we don't need to support + // all the values. + return if (identity != null) { + val verified = identity.verified() + + if (verified) { + UserTrustResult.Success + } else { + UserTrustResult.Failure("failed to verify $otherUserId") + } + } else { + UserTrustResult.CrossSigningNotConfigured(otherUserId) + } + } + + /** + * Initialize cross signing for this user. + * Users needs to enter credentials + */ + override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { + // ensure our keys are sent before initialising + outgoingRequestsProcessor.processOutgoingRequests(olmMachine) { + it is Request.KeysUpload + } + olmMachine.bootstrapCrossSigning(uiaInterceptor) + } + + /** + * Inject the private cross signing keys, likely from backup, into our store. + * + * This will check if the injected private cross signing keys match the public ones provided + * by the server and if they do so + */ + override suspend fun checkTrustFromPrivateKeys( + masterKeyPrivateKey: String?, + uskKeyPrivateKey: String?, + sskPrivateKey: String? + ): UserTrustResult { + val export = PrivateKeysInfo(masterKeyPrivateKey, sskPrivateKey, uskKeyPrivateKey) + return olmMachine.importCrossSigningKeys(export) + } + + /** + * Get the public cross signing keys for the given user. + * + * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. + */ + override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return olmMachine.getIdentity(otherUserId)?.toMxCrossSigningInfo() + } + + override fun getLiveCrossSigningKeys(userId: String): LiveData> { + return olmMachine.getLiveUserIdentity(userId) + } + + /** Get our own public cross signing keys. */ + override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return getUserCrossSigningKeys(olmMachine.userId()) + } + + /** Get our own private cross signing keys. */ + override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return olmMachine.exportCrossSigningKeys() + } + + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + return olmMachine.getLivePrivateCrossSigningKeys() + } + + /** + * Can we sign our other devices or other users. + * + * Returning true means that we have the private self-signing and user-signing keys at hand. + */ + override fun canCrossSign(): Boolean { + val status = olmMachine.crossSigningStatus() + + return status.hasSelfSigning && status.hasUserSigning + } + + override fun allPrivateKeysKnown(): Boolean { + val status = olmMachine.crossSigningStatus() + + return status.hasMaster && status.hasSelfSigning && status.hasUserSigning + } + + /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server. */ + override suspend fun trustUser(otherUserId: String) { + // This is only used in a test + val userIdentity = olmMachine.getIdentity(otherUserId) + if (userIdentity != null) { + userIdentity.verify() + } else { + throw Throwable("## CrossSigning - CrossSigning is not setup for this account") + } + } + + /** Mark our own master key as trusted. */ + override suspend fun markMyMasterKeyAsTrusted() { + // This doesn't seem to be used? + trustUser(olmMachine.userId()) + } + + /** + * Sign one of your devices and upload the signature. + */ + override suspend fun trustDevice(deviceId: String) { + val device = olmMachine.getDevice(olmMachine.userId(), deviceId) + if (device != null) { + val verified = device.verify() + if (verified) { + return + } else { + error("This device [$deviceId] is not known, or not yours") + } + } else { + error("This device [$deviceId] is not known") + } + } + + /** + * Check if a device is trusted. + * + * This will check that we have a valid trust chain from our own master key to a device, either + * using the self-signing key for our own devices or using the user-signing key and the master + * key of another user. + */ + override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val device = olmMachine.getDevice(otherUserId, otherDeviceId) + + return if (device != null) { + // TODO i don't quite understand the semantics here and there are no docs for + // DeviceTrustResult, what do the different result types mean exactly, + // do you return success only if the Device is cross signing verified? + // what about the local trust if it isn't? why is the local trust passed as an argument here? + DeviceTrustResult.Success(device.trustLevel()) + } else { + DeviceTrustResult.UnknownDevice(otherDeviceId) + } + } + + override suspend fun onSecretMSKGossip(mskPrivateKey: String) { + // This seems to be unused. + } + + override suspend fun onSecretSSKGossip(sskPrivateKey: String) { + // This as well + } + + override suspend fun onSecretUSKGossip(uskPrivateKey: String) { + // And + } + + override suspend fun shieldForGroup(userIds: List): RoomEncryptionTrustLevel { + return computeShieldForGroup(olmMachine, userIds) + } + + override suspend fun checkTrustAndAffectedRoomShields(userIds: List) { + // TODO + // is this needed in rust? + } + + override fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { + // is this needed in rust? should be moved to internal API? + val verified = runBlocking { + val identity = olmMachine.getIdentity(olmMachine.userId()) as? OwnUserIdentity + identity?.verified() + } + return if (verified == null) { + UserTrustResult.CrossSigningNotConfigured(olmMachine.userId()) + } else { + UserTrustResult.Success + } + } + + override fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { + // is this needed in rust? should be moved to internal API? + return UserTrustResult.Failure("Not used in rust") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt new file mode 100755 index 000000000..d74363126 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -0,0 +1,921 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.map +import androidx.paging.PagedList +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest +import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.model.shouldShareHistory +import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse +import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService +import org.matrix.android.sdk.internal.crypto.model.SessionInfo +import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor +import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator +import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask +import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask +import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask +import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId +import org.matrix.android.sdk.internal.crypto.verification.RustVerificationService +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.StreamEventsManager +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject +import kotlin.math.max + +/** + * A `CryptoService` class instance manages the end-to-end crypto for a session. + * + * + * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted + * before sending. + * In the other hand, received events goes through CryptoService for decrypting. + * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. + * Specially, it tracks all room membership changes events in order to do keys updates. + */ + +private val loggerTag = LoggerTag("RustCryptoService", LoggerTag.CRYPTO) + +@SessionScope +internal class RustCryptoService @Inject constructor( + @UserId private val myUserId: String, + @DeviceId private val deviceId: String, + // the crypto store + private val cryptoStore: IMXCommonCryptoStore, + // Set of parameters used to configure/customize the end-to-end crypto. + private val mxCryptoConfig: MXCryptoConfig, + // Actions + private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + // Tasks + private val deleteDeviceTask: DeleteDeviceTask, + private val getDevicesTask: GetDevicesTask, + private val getDeviceInfoTask: GetDeviceInfoTask, + private val setDeviceNameTask: SetDeviceNameTask, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val olmMachine: OlmMachine, + private val crossSigningService: CrossSigningService, + private val verificationService: RustVerificationService, + private val keysBackupService: RustKeyBackupService, + private val megolmSessionImportManager: MegolmSessionImportManager, + private val liveEventManager: dagger.Lazy, + private val prepareToEncrypt: PrepareToEncryptUseCase, + private val encryptEventContent: EncryptEventContentUseCase, + private val getRoomUserIds: GetRoomUserIdsUseCase, + private val outgoingRequestsProcessor: OutgoingRequestsProcessor, + private val matrixConfiguration: MatrixConfiguration, + private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter, +) : CryptoService { + + private val isStarting = AtomicBoolean(false) + private val isStarted = AtomicBoolean(false) + + override fun name() = "rust-sdk" + + override suspend fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { + when (event.type) { + EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) + } + } + + override suspend fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) { + if (event.isStateEvent()) { + when (event.getClearType()) { + EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) + EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) + } + } else { + if (!isInitialSync) { + verificationService.onEvent(roomId, event) + } + } + } + + override suspend fun setDeviceName(deviceId: String, deviceName: String) { + val params = SetDeviceNameTask.Params(deviceId, deviceName) + setDeviceNameTask.execute(params) + try { + downloadKeysIfNeeded(listOf(myUserId), true) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w(failure, "setDeviceName: Failed to refresh of crypto device") + } + } + + override suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { + val params = DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null) + deleteDeviceTask.execute(params) + } + + override suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { + deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor) + } + + override fun getCryptoVersion(context: Context, longFormat: Boolean): String { + val version = org.matrix.rustcomponents.sdk.crypto.version() + val gitHash = org.matrix.rustcomponents.sdk.crypto.versionInfo().gitSha + val vodozemac = org.matrix.rustcomponents.sdk.crypto.vodozemacVersion() + return if (longFormat) "Rust SDK $version ($gitHash), Vodozemac $vodozemac" else version + } + + override suspend fun getMyCryptoDevice(): CryptoDeviceInfo = withContext(coroutineDispatchers.io) { + olmMachine.ownDevice() + } + + override suspend fun fetchDevicesList(): List { + val devicesList: List + withContext(coroutineDispatchers.io) { + devicesList = getDevicesTask.execute(Unit).devices.orEmpty() + cryptoStore.saveMyDevicesInfo(devicesList) + } + return devicesList + } + + override fun getMyDevicesInfo(): List { + return cryptoStore.getMyDevicesInfo() + } + + override fun getMyDevicesInfoLive(): LiveData> { + return cryptoStore.getLiveMyDevicesInfo() + } + + override suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo { + val params = GetDeviceInfoTask.Params(deviceId) + return getDeviceInfoTask.execute(params) + } + + override suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return if (onlyBackedUp) { + keysBackupService.getTotalNumbersOfBackedUpKeys() + } else { + keysBackupService.getTotalNumbersOfKeys() + } + // return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) + } + + /** + * Tell if the MXCrypto is started. + * + * @return true if the crypto is started + */ + override fun isStarted(): Boolean { + return isStarted.get() + } + + /** + * Start the crypto module. + * Device keys will be uploaded, then one time keys if there are not enough on the homeserver + * and, then, if this is the first time, this new device will be announced to all other users + * devices. + * + */ + override fun start() { + internalStart() + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + cryptoStore.open() + // Just update + tryOrNull { fetchDevicesList() } + cryptoStore.tidyUpDataBase() + } + } + + private fun internalStart() { + if (isStarted.get() || isStarting.get()) { + return + } + isStarting.set(true) + + try { + setRustLogger() + Timber.tag(loggerTag.value).v( + "## CRYPTO | Successfully started up an Olm machine for " + + "$myUserId, $deviceId, identity keys: ${this.olmMachine.identityKeys()}" + ) + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).v("Failed create an Olm machine: $throwable") + } + + // After the initial rust migration the current keys & signature might not be there + // The session is then in an invalid state and can fire unexpected verify popups + // this will only do network request once. + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + tryOrNull { + downloadKeysIfNeeded(listOf(myUserId), false) + } + } + + // We try to enable key backups, if the backup version on the server is trusted, + // we're gonna continue backing up. + cryptoCoroutineScope.launch { + tryOrNull { + keysBackupService.checkAndStartKeysBackup() + } + } + + isStarting.set(false) + isStarted.set(true) + } + + /** + * Close the crypto. + */ + override fun close() { + cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) + cryptoCoroutineScope.launch { + withContext(NonCancellable) { + cryptoStore.close() + } + } + } + + // Always enabled on Matrix Android SDK2 + override fun isCryptoEnabled() = true + + /** + * @return the Keys backup Service + */ + override fun keysBackupService() = keysBackupService + + /** + * @return the VerificationService + */ + override fun verificationService() = verificationService + + override fun crossSigningService() = crossSigningService + + /** + * A sync response has been received. + */ + override suspend fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + if (isStarted()) { + cryptoStore.storeData(cryptoStoreAggregator) + + outgoingRequestsProcessor.processOutgoingRequests(olmMachine) + // This isn't a copy paste error. Sending the outgoing requests may + // claim one-time keys and establish 1-to-1 Olm sessions with devices, while some + // outgoing requests are waiting for an Olm session to be established (e.g. forwarding + // room keys or sharing secrets). + + // The second call sends out those requests that are waiting for the + // keys claim request to be sent out. + // This could be omitted but then devices might be waiting for the next + outgoingRequestsProcessor.processOutgoingRequests(olmMachine) + } + } + + /** + * Provides the device information for a user id and a device Id. + * + * @param userId the user id + * @param deviceId the device id + */ + override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { + if (userId.isEmpty() || deviceId.isNullOrEmpty()) return null + return withContext(coroutineDispatchers.io) { olmMachine.getCryptoDeviceInfo(userId, deviceId) } + } + + override suspend fun getCryptoDeviceInfo(userId: String): List { + return withContext(coroutineDispatchers.io) { + olmMachine.getCryptoDeviceInfo(userId) + } + } + + override fun getLiveCryptoDeviceInfo(): LiveData> { + return getLiveCryptoDeviceInfo(listOf(myUserId)) + } + + override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { + return getLiveCryptoDeviceInfo(listOf(userId)) + } + + override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { + return olmMachine.getLiveDevices(userIds) + } + + override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData> { + return getLiveCryptoDeviceInfo().map { + it.find { it.deviceId == deviceId }.toOptional() + } + } + + override suspend fun getCryptoDeviceInfoList(userId: String): List { + return olmMachine.getCryptoDeviceInfo(userId) + } + + override fun getMyDevicesInfoLive(deviceId: String): LiveData> { + return cryptoStore.getLiveMyDevicesInfo(deviceId) + } + +// override fun getLiveCryptoDeviceInfoList(userId: String) = getLiveCryptoDeviceInfoList(listOf(userId)) +// +// override fun getLiveCryptoDeviceInfoList(userIds: List): Flow> { +// return olmMachine.getLiveDevices(userIds) +// } + + /** + * Configure a room to use encryption. + * + * @param roomId the room id to enable encryption in. + * @param info the encryption config for the room. + * @param membersId list of members to start tracking their devices + * @return true if the operation succeeds. + */ + private suspend fun setEncryptionInRoom( + roomId: String, + info: EncryptionEventContent?, + membersId: List + ): Boolean { + // If we already have encryption in this room, we should ignore this event + // (for now at least. Maybe we should alert the user somehow?) + val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) + val algorithm = info?.algorithm + + if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { + Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + return false + } + + // TODO CHECK WITH VALERE + cryptoStore.setAlgorithmInfo(roomId, info) + + if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + Timber.tag(loggerTag.value).e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + return false + } + + // if encryption was not previously enabled in this room, we will have been + // ignoring new device events for these users so far. We may well have + // up-to-date lists for some users, for instance if we were sharing other + // e2e rooms with them, so there is room for optimisation here, but for now + // we just invalidate everyone in the room. + if (null == existingAlgorithm) { + Timber.tag(loggerTag.value).d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") + + val userIds = ArrayList(membersId) + olmMachine.updateTrackedUsers(userIds) + } + + return true + } + + /** + * Tells if a room is encrypted with MXCRYPTO_ALGORITHM_MEGOLM. + * + * @param roomId the room id + * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM + */ + override fun isRoomEncrypted(roomId: String): Boolean { + return cryptoSessionInfoProvider.isRoomEncrypted(roomId) + } + + /** + * @return the stored device keys for a user. + */ + override suspend fun getUserDevices(userId: String): MutableList { + return this.getCryptoDeviceInfoList(userId).toMutableList() + } + + private fun isEncryptionEnabledForInvitedUser(): Boolean { + return mxCryptoConfig.enableEncryptionForInvitedMembers + } + + override fun getEncryptionAlgorithm(roomId: String): String? { + return cryptoStore.getRoomAlgorithm(roomId) + } + + /** + * Determine whether we should encrypt messages for invited users in this room. + *

+ * Check here whether the invited members are allowed to read messages in the room history + * from the point they were invited onwards. + * + * @return true if we should encrypt messages for invited users. + */ + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return cryptoStore.shouldEncryptForInvitedMembers(roomId) + } + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param roomId the room identifier the event will be sent. + */ + override suspend fun encryptEventContent( + eventContent: Content, + eventType: String, + roomId: String + ): MXEncryptEventContentResult { + return encryptEventContent.invoke(eventContent, eventType, roomId) + } + + override fun discardOutboundSession(roomId: String) { + olmMachine.discardRoomKey(roomId) + } + + /** + * Decrypt an event. + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or throw in case of error + */ + @Throws(MXCryptoError::class) + override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + return try { + olmMachine.decryptRoomEvent(event) + } catch (mxCryptoError: MXCryptoError) { + if (mxCryptoError is MXCryptoError.Base && ( + mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID || + mxCryptoError.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX)) { + Timber.v("Try to perform a lazy migration from legacy store") + /** + * It's a bit hacky, check how this can be better integrated with rust? + */ + val content = event.content?.toModel() ?: throw mxCryptoError + val roomId = event.roomId + val sessionId = content.sessionId + val senderKey = content.senderKey + if (roomId != null && sessionId != null) { + // try to perform a lazy migration from legacy store + val legacy = tryOrNull("Failed to access legacy crypto store") { + cryptoStore.getInboundGroupSession(sessionId, senderKey.orEmpty()) + } + if (legacy == null || olmMachine.importRoomKey(legacy).isFailure) { + perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) + } + } + } + throw mxCryptoError + } + } + + /** + * Handle an m.room.encryption event. + * + * @param roomId the roomId. + * @param event the encryption event. + */ + private suspend fun onRoomEncryptionEvent(roomId: String, event: Event) { + if (!event.isStateEvent()) { + // Ignore + Timber.tag(loggerTag.value).w("Invalid encryption event") + return + } + + // Do not load members here, would defeat lazy loading +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// val params = LoadRoomMembersTask.Params(roomId) +// try { +// loadRoomMembersTask.execute(params) +// } catch (throwable: Throwable) { +// Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") +// } finally { + val userIds = getRoomUserIds(roomId) + setEncryptionInRoom(roomId, event.content?.toModel(), userIds) +// } +// } + } + + override fun onE2ERoomMemberLoadedFromServer(roomId: String) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val userIds = getRoomUserIds(roomId) + // Because of LL we might want to update tracked users + olmMachine.updateTrackedUsers(userIds) + } + } + + override suspend fun deviceWithIdentityKey(userId: String, senderKey: String, algorithm: String): CryptoDeviceInfo? { + return olmMachine.getCryptoDeviceInfo(userId) + .firstOrNull { it.identityKey() == senderKey } + } + + override suspend fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + Timber.w("Rust stack only support API to set local trust") + olmMachine.setDeviceLocalTrust(userId, deviceId, trustLevel.isLocallyVerified().orFalse()) + } + + /** + * Handle a change in the membership state of a member of a room. + * + * @param roomId the roomId + * @param event the membership event causing the change + */ + private suspend fun onRoomMembershipEvent(roomId: String, event: Event) { + // We only care about the memberships if this room is encrypted + if (!isRoomEncrypted(roomId)) { + return + } + event.stateKey?.let { userId -> + val roomMember: RoomMemberContent? = event.content.toModel() + val membership = roomMember?.membership + if (membership == Membership.JOIN) { + // make sure we are tracking the deviceList for this user. + cryptoCoroutineScope.launch { + olmMachine.updateTrackedUsers(listOf(userId)) + } + } else if (membership == Membership.INVITE && + shouldEncryptForInvitedMembers(roomId) && + isEncryptionEnabledForInvitedUser()) { + // track the deviceList for this invited user. + // Caution: there's a big edge case here in that federated servers do not + // know what other servers are in the room at the time they've been invited. + // They therefore will not send device updates if a user logs in whilst + // their state is invite. + olmMachine.updateTrackedUsers(listOf(userId)) + } else { + // nop + } + } + } + + private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { + if (!event.isStateEvent()) return + val eventContent = event.content.toModel() + val historyVisibility = eventContent?.historyVisibility + if (historyVisibility == null) { + if (cryptoStoreAggregator != null) { + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false + } else { + cryptoStore.setShouldShareHistory(roomId, false) + } + } else { + if (cryptoStoreAggregator != null) { + // encryption for the invited members will be blocked if the history visibility is "joined" + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory() + } else { + // encryption for the invited members will be blocked if the history visibility is "joined" + cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) + cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) + } + } + } + + private fun notifyRoomKeyReceived( + roomId: String, + sessionId: String, + ) { + megolmSessionImportManager.dispatchNewSession(roomId, sessionId) + cryptoCoroutineScope.launch { + keysBackupService.maybeBackupKeys() + } + } + + override suspend fun onSyncWillProcess(isInitialSync: Boolean) { + // nothing no rust + } + + override suspend fun receiveSyncChanges( + toDevice: ToDeviceSyncResponse?, + deviceChanges: DeviceListResponse?, + keyCounts: DeviceOneTimeKeysCountSyncResponse?, + deviceUnusedFallbackKeyTypes: List?, + nextBatch: String?, + ) { + // Decrypt and handle our to-device events + val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts, deviceUnusedFallbackKeyTypes, nextBatch) + + // Notify the our listeners about room keys so decryption is retried. + toDeviceEvents.events.orEmpty().forEach { event -> + Timber.tag(loggerTag.value) + .d("[${myUserId.take(7)}|${deviceId}] Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}") + + if (event.getClearType() == EventType.ENCRYPTED) { + // rust failed to decrypt it + matrixConfiguration.cryptoAnalyticsPlugin?.onFailToDecryptToDevice( + Throwable("receiveSyncChanges") + ) + } + when (event.type) { + EventType.ROOM_KEY -> { + val content = event.getClearContent().toModel() ?: return@forEach + content.sessionKey + val roomId = content.sessionId ?: return@forEach + val sessionId = content.sessionId + + notifyRoomKeyReceived(roomId, sessionId) + matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.ROOM_KEY) + } + EventType.FORWARDED_ROOM_KEY -> { + val content = event.getClearContent().toModel() ?: return@forEach + + val roomId = content.sessionId ?: return@forEach + val sessionId = content.sessionId + + notifyRoomKeyReceived(roomId, sessionId) + matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.FORWARDED_ROOM_KEY) + } + EventType.SEND_SECRET -> { + // The rust-sdk will clear this event if it's invalid, this will produce an invalid base64 error + // when we try to construct the recovery key. + val secretContent = event.getClearContent().toModel() ?: return@forEach + this.keysBackupService.onSecretKeyGossip(secretContent.secretValue) + } + else -> { + this.verificationService.onEvent(null, event) + } + } + liveEventManager.get().dispatchOnLiveToDevice(event) + } + } + + /** + * Export the crypto keys. + * + * @param password the password + * @return the exported keys + */ + override suspend fun exportRoomKeys(password: String): ByteArray { + val iterationCount = max(10000, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT) + return olmMachine.exportKeys(password, iterationCount) + } + + override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) { + cryptoStore.blockUnverifiedDevicesInRoom(roomId, block) + } + + /** + * Import the room keys. + * + * @param roomKeysAsArray the room keys as array. + * @param password the password + * @param progressListener the progress listener + * @return the result ImportRoomKeysResult + */ + override suspend fun importRoomKeys( + roomKeysAsArray: ByteArray, + password: String, + progressListener: ProgressListener? + ): ImportRoomKeysResult { + val result = olmMachine.importKeys(roomKeysAsArray, password, progressListener).also { + megolmSessionImportManager.dispatchKeyImportResults(it) + } + keysBackupService.maybeBackupKeys() + + return result + } + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + override fun setWarnOnUnknownDevices(warn: Boolean) { + // TODO this doesn't seem to be used anymore? + warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn) + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + cryptoStore.setGlobalBlacklistUnverifiedDevices(block) + } + + override fun getLiveGlobalCryptoConfig(): LiveData { + return cryptoStore.getLiveGlobalCryptoConfig() + } + + // Until https://github.com/matrix-org/matrix-rust-sdk/issues/1364 + override fun supportsDisablingKeyGossiping() = false + override fun enableKeyGossiping(enable: Boolean) { + if (!enable) throw UnsupportedOperationException("Not supported by rust") + } + + override fun isKeyGossipingEnabled(): Boolean { + return true + } + + override fun supportsShareKeysOnInvite() = false + + override fun supportsKeyWithheld() = true + override fun supportsForwardedKeyWiththeld() = false + + override fun enableShareKeyOnInvite(enable: Boolean) { + if (enable && !supportsShareKeysOnInvite()) { + throw java.lang.UnsupportedOperationException("Enable share key on invite not implemented in rust") + } + } + + override fun isShareKeysOnInviteEnabled() = false + + override fun setRoomUnBlockUnverifiedDevices(roomId: String) { + cryptoStore.blockUnverifiedDevicesInRoom(roomId, false) + } + +// override fun getDeviceTrackingStatus(userId: String): Int { +// olmMachine.isUserTracked(userId) +// } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @return true to unilaterally blacklist all unverified devices. + */ + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return cryptoStore.getGlobalBlacklistUnverifiedDevices() + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * + * @param roomId the room id + * @return true if the client should encrypt messages only for the verified devices. + */ +// TODO add this info in CryptoRoomEntity? + override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { + return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) } + ?: false + } + + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { + return cryptoStore.getLiveBlockUnverifiedDevices(roomId) + } + + /** + * Re request the encryption keys required to decrypt an event. + * + * @param event the event to decrypt again. + */ + override suspend fun reRequestRoomKeyForEvent(event: Event) { + outgoingRequestsProcessor.processRequestRoomKey(olmMachine, event) + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + // TODO + } + + /** + * Add a GossipingRequestListener listener. + * + * @param listener listener + */ + override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + // TODO + } + + override suspend fun downloadKeysIfNeeded(userIds: List, forceDownload: Boolean): MXUsersDevicesMap { + return withContext(coroutineDispatchers.crypto) { + olmMachine.ensureUserDevicesMap(userIds, forceDownload) + } + } + + override fun addNewSessionListener(newSessionListener: NewSessionListener) { + megolmSessionImportManager.addListener(newSessionListener) + } + + override fun removeSessionListener(listener: NewSessionListener) { + megolmSessionImportManager.removeListener(listener) + } +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString(): String { + return "DefaultCryptoService of $myUserId ($deviceId)" + } + + // Until https://github.com/matrix-org/matrix-rust-sdk/issues/701 + // https://github.com/matrix-org/matrix-rust-sdk/issues/702 + override fun supportKeyRequestInspection() = false + override fun getOutgoingRoomKeyRequests(): List { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + throw UnsupportedOperationException("Not supported by rust") +// return cryptoStore.getOutgoingRoomKeyRequestsPaged() + } + + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + throw UnsupportedOperationException("Not supported by rust") + } + + override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { + // TODO rust? + } + + override fun getIncomingRoomKeyRequests(): List { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun getGossipingEventsTrail(): LiveData> { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun getGossipingEvents(): List { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { + throw UnsupportedOperationException("Not supported by rust") + } + + override fun logDbUsageInfo() { + // not available with rust + // cryptoStore.logDbUsageInfo() + } + + override suspend fun prepareToEncrypt(roomId: String) = prepareToEncrypt.invoke(roomId, ensureAllMembersAreLoaded = true) + + override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set?) { + // TODO("Not yet implemented") + } + + companion object { + const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustEncryptionConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustEncryptionConfiguration.kt new file mode 100644 index 000000000..f86e76b78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustEncryptionConfiguration.kt @@ -0,0 +1,35 @@ +/* +* Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.internal.database.RealmKeysUtils +import org.matrix.android.sdk.internal.di.UserMd5 +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class RustEncryptionConfiguration @Inject constructor( + @UserMd5 private val userMd5: String, + private val realmKeyUtil: RealmKeysUtils, +) { + + fun getDatabasePassphrase(): String { + // let's reuse the code for realm that creates a random 64 bytes array. + return realmKeyUtil.getRealmEncryptionKey("crypto_module_rust_$userMd5").toBase64NoPadding() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt index 5691f24d1..ba4677666 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -16,296 +16,29 @@ package org.matrix.android.sdk.internal.crypto -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor +import org.matrix.rustcomponents.sdk.crypto.Request import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider -private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) - -@SessionScope internal class SecretShareManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val cryptoCoroutineScope: CoroutineScope, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val clock: Clock, -) { - - companion object { - private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes - } - - /** - * Secret gossiping only occurs during a limited window period after interactive verification. - * We keep track of recent verification in memory for that purpose (no need to persist) - */ - private val recentlyVerifiedDevices = mutableMapOf() - private val verifMutex = Mutex() - - /** - * Secrets are exchanged as part of interactive verification, - * so we can just store in memory. - */ - private val outgoingSecretRequests = mutableListOf() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - fun addListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - /** - * Called when a session has been verified. - * This information can be used by the manager to decide whether or not to fullfill gossiping requests. - * This should be called as fast as possible after a successful self interactive verification - */ - fun onVerificationCompleteForDevice(deviceId: String) { - // For now we just keep an in memory cache - cryptoCoroutineScope.launch { - verifMutex.withLock { - recentlyVerifiedDevices[deviceId] = clock.epochMillis() - } - } - } - - suspend fun handleSecretRequest(toDevice: Event) { - val request = toDevice.getClearContent().toModel() - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request") - } - -// val (action, requestingDeviceId, requestId, secretName) = it - val secretName = request.secretName ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing secret name") - } - - val userId = toDevice.senderId ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing senderId") - } - - if (userId != credentials.userId) { - // secrets are only shared between our own devices - Timber.tag(loggerTag.value) - .e("Ignoring secret share request from other users $userId") - return - } - - val deviceId = request.requestingDeviceId - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request norequestingDeviceId ") - } - - val device = cryptoStore.getUserDevice(credentials.userId, deviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .e("Received secret share request from unknown device $deviceId") - } - - val isRequestingDeviceTrusted = device.isVerified - val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) - if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { - // we can share the secret - - val secretValue = when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master - SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned - USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user - KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey - ?.let { - extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() - } - else -> null - } - if (secretValue == null) { - Timber.tag(loggerTag.value) - .i("The secret is unknown $secretName, passing to app layer") - val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } - toList.onEach { listener -> - listener.onSecretShareRequest(request) - } - return - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to mapOf( - "request_id" to request.requestId, - "secret" to secretValue - ) - ) - - // Is it possible that we don't have an olm session? - val devicesByUser = mapOf(device.userId to listOf(device)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Can't share secret ${request.secretName}: Failed to establish olm session") - return - } - - val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value) - .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") - return - } - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - // raise the retries for secret - sendToDeviceTask.executeRetry(sendToDeviceParams, 6) - Timber.tag(loggerTag.value) - .i("successfully shared secret $secretName to ${device.shortDebugString()}") - // TODO add a trail for that in audit logs - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") - } - } else { - Timber.tag(loggerTag.value) - .d(" Received secret share request from un-authorised device ${device.deviceId}") - } - } - - private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { - val verifTimestamp = verifMutex.withLock { - recentlyVerifiedDevices[deviceId] - } ?: return false - - val age = clock.epochMillis() - verifTimestamp - - return age < SECRET_SHARE_WINDOW_DURATION - } + private val olmMachine: Provider, + private val outgoingRequestsProcessor: OutgoingRequestsProcessor) { suspend fun requestSecretTo(deviceId: String, secretName: String) { - val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { - Timber.tag(loggerTag.value) - .d("Can't request secret for $secretName unknown device $deviceId") - } - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - secretName = secretName, - requestId = createUniqueTxnId() - ) - - verifMutex.withLock { - outgoingSecretRequests.add(toDeviceContent) - } - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) - - val params = SendToDeviceTask.Params( - eventType = EventType.REQUEST_SECRET, - contentMap = contentMap - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - Timber.tag(loggerTag.value) - .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") - } + Timber.w("SecretShareManager requesting custom secrets not supported $deviceId, $secretName") + // rust stack only support requesting secrets defined in the spec (not custom secret yet) + requestMissingSecrets() } - suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { - Timber.tag(loggerTag.value) - .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") - if (!toDevice.isEncrypted()) { - // secret send messages must be encrypted - Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") - return - } - // no need to download keys, after a verification we already forced download - val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) } - if (sendingDevice == null) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}") - return - } - - // Was that sent by us? - if (sendingDevice.userId != credentials.userId) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") - return - } - - if (!sendingDevice.isVerified) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}") - return - } - - val secretContent = toDevice.getClearContent().toModel() ?: return - - val existingRequest = verifMutex.withLock { - outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } - } - - // As per spec: - // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. - if (existingRequest?.secretName == null) { - Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") - return - } - // we don't need to cancel the request as we only request to one device - // just forget about the request now - verifMutex.withLock { - outgoingSecretRequests.remove(existingRequest) - } + suspend fun requestMissingSecrets() { + this.olmMachine.get().requestMissingSecretsFromOtherSessions() - if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { - // TODO Ask to application layer? - Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + // immediately send the requests + outgoingRequestsProcessor.processOutgoingRequests(this.olmMachine.get()) { + it is Request.ToDevice && it.eventType == EventType.REQUEST_SECRET } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt new file mode 100644 index 000000000..7c7c8ce90 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore +import javax.inject.Inject + +internal class ShouldEncryptForInvitedMembersUseCase @Inject constructor(private val cryptoConfig: MXCryptoConfig, + private val cryptoStore: IMXCommonCryptoStore) { + + operator fun invoke(roomId: String): Boolean { + return cryptoConfig.enableEncryptionForInvitedMembers && cryptoStore.shouldEncryptForInvitedMembers(roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt new file mode 100644 index 000000000..8d70482ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/UserIdentities.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest +import org.matrix.android.sdk.internal.crypto.verification.prepareMethods +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import org.matrix.rustcomponents.sdk.crypto.OlmMachine +import org.matrix.rustcomponents.sdk.crypto.SignatureException + +/** + * A sealed class representing user identities. + * + * User identities can come in the form of [OwnUserIdentity] which represents + * our own user identity, or [UserIdentity] which represents a user identity + * belonging to another user. + */ +sealed class UserIdentities { + /** + * The unique ID of the user this identity belongs to. + */ + abstract fun userId(): String + + /** + * Check the verification state of the user identity. + * + * @return True if the identity is considered to be verified and trusted, false otherwise. + */ + @Throws(CryptoStoreException::class) + abstract suspend fun verified(): Boolean + + /** + * Manually verify the user identity. + * + * This will either sign the identity with our user-signing key if + * it is a identity belonging to another user, or sign the identity + * with our own device. + * + * Throws a SignatureErrorException if we can't sign the identity, + * if for example we don't have access to our user-signing key. + */ + @Throws(SignatureException::class) + abstract suspend fun verify() + + /** + * Convert the identity into a MxCrossSigningInfo class. + */ + abstract suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo +} + +/** + * A class representing our own user identity. + * + * This is backed by the public parts of our cross signing keys. + **/ +internal class OwnUserIdentity( + private val userId: String, + private val masterKey: CryptoCrossSigningKey, + private val selfSigningKey: CryptoCrossSigningKey, + private val userSigningKey: CryptoCrossSigningKey, + private val trustsOurOwnDevice: Boolean, + private val innerMachine: OlmMachine, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationRequestFactory: VerificationRequest.Factory, +) : UserIdentities() { + /** + * Our own user id. + */ + override fun userId() = userId + + /** + * Manually verify our user identity. + * + * This signs the identity with our own device and upload the signatures to the server. + * + * To perform an interactive verification user the [requestVerification] method instead. + */ + @Throws(SignatureException::class) + override suspend fun verify() { + val request = withContext(coroutineDispatchers.computation) { innerMachine.verifyIdentity(userId) } + requestSender.sendSignatureUpload(request) + } + + /** + * Check the verification state of the user identity. + * + * @return True if the identity is considered to be verified and trusted, false otherwise. + */ + @Throws(CryptoStoreException::class) + override suspend fun verified(): Boolean { + return withContext(coroutineDispatchers.io) { innerMachine.isIdentityVerified(userId) } + } + + /** + * Does the identity trust our own device. + */ + fun trustsOurOwnDevice() = trustsOurOwnDevice + + /** + * Request an interactive verification to begin + * + * This method should be used if we don't have a specific device we want to verify, + * instead we want to send out a verification request to all our devices. + * + * This sends out an `m.key.verification.request` out to all our devices that support E2EE. + * If the identity should be marked as manually verified, use the [verify] method instead. + * + * If a specific device should be verified instead + * the [org.matrix.android.sdk.internal.crypto.Device.requestVerification] method should be + * used instead. + * + * @param methods The list of [VerificationMethod] that we wish to advertise to the other + * side as being supported. + */ + @Throws(CryptoStoreException::class) + suspend fun requestVerification(methods: List): VerificationRequest { + val stringMethods = prepareMethods(methods) + val result = innerMachine.requestSelfVerification(stringMethods) + requestSender.sendVerificationRequest(result!!.request) + return verificationRequestFactory.create(result.verification) + } + + /** + * Convert the identity into a MxCrossSigningInfo class. + */ + override suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo { + val masterKey = masterKey + val selfSigningKey = selfSigningKey + val userSigningKey = userSigningKey + val trustLevel = DeviceTrustLevel(verified(), false) + // TODO remove this, this is silly, we have way too many methods to check if a user is verified + masterKey.trustLevel = trustLevel + selfSigningKey.trustLevel = trustLevel + userSigningKey.trustLevel = trustLevel + + val crossSigningKeys = listOf(masterKey, selfSigningKey, userSigningKey) + // TODO https://github.com/matrix-org/matrix-rust-sdk/issues/1129 + return MXCrossSigningInfo(userId, crossSigningKeys, false) + } +} + +/** + * A class representing another users identity. + * + * This is backed by the public parts of the users cross signing keys. + **/ +internal class UserIdentity( + private val userId: String, + private val masterKey: CryptoCrossSigningKey, + private val selfSigningKey: CryptoCrossSigningKey, + private val innerMachine: OlmMachine, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationRequestFactory: VerificationRequest.Factory, +) : UserIdentities() { + /** + * The unique ID of the user that this identity belongs to. + */ + override fun userId() = userId + + /** + * Manually verify this user identity. + * + * This signs the identity with our user-signing key. + * + * This method can fail if we don't have the private part of our user-signing key at hand. + * + * To perform an interactive verification user the [requestVerification] method instead. + */ + @Throws(SignatureException::class) + override suspend fun verify() { + val request = withContext(coroutineDispatchers.computation) { innerMachine.verifyIdentity(userId) } + requestSender.sendSignatureUpload(request) + } + + /** + * Check the verification state of the user identity. + * + * @return True if the identity is considered to be verified and trusted, false otherwise. + */ + override suspend fun verified(): Boolean { + return withContext(coroutineDispatchers.io) { innerMachine.isIdentityVerified(userId) } + } + + /** + * Request an interactive verification to begin. + * + * This method should be used if we don't have a specific device we want to verify, + * instead we want to send out a verification request to all our devices. For user + * identities that aren't our own, this method should be the primary way to verify users + * and their devices. + * + * This sends out an `m.key.verification.request` out to the room with the given room ID. + * The room **must** be a private DM that we share with this user. + * + * If the identity should be marked as manually verified, use the [verify] method instead. + * + * If a specific device should be verified instead + * the [org.matrix.android.sdk.internal.crypto.Device.requestVerification] method should be + * used instead. + * + * @param methods The list of [VerificationMethod] that we wish to advertise to the other + * side as being supported. + * @param roomId The ID of the room which represents a DM that we share with this user. + * @param transactionId The transaction id that should be used for the request that sends + * the `m.key.verification.request` to the room. + */ + @Throws(CryptoStoreException::class) + suspend fun requestVerification( + methods: List, + roomId: String, + transactionId: String + ): VerificationRequest { + val stringMethods = prepareMethods(methods) + val content = innerMachine.verificationRequestContent(userId, stringMethods)!! + val eventId = requestSender.sendRoomMessage(EventType.MESSAGE, roomId, content, transactionId).eventId + val innerRequest = innerMachine.requestVerification(userId, roomId, eventId, stringMethods)!! + return verificationRequestFactory.create(innerRequest) + } + + /** + * Convert the identity into a MxCrossSigningInfo class. + */ + override suspend fun toMxCrossSigningInfo(): MXCrossSigningInfo { +// val crossSigningKeys = listOf(masterKey, selfSigningKey) + val trustLevel = DeviceTrustLevel(verified(), false) + // TODO remove this, this is silly, we have way too many methods to check if a user is verified + masterKey.trustLevel = trustLevel + selfSigningKey.trustLevel = trustLevel + return MXCrossSigningInfo( + userId, + listOf( + masterKey.also { it.trustLevel = trustLevel }, + selfSigningKey.also { it.trustLevel = trustLevel }, + ), + // TODO https://github.com/matrix-org/matrix-rust-sdk/issues/1129 + wasTrustedOnce = false + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt deleted file mode 100644 index c263192fe..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.MXKey -import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult -import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -private const val ONE_TIME_KEYS_RETRY_COUNT = 3 - -private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO) - -@SessionScope -internal class EnsureOlmSessionsForDevicesAction @Inject constructor( - private val olmDevice: MXOlmDevice, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask -) { - - private val ensureMutex = Mutex() - - /** - * We want to synchronize a bit here, because we are iterating to check existing olm session and - * also adding some. - */ - suspend fun handle(devicesByUser: Map>, force: Boolean = false): MXUsersDevicesMap { - ensureMutex.withLock { - val results = MXUsersDevicesMap() - val deviceList = devicesByUser.flatMap { it.value } - Timber.tag(loggerTag.value) - .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}") - val devicesToCreateSessionWith = mutableListOf() - if (force) { - // we take all devices and will query otk for them - devicesToCreateSessionWith.addAll(deviceList) - } else { - // only peek devices without active session - deviceList.forEach { deviceInfo -> - val deviceId = deviceInfo.deviceId - val userId = deviceInfo.userId - val key = deviceInfo.identityKey() ?: return@forEach Unit.also { - Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key") - } - - // is there a session that as been already used? - val sessionId = olmDevice.getSessionId(key) - if (sessionId.isNullOrEmpty()) { - Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list") - devicesToCreateSessionWith.add(deviceInfo) - } else { - Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") - val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) - results.setObject(userId, deviceId, olmSessionResult) - } - } - } - - if (devicesToCreateSessionWith.isEmpty()) { - // no session to create - return results - } - val usersDevicesToClaim = MXUsersDevicesMap().apply { - devicesToCreateSessionWith.forEach { - setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE) - } - } - - // Let's now claim one time keys - val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) - val oneTimeKeys = withContext(coroutineDispatchers.io) { - oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT) - } - - // let now start olm session using the new otks - devicesToCreateSessionWith.forEach { deviceInfo -> - val userId = deviceInfo.userId - val deviceId = deviceInfo.deviceId - // Did we get an OTK - val oneTimeKey = oneTimeKeys.getObject(userId, deviceId) - if (oneTimeKey == null) { - Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}") - } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) { - Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}") - } else { - val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) - if (olmSessionId != null) { - val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId) - results.setObject(userId, deviceId, olmSessionResult) - } else { - Timber - .tag(loggerTag.value) - .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}") - } - } - } - return results - } - } - - private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { - var sessionId: String? = null - - val deviceId = deviceInfo.deviceId - val signKeyId = "ed25519:$deviceId" - val signature = oneTimeKey.signatureForUserId(userId, signKeyId) - - val fingerprint = deviceInfo.fingerprint() - if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) { - var isVerified = false - var errorMessage: String? = null - - try { - olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature) - isVerified = true - } catch (e: Exception) { - Timber.tag(loggerTag.value).d( - e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," + - " signature:$signature fingerprint:$fingerprint" - ) - Timber.tag(loggerTag.value).e( - "verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " + - " - signable json ${oneTimeKey.signalableJSONDictionary()}" - ) - errorMessage = e.message - } - - // Check one-time key signature - if (isVerified) { - sessionId = deviceInfo.identityKey()?.let { identityKey -> - olmDevice.createOutboundSession(identityKey, oneTimeKey.value) - } - - if (sessionId.isNullOrEmpty()) { - // Possibly a bad key - Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") - } else { - Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId") - } - } else { - Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage") - } - } - - return sessionId - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt deleted file mode 100644 index da0952466..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber -import javax.inject.Inject - -internal class EnsureOlmSessionsForUsersAction @Inject constructor( - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction -) { - - /** - * Try to make sure we have established olm sessions for the given users. - * @param users a list of user ids. - */ - suspend fun handle(users: List): MXUsersDevicesMap { - Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") - val devicesByUser = users.associateWith { userId -> - val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - - devices.filter { - // Don't bother setting up session to ourself - it.identityKey() != olmDevice.deviceCurve25519Key && - // Don't bother setting up sessions with blocked users - !(it.trustLevel?.isVerified() ?: false) - } - } - return ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt deleted file mode 100644 index a624b92a1..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import androidx.annotation.WorkerThread -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO) - -internal class MegolmSessionDataImporter @Inject constructor( - private val olmDevice: MXOlmDevice, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val clock: Clock, -) { - - /** - * Import a list of megolm session keys. - * Must be call on the crypto coroutine thread - * - * @param megolmSessionsData megolm sessions. - * @param fromBackup true if the imported keys are already backed up on the server. - * @param progressListener the progress listener - * @return import room keys result - */ - @WorkerThread - fun handle( - megolmSessionsData: List, - fromBackup: Boolean, - progressListener: ProgressListener? - ): ImportRoomKeysResult { - val t0 = clock.epochMillis() - - val totalNumbersOfKeys = megolmSessionsData.size - var lastProgress = 0 - var totalNumbersOfImportedKeys = 0 - - progressListener?.onProgress(0, 100) - val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) - - megolmSessionsData.forEachIndexed { cpt, megolmSessionData -> - val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) - - if (null != decrypting) { - try { - val sessionId = megolmSessionData.sessionId - Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId") - - totalNumbersOfImportedKeys++ - - // cancel any outstanding room key requests for this session - - Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}") - outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded( - megolmSessionData.sessionId ?: "", - megolmSessionData.roomId ?: "", - megolmSessionData.senderKey ?: "", - tryOrNull { - olmInboundGroupSessionWrappers - .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId } - ?.session?.firstKnownIndex - ?.toInt() - } ?: 0 - ) - - // Have another go at decrypting events sent with this session - when (decrypting) { - is MXMegolmDecryption -> { - decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!) - } - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed") - } - } - - if (progressListener != null) { - val progress = 100 * (cpt + 1) / totalNumbersOfKeys - - if (lastProgress != progress) { - lastProgress = progress - - progressListener.onProgress(progress, 100) - } - } - } - - // Do not back up the key if it comes from a backup recovery - if (fromBackup) { - cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) - } - - val t1 = clock.epochMillis() - - Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") - - return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt deleted file mode 100644 index eff213282..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertToUTF8 -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO) - -internal class MessageEncrypter @Inject constructor( - @UserId - private val userId: String, - @DeviceId - private val deviceId: String?, - private val olmDevice: MXOlmDevice -) { - /** - * Encrypt an event payload for a list of devices. - * This method must be called from the getCryptoHandler() thread. - * - * @param payloadFields fields to include in the encrypted payload. - * @param deviceInfos list of device infos to encrypt for. - * @return the content for an m.room.encrypted event. - */ - suspend fun encryptMessage(payloadFields: Content, deviceInfos: List): EncryptedMessage { - val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } - - val payloadJson = payloadFields.toMutableMap() - - payloadJson["sender"] = userId - payloadJson["sender_device"] = deviceId!! - - // Include the Ed25519 key so that the recipient knows what - // device this message came from. - // We don't need to include the curve25519 key since the - // recipient will already know this from the olm headers. - // When combined with the device keys retrieved from the - // homeserver signed by the ed25519 key this proves that - // the curve25519 key and the ed25519 key are owned by - // the same device. - payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!) - - val ciphertext = mutableMapOf() - - for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { - val sessionId = olmDevice.getSessionId(deviceKey) - - if (!sessionId.isNullOrEmpty()) { - Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey") - - payloadJson["recipient"] = deviceInfo.userId - payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!) - - val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!! - } - } - - return EncryptedMessage( - algorithm = MXCRYPTO_ALGORITHM_OLM, - senderKey = olmDevice.deviceCurve25519Key, - cipherText = ciphertext - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt deleted file mode 100644 index 6028b1a5a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.actions - -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import timber.log.Timber -import javax.inject.Inject - -internal class SetDeviceVerificationAction @Inject constructor( - private val cryptoStore: IMXCryptoStore, - @UserId private val userId: String, - private val defaultKeysBackupService: DefaultKeysBackupService -) { - - fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { - val device = cryptoStore.getUserDevice(userId, deviceId) - - // Sanity check - if (null == device) { - Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId") - return - } - - if (device.isVerified != trustLevel.isVerified()) { - if (userId == this.userId) { - // If one of the user's own devices is being marked as verified / unverified, - // check the key backup status, since whether or not we use this depends on - // whether it has a signature from a verified device - defaultKeysBackupService.checkAndStartKeysBackup() - } - } - - if (device.trustLevel != trustLevel) { - device.trustLevel = trustLevel - cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt deleted file mode 100644 index d9fd5f10c..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService - -/** - * An interface for decrypting data. - */ -internal interface IMXDecrypting { - - /** - * Decrypt an event. - * - * @param event the raw event. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @return the decryption information, or an error - */ - @Throws(MXCryptoError::class) - suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult - - /** - * Handle a key event. - * - * @param event the key event. - * @param defaultKeysBackupService the keys backup service - * @param forceAccept the keys backup service - */ - fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {} -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt deleted file mode 100644 index 1454f5b48..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder - -/** - * An interface for encrypting data. - */ -internal interface IMXEncrypting { - - /** - * Encrypt an event content according to the configuration of the room. - * - * @param eventContent the content of the event. - * @param eventType the type of the event. - * @param userIds the room members the event will be sent to. - * @return the encrypted content - */ - suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content - - suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {} -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt deleted file mode 100644 index 9ec78f37c..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms - -internal interface IMXGroupEncryption { - - /** - * In Megolm, each recipient maintains a record of the ratchet value which allows - * them to decrypt any messages sent in the session after the corresponding point - * in the conversation. If this value is compromised, an attacker can similarly - * decrypt past messages which were encrypted by a key derived from the - * compromised or subsequent ratchet values. This gives 'partial' forward - * secrecy. - * - * To mitigate this issue, the application should offer the user the option to - * discard historical conversations, by winding forward any stored ratchet values, - * or discarding sessions altogether. - */ - fun discardSessionKey() - - suspend fun preshareKey(userIds: List) - - /** - * Re-shares a session key with devices if the key has already been - * sent to them. - * - * @param groupSessionId The id of the outbound session to share. - * @param userId The id of the user who owns the target device. - * @param deviceId The id of the target device. - * @param senderKey The key of the originating device for the session. - * - * @return true in case of success - */ - suspend fun reshareKey( - groupSessionId: String, - userId: String, - deviceId: String, - senderKey: String - ): Boolean -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt deleted file mode 100644 index 64bd52dd3..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import dagger.Lazy -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) - -internal class MXMegolmDecryption( - private val olmDevice: MXOlmDevice, - private val myUserId: String, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val liveEventManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, - private val cryptoConfig: MXCryptoConfig, - private val clock: Clock, -) : IMXDecrypting { - - var newSessionListener: NewSessionListener? = null - - /** - * Events which we couldn't decrypt due to unknown sessions / indexes: map from - * senderKey|sessionId to timelines to list of MatrixEvents. - */ -// private var pendingEvents: MutableMap>> = HashMap() - - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return decryptEvent(event, timeline, true) - } - - @Throws(MXCryptoError::class) - private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { - Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") - if (event.roomId.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - val encryptedEventContent = event.content.toModel() - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - - if (encryptedEventContent.senderKey.isNullOrBlank() || - encryptedEventContent.sessionId.isNullOrBlank() || - encryptedEventContent.ciphertext.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - - return runCatching { - olmDevice.decryptGroupMessage( - encryptedEventContent.ciphertext, - event.roomId, - timeline, - eventId = event.eventId.orEmpty(), - encryptedEventContent.sessionId, - encryptedEventContent.senderKey - ) - } - .fold( - { olmDecryptionResult -> - // the decryption succeeds - if (olmDecryptionResult.payload != null) { - MXEventDecryptionResult( - clearEvent = olmDecryptionResult.payload, - senderCurve25519Key = olmDecryptionResult.senderKey, - claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), - forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain - .orEmpty(), - isSafe = olmDecryptionResult.isSafe.orFalse() - ).also { - liveEventManager.get().dispatchLiveEventDecrypted(event, it) - } - } else { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) - } - }, - { throwable -> - liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable) - if (throwable is MXCryptoError.OlmError) { - // TODO Check the value of .message - if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // So we know that session, but it's ratcheted and we can't decrypt at that index - - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - // Check if partially withheld - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, - "UNKNOWN_MESSAGE_INDEX", - null - ) - } - - val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message) - val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason) - - throw MXCryptoError.Base( - MXCryptoError.ErrorType.OLM, - reason, - detailedReason - ) - } - if (throwable is MXCryptoError.Base) { - if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - // Check if it was withheld by sender to enrich error code - val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) - if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - // Encapsulate as withHeld exception - throw MXCryptoError.Base( - MXCryptoError.ErrorType.KEYS_WITHHELD, - withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } - - if (requestKeysOnFail) { - requestKeysForEvent(event) - } - } - } - throw throwable - } - ) - } - - /** - * Helper for the real decryptEvent and for _retryDecryption. If - * requestKeysOnFail is true, we'll send an m.room_key_request when we fail - * to decrypt the event due to missing megolm keys. - * - * @param event the event - */ - private fun requestKeysForEvent(event: Event) { - outgoingKeyRequestManager.requestKeyForEvent(event, false) - } - - /** - * Handle a key event. - * - * @param event the key event. - * @param defaultKeysBackupService the keys backup service - * @param forceAccept if true will force to accept the forwarded key - */ - override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) { - Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})") - var exportFormat = false - val roomKeyContent = event.getDecryptedContent()?.toModel() ?: return - - val eventSenderKey: String = event.getSenderKey() ?: return Unit.also { - Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field") - } - - // this device might not been downloaded now? - val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey) - - lateinit var sessionInitiatorSenderKey: String - val trusted: Boolean - - var keysClaimed: MutableMap = HashMap() - val forwardingCurve25519KeyChain: MutableList = ArrayList() - - if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") - return - } - if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) { - if (!cryptoStore.isKeyGossipingEnabled()) { - Timber.tag(loggerTag.value) - .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - return - } - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel() - ?: return - - forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { - forwardingCurve25519KeyChain.addAll(it) - } - - forwardingCurve25519KeyChain.add(eventSenderKey) - - exportFormat = true - sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also { - Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") - } - - if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { - Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field") - return - } - - keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key - - // checking if was requested once. - // should we check if the request is sort of active? - val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest( - roomId = forwardedRoomKeyContent.roomId.orEmpty(), - sessionId = forwardedRoomKeyContent.sessionId.orEmpty(), - algorithm = forwardedRoomKeyContent.algorithm.orEmpty(), - senderKey = forwardedRoomKeyContent.senderKey.orEmpty(), - ).isEmpty() - - trusted = false - - if (!forceAccept && wasNotRequested) { -// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty() - unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis()) - // Ignore unsolicited - Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested") - return - } - - // Check who sent the request, as we requested we have the device keys (no need to download) - val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey) - if (sessionThatIsSharing == null) { - Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey") - return - } - val isOwnDevice = myUserId == sessionThatIsSharing.userId - val isDeviceVerified = sessionThatIsSharing.isVerified - val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey - - val isLegitForward = (isOwnDevice && isDeviceVerified) || - (!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator) - - val shouldAcceptForward = forceAccept || isLegitForward - - if (!shouldAcceptForward) { - Timber.tag(loggerTag.value) - .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," + - " fromInitiator:$isFromSessionInitiator") - return - } - } else { - // It's a m.room_key so safe - trusted = true - sessionInitiatorSenderKey = eventSenderKey - Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") - // inherit the claimed ed25519 key from the setup message - keysClaimed = event.getKeysClaimed().toMutableMap() - } - - Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val addSessionResult = olmDevice.addInboundGroupSession( - sessionId = roomKeyContent.sessionId, - sessionKey = roomKeyContent.sessionKey, - roomId = roomKeyContent.roomId, - senderKey = sessionInitiatorSenderKey, - forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, - keysClaimed = keysClaimed, - exportFormat = exportFormat, - sharedHistory = roomKeyContent.getSharedKey(), - trusted = trusted - ).also { - Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it") - } - - when (addSessionResult) { - is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex - is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex - else -> null - }?.let { index -> - if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - outgoingKeyRequestManager.onRoomKeyForwarded( - sessionId = roomKeyContent.sessionId, - algorithm = roomKeyContent.algorithm ?: "", - roomId = roomKeyContent.roomId, - senderKey = sessionInitiatorSenderKey, - fromIndex = index, - fromDevice = fromDevice?.deviceId, - event = event - ) - - cryptoStore.saveIncomingForwardKeyAuditTrail( - roomId = roomKeyContent.roomId, - sessionId = roomKeyContent.sessionId, - senderKey = sessionInitiatorSenderKey, - algorithm = roomKeyContent.algorithm ?: "", - userId = event.senderId.orEmpty(), - deviceId = fromDevice?.deviceId.orEmpty(), - chainIndex = index.toLong() - ) - - // The index is used to decide if we cancel sent request or if we wait for a better key - outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index) - } - } - - if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { - Timber.tag(loggerTag.value) - .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") - defaultKeysBackupService.maybeBackupKeys() - - onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId) - } - } - - /** - * Returns boolean shared key flag, if enabled with respect to matrix configuration. - */ - private fun RoomKeyContent.getSharedKey(): Boolean { - if (!cryptoStore.isShareKeysOnInviteEnabled()) return false - return sharedHistory ?: false - } - - /** - * Check if the some messages can be decrypted with a new session. - * - * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions - * @param senderKey the session sender key - * @param sessionId the session id - */ - fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { - Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") - newSessionListener?.onNewSession(roomId, senderKey, sessionId) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt deleted file mode 100644 index 99f8bc69e..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import dagger.Lazy -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.StreamEventsManager -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class MXMegolmDecryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - @UserId private val myUserId: String, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val cryptoStore: IMXCryptoStore, - private val eventsManager: Lazy, - private val unrequestedForwardManager: UnRequestedForwardManager, - private val mxCryptoConfig: MXCryptoConfig, - private val clock: Clock, -) { - - fun create(): MXMegolmDecryption { - return MXMegolmDecryption( - olmDevice = olmDevice, - myUserId = myUserId, - outgoingKeyRequestManager = outgoingKeyRequestManager, - cryptoStore = cryptoStore, - liveEventManager = eventsManager, - unrequestedForwardManager = unrequestedForwardManager, - cryptoConfig = mxCryptoConfig, - clock = clock, - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt deleted file mode 100644 index 0b7af9f4d..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ /dev/null @@ -1,611 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.forEach -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.model.toDebugCount -import org.matrix.android.sdk.internal.crypto.model.toDebugString -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertToUTF8 -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO) - -internal class MXMegolmEncryption( - // The id of the room we will be sending to. - private val roomId: String, - private val olmDevice: MXOlmDevice, - private val defaultKeysBackupService: DefaultKeysBackupService, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val myUserId: String, - private val myDeviceId: String, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) : IMXEncrypting, IMXGroupEncryption { - - // OutboundSessionInfo. Null if we haven't yet started setting one up. Note - // that even if this is non-null, it may not be ready for use (in which - // case outboundSession.shareOperation will be non-null.) - private var outboundSession: MXOutboundSessionInfo? = null - - init { - // restore existing outbound session if any - outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId) - } - - // Default rotation periods - // TODO Make it configurable via parameters - // Session rotation periods - private var sessionRotationPeriodMsgs: Int = 100 - private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000 - - override suspend fun encryptEventContent( - eventContent: Content, - eventType: String, - userIds: List - ): Content { - val ts = clock.epochMillis() - Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") - - /** - * When using in-room messages and the room has encryption enabled, - * clients should ensure that encryption does not hinder the verification. - * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s - * unverified devices receive the keys necessary to decrypt the messages, - * even if they would normally not be given the keys to decrypt messages in the room. - */ - val shouldSendToUnverified = isVerificationEvent(eventType, eventContent) - - val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified) - - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") - Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}") - val outboundSession = ensureOutboundSession(devices.allowedDevices) - - return encryptContent(outboundSession, eventType, eventContent) - .also { - notifyWithheldForSession(devices.withHeldDevices, outboundSession) - // annoyingly we have to serialize again the saved outbound session to store message index :/ - // if not we would see duplicate message index errors - olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis") - } - } - - private fun isVerificationEvent(eventType: String, eventContent: Content) = - EventType.isVerificationEvent(eventType) || - (eventType == EventType.MESSAGE && - eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST) - - private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { - // offload to computation thread - cryptoCoroutineScope.launch(coroutineDispatchers.computation) { - mutableListOf>().apply { - devices.forEach { userId, deviceId, withheldCode -> - this.add(UserDevice(userId, deviceId) to withheldCode) - } - }.groupBy( - { it.second }, - { it.first } - ).forEach { (code, targets) -> - notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) - } - } - } - - override fun discardSessionKey() { - outboundSession = null - olmDevice.discardOutboundGroupSessionForRoom(roomId) - } - - override suspend fun preshareKey(userIds: List) { - val ts = clock.epochMillis() - Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...") - val devices = getDevicesInRoom(userIds) - val outboundSession = ensureOutboundSession(devices.allowedDevices) - - notifyWithheldForSession(devices.withHeldDevices, outboundSession) - - Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis") - } - - /** - * Prepare a new session. - * - * @return the session description - */ - private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { - Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ") - val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) - - val keysClaimedMap = mapOf( - "ed25519" to olmDevice.deviceEd25519Key!! - ) - - val sharedHistory = cryptoStore.shouldShareHistory(roomId) - Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory") - olmDevice.addInboundGroupSession( - sessionId = sessionId!!, - sessionKey = olmDevice.getSessionKey(sessionId)!!, - roomId = roomId, - senderKey = olmDevice.deviceCurve25519Key!!, - forwardingCurve25519KeyChain = emptyList(), - keysClaimed = keysClaimedMap, - exportFormat = false, - sharedHistory = sharedHistory, - trusted = true - ) - - defaultKeysBackupService.maybeBackupKeys() - - return MXOutboundSessionInfo( - sessionId = sessionId, - sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore), - clock = clock, - sharedHistory = sharedHistory - ) - } - - /** - * Ensure the outbound session. - * - * @param devicesInRoom the devices list - */ - private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { - Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId") - var session = outboundSession - if (session == null || - // Need to make a brand new session? - session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || - // Is there a room history visibility change since the last outboundSession - cryptoStore.shouldShareHistory(roomId) != session.sharedHistory || - // Determine if we have shared with anyone we shouldn't have - session.sharedWithTooManyDevices(devicesInRoom)) { - Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") - session = prepareNewSessionInRoom() - outboundSession = session - } - val safeSession = session - val shareMap = HashMap>()/* userId */ - val userIds = devicesInRoom.userIds - for (userId in userIds) { - val deviceIds = devicesInRoom.getUserDeviceIds(userId) - for (deviceId in deviceIds!!) { - val deviceInfo = devicesInRoom.getObject(userId, deviceId) - if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, deviceInfo).found) { - val devices = shareMap.getOrPut(userId) { ArrayList() } - devices.add(deviceInfo) - } - } - } - val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size } - Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})") - shareKey(safeSession, shareMap) - return safeSession - } - - /** - * Share the device key to a list of users. - * - * @param session the session info - * @param devicesByUsers the devices map - */ - private suspend fun shareKey( - session: MXOutboundSessionInfo, - devicesByUsers: Map> - ) { - // nothing to send, the task is done - if (devicesByUsers.isEmpty()) { - Timber.tag(loggerTag.value).v("shareKey() : nothing more to do") - return - } - // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) - val subMap = HashMap>() - var devicesCount = 0 - for ((userId, devices) in devicesByUsers) { - subMap[userId] = devices - devicesCount += devices.size - if (devicesCount > 100) { - break - } - } - Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") - shareUserDevicesKey(session, subMap) - val remainingDevices = devicesByUsers - subMap.keys - shareKey(session, remainingDevices) - } - - /** - * Share the device keys of a an user. - * - * @param sessionInfo the session info - * @param devicesByUser the devices map - */ - private suspend fun shareUserDevicesKey( - sessionInfo: MXOutboundSessionInfo, - devicesByUser: Map> - ) { - val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { - Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") - } - val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId) - - val payload = mapOf( - "type" to EventType.ROOM_KEY, - "content" to mapOf( - "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM, - "room_id" to roomId, - "session_id" to sessionInfo.sessionId, - "session_key" to sessionKey, - "chain_index" to chainIndex, - "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory - ) - ) - - var t0 = clock.epochMillis() - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") - - val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - Timber.tag(loggerTag.value).v( - """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms""" - .trimMargin() - ) - val contentMap = MXUsersDevicesMap() - var haveTargets = false - val userIds = results.userIds - val noOlmToNotify = mutableListOf() - for (userId in userIds) { - val devicesToShareWith = devicesByUser[userId] - for ((deviceID) in devicesToShareWith!!) { - val sessionResult = results.getObject(userId, deviceID) - if (sessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - - // MSC 2399 - // send withheld m.no_olm: an olm session could not be established. - // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld") - noOlmToNotify.add(UserDevice(userId, deviceID)) - continue - } - Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") - contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) - haveTargets = true - } - } - - // Add the devices we have shared with to session.sharedWithDevices. - // we deliberately iterate over devicesByUser (ie, the devices we - // attempted to share with) rather than the contentMap (those we did - // share with), because we don't want to try to claim a one-time-key - // for dead devices on every message. - for ((_, devicesToShareWith) in devicesByUser) { - for (deviceInfo in devicesToShareWith) { - sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) - // XXX is it needed to add it to the audit trail? - // For now decided that no, we are more interested by forward trail - } - } - - if (haveTargets) { - t0 = clock.epochMillis() - Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target") - Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(sendToDeviceParams) - } - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms") - } catch (failure: Throwable) { - // What to do here... - Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>") - } - } else { - Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") - } - - if (noOlmToNotify.isNotEmpty()) { - // XXX offload?, as they won't read the message anyhow? - notifyKeyWithHeld( - noOlmToNotify, - sessionInfo.sessionId, - olmDevice.deviceCurve25519Key, - WithHeldCode.NO_OLM - ) - } - } - - private suspend fun notifyKeyWithHeld( - targets: List, - sessionId: String, - senderKey: String?, - code: WithHeldCode - ) { - Timber.tag(loggerTag.value).d( - "notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + - " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}" - ) - val withHeldContent = RoomKeyWithHeldContent( - roomId = roomId, - senderKey = senderKey, - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - sessionId = sessionId, - codeString = code.value, - fromDevice = myDeviceId - ) - val params = SendToDeviceTask.Params( - EventType.ROOM_KEY_WITHHELD.stable, - MXUsersDevicesMap().apply { - targets.forEach { - setObject(it.userId, it.deviceId, withHeldContent) - } - } - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(params) - } - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}") - } - } - - /** - * process the pending encryptions. - */ - private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content { - // Everything is in place, encrypt all pending events - val payloadJson = HashMap() - payloadJson["room_id"] = roomId - payloadJson["type"] = eventType - payloadJson["content"] = eventContent - - // Get canonical Json from - - val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson)) - val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString) - - val map = HashMap() - map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM - map["sender_key"] = olmDevice.deviceCurve25519Key!! - map["ciphertext"] = ciphertext!! - map["session_id"] = session.sessionId - - // Include our device ID so that recipients can send us a - // m.new_device message if they don't have our session key. - map["device_id"] = myDeviceId - session.useCount++ - return map - } - - /** - * Get the list of devices which can encrypt data to. - * This method must be called in getDecryptingThreadHandler() thread. - * - * @param userIds the user ids whose devices must be checked. - * @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if - * such devices are blocked in crypto settings - */ - private suspend fun getDevicesInRoom(userIds: List, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo { - // We are happy to use a cached version here: we assume that if we already - // have a list of the user's devices, then we already share an e2e room - // with them, which means that they will have announced any new devices via - // an m.new_device. - val keys = deviceListManager.downloadKeys(userIds, false) - val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || - cryptoStore.getBlockUnverifiedDevices(roomId) - - val devicesInRoom = DeviceInRoomInfo() - val unknownDevices = MXUsersDevicesMap() - - for (userId in keys.userIds) { - val deviceIds = keys.getUserDeviceIds(userId) ?: continue - for (deviceId in deviceIds) { - val deviceInfo = keys.getObject(userId, deviceId) ?: continue - if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) { - // The device is not yet known by the user - unknownDevices.setObject(userId, deviceId, deviceInfo) - continue - } - if (deviceInfo.isBlocked) { - // Remove any blocked devices - devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED) - continue - } - - if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) { - devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED) - continue - } - - if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) { - // Don't bother sending to ourself - continue - } - devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo) - } - } - if (unknownDevices.isEmpty) { - return devicesInRoom - } else { - throw MXCryptoError.UnknownDevice(unknownDevices) - } - } - - override suspend fun reshareKey( - groupSessionId: String, - userId: String, - deviceId: String, - senderKey: String - ): Boolean { - Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId") - val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false - .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") } - - // Get the chain index of the key we previously sent this device - val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo) - if (!wasSessionSharedWithUser.found) { - // This session was never shared with this user - // Send a room key with held - notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED) - Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device") - return false - } - // if found chain index should not be null - val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false - .also { - Timber.tag(loggerTag.value).w("reshareKey: Null chain index") - } - - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - null - } - val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") - return false - } - Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}") - - val sessionHolder = try { - olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId") - return false - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys() - } ?: return false.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - return try { - sendToDeviceTask.execute(sendToDeviceParams) - Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId") - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId") - false - } - } - - @Throws - override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) { - require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" } - Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") - val userId = deviceInfo.userId - val deviceId = deviceInfo.deviceId - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm") - // process anyway? - null - } - val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys") - return - } - - val export = inboundSessionWrapper.mutex.withLock { - inboundSessionWrapper.wrapper.exportKeys() - } ?: return Unit.also { - Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}") - } - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = - withContext(coroutineDispatchers.computation) { - messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - } - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value) - .d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - withContext(coroutineDispatchers.io) { - sendToDeviceTask.execute(sendToDeviceParams) - } - } - - data class DeviceInRoomInfo( - val allowedDevices: MXUsersDevicesMap = MXUsersDevicesMap(), - val withHeldDevices: MXUsersDevicesMap = MXUsersDevicesMap() - ) - - data class UserDevice( - val userId: String, - val deviceId: String - ) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt deleted file mode 100644 index 4225d604a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService -import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class MXMegolmEncryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - private val defaultKeysBackupService: DefaultKeysBackupService, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter, - private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) { - - fun create(roomId: String): MXMegolmEncryption { - return MXMegolmEncryption( - roomId = roomId, - olmDevice = olmDevice, - defaultKeysBackupService = defaultKeysBackupService, - cryptoStore = cryptoStore, - deviceListManager = deviceListManager, - ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction, - myUserId = userId, - myDeviceId = deviceId!!, - sendToDeviceTask = sendToDeviceTask, - messageEncrypter = messageEncrypter, - warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, - coroutineDispatchers = coroutineDispatchers, - cryptoCoroutineScope = cryptoCoroutineScope, - clock = clock, - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt deleted file mode 100644 index e0caa0d9a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -internal class MXOutboundSessionInfo( - // The id of the session - val sessionId: String, - val sharedWithHelper: SharedWithHelper, - private val clock: Clock, - // When the session was created - private val creationTime: Long = clock.epochMillis(), - val sharedHistory: Boolean = false -) { - - // Number of times this session has been used - var useCount: Int = 0 - - fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { - var needsRotation = false - val sessionLifetime = clock.epochMillis() - creationTime - - if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { - Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms") - needsRotation = true - } - - return needsRotation - } - - /** - * Determine if this session has been shared with devices which it shouldn't have been. - * - * @param devicesInRoom the devices map - * @return true if we have shared the session with devices which aren't in devicesInRoom. - */ - fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { - val sharedWithDevices = sharedWithHelper.sharedWithDevices() - val userIds = sharedWithDevices.userIds - - for (userId in userIds) { - if (null == devicesInRoom.getUserDeviceIds(userId)) { - Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId") - return true - } - - val deviceIds = sharedWithDevices.getUserDeviceIds(userId) - - for (deviceId in deviceIds!!) { - if (null == devicesInRoom.getObject(userId, deviceId)) { - Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId") - return true - } - } - } - - return false - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt deleted file mode 100644 index 30fd403ce..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.megolm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore - -internal class SharedWithHelper( - private val roomId: String, - private val sessionId: String, - private val cryptoStore: IMXCryptoStore -) { - - fun sharedWithDevices(): MXUsersDevicesMap { - return cryptoStore.getSharedWithInfo(roomId, sessionId) - } - - fun markedSessionAsShared(deviceInfo: CryptoDeviceInfo, chainIndex: Int) { - cryptoStore.markedSessionAsShared( - roomId = roomId, - sessionId = sessionId, - userId = deviceInfo.userId, - deviceId = deviceInfo.deviceId, - deviceIdentityKey = deviceInfo.identityKey() ?: "", - chainIndex = chainIndex - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt index 9235cd2ab..85c48ce28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,135 +16,13 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import timber.log.Timber -import java.util.concurrent.Executors import javax.inject.Inject -import kotlin.math.abs -private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000 +// empty in rust +class UnRequestedForwardManager @Inject constructor() { -@SessionScope -internal class UnRequestedForwardManager @Inject constructor( - private val deviceListManager: DeviceListManager, -) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val scope = CoroutineScope(SupervisorJob() + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - // For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups? - private val forwardedKeysPerRoom = mutableMapOf>>() - - data class InviteInfo( - val roomId: String, - val fromMxId: String, - val timestamp: Long - ) - - data class ForwardInfo( - val event: Event, - val timestamp: Long - ) - - // roomId, local timestamp of invite - private val recentInvites = mutableListOf() - - fun close() { - try { - scope.cancel("User Terminate") - } catch (failure: Throwable) { - Timber.w(failure, "Failed to shutDown UnrequestedForwardManager") - } - } - - fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) { - Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp") - scope.launch { - sequencer.post { - if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) { - recentInvites.add( - InviteInfo( - roomId, - fromUserId, - localTimeStamp - ) - ) - } - } - } - } - - fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) { - Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp") - scope.launch { - sequencer.post { - val claimSenderId = event.senderId.orEmpty() - val senderKey = event.getSenderKey() - // we might want to download keys, as this user might not be known yet, cache is ok - val ownerMxId = - tryOrNull { - deviceListManager.downloadKeys(listOf(claimSenderId), false) - .map[claimSenderId] - ?.values - ?.firstOrNull { it.identityKey() == senderKey } - ?.userId - } - // Not sure what to do if the device has been deleted? I can't proove the mxid - if (ownerMxId == null || claimSenderId != ownerMxId) { - Timber.w("Mismatch senderId between event and olm owner") - return@post - } - - forwardedKeysPerRoom - .getOrPut(roomId) { mutableMapOf() } - .getOrPut(ownerMxId) { mutableListOf() } - .add(ForwardInfo(event, localTimeStamp)) - } - } - } - - fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List) -> Unit) { - scope.launch { - sequencer.post { - // Prune outdated invites - recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS } - val cleanUpEvents = mutableListOf>() - forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) -> - senderIdToForwardMap.forEach { (senderId, eventList) -> - // is there a matching invite in a valid timewindow? - val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId } - if (matchingInvite != null) { - Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}") - - eventList.filter { - abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS - }.map { - it.event - }.takeIf { it.isNotEmpty() }?.let { - Timber.w("Re-processing forwarded_room_key_event that was not requested after invite") - scope.launch { - handleForwards.invoke(it) - } - } - cleanUpEvents.add(roomId to senderId) - } - } - } - - cleanUpEvents.forEach { roomIdToSenderPair -> - forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear() - } - } - } + fun onInviteReceived(roomId: String, inviterId: String, epochMillis: Long) { + Timber.e("UnRequestedForwardManager not yet implemented $roomId, $inviterId, $epochMillis") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt deleted file mode 100644 index 219cadac4..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.util.convertFromUTF8 -import timber.log.Timber - -private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO) - -internal class MXOlmDecryption( - // The olm device interface - private val olmDevice: MXOlmDevice, - // the matrix userId - private val userId: String -) : - IMXDecrypting { - - @Throws(MXCryptoError::class) - override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val olmEventContent = event.content.toModel() ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_EVENT_FORMAT, - MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON - ) - } - - val cipherText = olmEventContent.ciphertext ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON - ) - } - - val senderKey = olmEventContent.senderKey ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_SENDER_KEY, - MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON - ) - } - - val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") - throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) - } - - // The message for myUser - @Suppress("UNCHECKED_CAST") - val message = messageAny as JsonDict - - val decryptedPayload = decryptMessage(message, senderKey) - - if (decryptedPayload == null) { - Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } - val payloadString = convertFromUTF8(decryptedPayload) - - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payload = adapter.fromJson(payloadString) - - if (payload == null) { - Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) - } - - val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - if (olmPayloadContent.recipient.isNullOrBlank()) { - val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") - Timber.tag(loggerTag.value).e("## decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) - } - - if (olmPayloadContent.recipient != userId) { - Timber.tag(loggerTag.value).e( - "## decryptEvent() : Event ${event.eventId}:" + - " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId" - ) - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_RECIPIENT, - String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient) - ) - } - - val recipientKeys = olmPayloadContent.recipientKeys ?: run { - Timber.tag(loggerTag.value).e( - "## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + - " property; cannot prevent unknown-key attack" - ) - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys") - ) - } - - val ed25519 = recipientKeys["ed25519"] - - if (ed25519 != olmDevice.deviceEd25519Key) { - Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, - MXCryptoError.BAD_RECIPIENT_KEY_REASON - ) - } - - if (olmPayloadContent.sender.isNullOrBlank()) { - Timber.tag(loggerTag.value) - .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.MISSING_PROPERTY, - String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender") - ) - } - - if (olmPayloadContent.sender != event.senderId) { - Timber.tag(loggerTag.value) - .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.FORWARDED_MESSAGE, - String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender) - ) - } - - if (olmPayloadContent.roomId != event.roomId) { - Timber.tag(loggerTag.value) - .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.BAD_ROOM, - String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId) - ) - } - - val keys = olmPayloadContent.keys ?: run { - Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys") - throw MXCryptoError.Base( - MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, - MXCryptoError.MISSING_CIPHER_TEXT_REASON - ) - } - - return MXEventDecryptionResult( - clearEvent = payload, - senderCurve25519Key = senderKey, - claimedEd25519Key = keys["ed25519"] - ) - } - - /** - * Attempt to decrypt an Olm message. - * - * @param message message object, with 'type' and 'body' fields. - * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. - * @return payload, if decrypted successfully. - */ - private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? { - val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) - - val messageBody = message["body"] as? String ?: return null - val messageType = when (val typeAsVoid = message["type"]) { - is Double -> typeAsVoid.toInt() - is Int -> typeAsVoid - is Long -> typeAsVoid.toInt() - else -> return null - } - - // Try each session in turn - // decryptionErrors = {}; - - val isPreKey = messageType == 0 - // we want to synchronize on prekey if not we could end up create two olm sessions - // Not very clear but it looks like the js-sdk for consistency - return if (isPreKey) { - olmDevice.mutex.withLock { - reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) - } - } else { - reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey) - } - } - - private suspend fun reallyDecryptMessage(sessionIds: List, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? { - Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions") - for (sessionId in sessionIds) { - val payload = try { - olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) - } catch (throwable: Exception) { - // As we are trying one by one, we don't really care of the error here - Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId") - null - } - - if (null != payload) { - Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") - return payload - } else { - val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) - - if (foundSession) { - // Decryption failed, but it was a prekey message matching this - // session, so it should have worked. - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") - return null - } - } - } - - if (messageType != 0) { - // not a prekey message, so it should have matched an existing session, but it - // didn't work. - - if (sessionIds.isEmpty()) { - Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions") - } else { - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") - } - - return null - } - - // prekey message which doesn't match any existing sessions: make a new - // session. - // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions? - Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey") - - val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) - - if (null == res) { - Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") - return null - } - - Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey") - - return res["payload"] - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt deleted file mode 100644 index fb70e23b0..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore - -internal class MXOlmEncryption( - private val roomId: String, - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val messageEncrypter: MessageEncrypter, - private val deviceListManager: DeviceListManager, - private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction -) : - IMXEncrypting { - - override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { - // pick the list of recipients based on the membership list. - // - // TODO there is a race condition here! What if a new user turns up - ensureSession(userIds) - val deviceInfos = ArrayList() - for (userId in userIds) { - val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty() - for (device in devices) { - val key = device.identityKey() - if (key == olmDevice.deviceCurve25519Key) { - // Don't bother setting up session to ourself - continue - } - if (device.isBlocked) { - // Don't bother setting up sessions with blocked users - continue - } - deviceInfos.add(device) - } - } - - val messageMap = mapOf( - "room_id" to roomId, - "type" to eventType, - "content" to eventContent - ) - - messageEncrypter.encryptMessage(messageMap, deviceInfos) - return messageMap.toContent() - } - - /** - * Ensure that the session. - * - * @param users the user ids list - */ - private suspend fun ensureSession(users: List) { - deviceListManager.downloadKeys(users, false) - ensureOlmSessionsForUsersAction.handle(users) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt deleted file mode 100644 index 012886203..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.algorithms.olm - -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import javax.inject.Inject - -internal class MXOlmEncryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - private val cryptoStore: IMXCryptoStore, - private val messageEncrypter: MessageEncrypter, - private val deviceListManager: DeviceListManager, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction -) { - - fun create(roomId: String): MXOlmEncryption { - return MXOlmEncryption( - roomId, - olmDevice, - cryptoStore, - messageEncrypter, - deviceListManager, - ensureOlmSessionsForUsersAction - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index cfe4681bf..d496d1478 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.api import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse @@ -24,7 +25,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse @@ -56,13 +56,11 @@ internal interface CryptoApi { suspend fun getDeviceInfo(@Path("deviceId") deviceId: String): DeviceInfo /** - * Upload device and/or one-time keys. - * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload - * + * Upload device and one-time keys. * @param body the keys to be sent. */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload") - suspend fun uploadKeys(@Body body: KeysUploadBody): KeysUploadResponse + suspend fun uploadKeys(@Body body: JsonDict): KeysUploadResponse /** * Download device keys. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt deleted file mode 100644 index 02ea94328..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.crosssigning - -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.Task -import javax.inject.Inject - -internal interface ComputeTrustTask : Task { - data class Params( - val activeMemberUserIds: List, - val isDirectRoom: Boolean - ) -} - -internal class DefaultComputeTrustTask @Inject constructor( - private val cryptoStore: IMXCryptoStore, - @UserId private val userId: String, - private val coroutineDispatchers: MatrixCoroutineDispatchers -) : ComputeTrustTask { - - override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) { - // The set of “all users” depends on the type of room: - // For regular / topic rooms, all users including yourself, are considered when decorating a room - // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (params.isDirectRoom) { - params.activeMemberUserIds.filter { it != userId } - } else { - params.activeMemberUserIds - } - - val allTrustedUserIds = listToCheck - .filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true } - - if (allTrustedUserIds.isEmpty()) { - RoomEncryptionTrustLevel.Default - } else { - // If one of the verified user as an untrusted device -> warning - // If all devices of all verified users are trusted -> green - // else -> black - allTrustedUserIds - .mapNotNull { cryptoStore.getUserDeviceList(it) } - .flatten() - .let { allDevices -> - if (getMyCrossSigningKeys() != null) { - allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } - } else { - // Legacy method - allDevices.any { !it.isVerified } - } - } - .let { hasWarning -> - if (hasWarning) { - RoomEncryptionTrustLevel.Warning - } else { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted - } else { - RoomEncryptionTrustLevel.Default - } - } - } - } - } - - private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return cryptoStore.getCrossSigningInfo(otherUserId) - } - - private fun getMyCrossSigningKeys(): MXCrossSigningInfo? { - return cryptoStore.getMyCrossSigningInfo() - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt deleted file mode 100644 index 0f29404d4..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.crosssigning - -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmPkSigning -import org.matrix.olm.OlmUtility -import javax.inject.Inject - -/** - * Holds the OlmPkSigning for cross signing. - * Can be injected without having to get the full cross signing service - */ -@SessionScope -internal class CrossSigningOlm @Inject constructor( - private val cryptoStore: IMXCryptoStore, -) { - - enum class KeyType { - SELF, - USER, - MASTER - } - - var olmUtility: OlmUtility = OlmUtility() - - var masterPkSigning: OlmPkSigning? = null - var userPkSigning: OlmPkSigning? = null - var selfSigningPkSigning: OlmPkSigning? = null - - fun release() { - olmUtility.releaseUtility() - listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } - } - - fun signObject(type: KeyType, strToSign: String): Map { - val myKeys = cryptoStore.getMyCrossSigningInfo() - val pubKey = when (type) { - KeyType.SELF -> myKeys?.selfSigningKey() - KeyType.USER -> myKeys?.userKey() - KeyType.MASTER -> myKeys?.masterKey() - }?.unpaddedBase64PublicKey - val pkSigning = when (type) { - KeyType.SELF -> selfSigningPkSigning - KeyType.USER -> userPkSigning - KeyType.MASTER -> masterPkSigning - } - if (pubKey == null || pkSigning == null) { - throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning") - } - val signature = pkSigning.sign(strToSign) - return mapOf( - "ed25519:$pubKey" to signature - ) - } - - fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map>) { - val myKeys = cryptoStore.getMyCrossSigningInfo() - ?: throw NoSuchElementException("Cross Signing not configured") - val myUserID = myKeys.userId - val pubKey = when (type) { - KeyType.SELF -> myKeys.selfSigningKey() - KeyType.USER -> myKeys.userKey() - KeyType.MASTER -> myKeys.masterKey() - }?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured") - val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me - ?.get("ed25519:$pubKey") - - require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" } - - // Check that Alice USK signature of Bob MSK is valid - olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable)) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt deleted file mode 100644 index f4796155c..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ /dev/null @@ -1,833 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.crosssigning - -import androidx.lifecycle.LiveData -import androidx.work.BackoffPolicy -import androidx.work.ExistingWorkPolicy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask -import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.TaskThread -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.logLimit -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.olm.OlmPkSigning -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@SessionScope -internal class DefaultCrossSigningService @Inject constructor( - @UserId private val myUserId: String, - @SessionId private val sessionId: String, - private val cryptoStore: IMXCryptoStore, - private val deviceListManager: DeviceListManager, - private val initializeCrossSigningTask: InitializeCrossSigningTask, - private val uploadSignaturesTask: UploadSignaturesTask, - private val taskExecutor: TaskExecutor, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val workManagerProvider: WorkManagerProvider, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val crossSigningOlm: CrossSigningOlm, - private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository -) : CrossSigningService, - DeviceListManager.UserDevicesUpdateListener { - - init { - try { - - // Try to get stored keys if they exist - cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo -> - Timber.i("## CrossSigning - Found Existing self signed keys") - Timber.i("## CrossSigning - Checking if private keys are known") - - cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> - privateKeysInfo.master - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning = pkSigning - Timber.i("## CrossSigning - Loading master key success") - } else { - Timber.w("## CrossSigning - Public master key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - privateKeysInfo.user - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning = pkSigning - Timber.i("## CrossSigning - Loading User Signing key success") - } else { - Timber.w("## CrossSigning - Public User key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - privateKeysInfo.selfSigned - ?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning = pkSigning - Timber.i("## CrossSigning - Loading Self Signing key success") - } else { - Timber.w("## CrossSigning - Public Self Signing key does not match the private key") - pkSigning.releaseSigning() - // TODO untrust? - } - } - } - - // Recover local trust in case private key are there? - setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified()) - } - } catch (e: Throwable) { - // Mmm this kind of a big issue - Timber.e(e, "Failed to initialize Cross Signing") - } - - deviceListManager.addListener(this) - } - - fun release() { - crossSigningOlm.release() - deviceListManager.removeListener(this) - } - - protected fun finalize() { - release() - } - - /** - * - Make 3 key pairs (MSK, USK, SSK) - * - Save the private keys with proper security - * - Sign the keys and upload them - * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures. - */ - override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { - Timber.d("## CrossSigning initializeCrossSigning") - - val params = InitializeCrossSigningTask.Params( - interactiveAuthInterceptor = uiaInterceptor - ) - initializeCrossSigningTask.configureWith(params) { - this.callbackThread = TaskThread.CRYPTO - this.callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.e(failure, "Error in initializeCrossSigning()") - callback.onFailure(failure) - } - - override fun onSuccess(data: InitializeCrossSigningTask.Result) { - val crossSigningInfo = MXCrossSigningInfo( - myUserId, - listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo), - true - ) - cryptoStore.setMyCrossSigningInfo(crossSigningInfo) - setUserKeysAsTrusted(myUserId, true) - cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) - crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } - crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } - crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } - - callback.onSuccess(Unit) - } - } - }.executeBy(taskExecutor) - } - - override fun onSecretMSKGossip(mskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretSSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known") - } - - mskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning?.releaseSigning() - crossSigningOlm.masterPkSigning = pkSigning - Timber.i("## CrossSigning - Loading MSK success") - cryptoStore.storeMSKPrivateKey(mskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}") - pkSigning.releaseSigning() - } - } - } - - override fun onSecretSSKGossip(sskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretSSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") - } - - sskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning?.releaseSigning() - crossSigningOlm.selfSigningPkSigning = pkSigning - Timber.i("## CrossSigning - Loading SSK success") - cryptoStore.storeSSKPrivateKey(sskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") - pkSigning.releaseSigning() - } - } - } - - override fun onSecretUSKGossip(uskPrivateKey: String) { - Timber.i("## CrossSigning - onSecretUSKGossip") - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { - Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") - } - - uskPrivateKey.fromBase64() - .let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning?.releaseSigning() - crossSigningOlm.userPkSigning = pkSigning - Timber.i("## CrossSigning - Loading USK success") - cryptoStore.storeUSKPrivateKey(uskPrivateKey) - return - } else { - Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - } - - override fun checkTrustFromPrivateKeys( - masterKeyPrivateKey: String?, - uskKeyPrivateKey: String?, - sskPrivateKey: String? - ): UserTrustResult { - val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - var masterKeyIsTrusted = false - var userKeyIsTrusted = false - var selfSignedKeyIsTrusted = false - - masterKeyPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.masterPkSigning?.releaseSigning() - crossSigningOlm.masterPkSigning = pkSigning - masterKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - uskKeyPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.userPkSigning?.releaseSigning() - crossSigningOlm.userPkSigning = pkSigning - userKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - sskPrivateKey?.fromBase64() - ?.let { privateKeySeed -> - val pkSigning = OlmPkSigning() - try { - if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - crossSigningOlm.selfSigningPkSigning?.releaseSigning() - crossSigningOlm.selfSigningPkSigning = pkSigning - selfSignedKeyIsTrusted = true - Timber.i("## CrossSigning - Loading master key success") - } else { - pkSigning.releaseSigning() - } - } catch (failure: Throwable) { - pkSigning.releaseSigning() - } - } - - if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) { - return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo) - } else { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - val checkSelfTrust = checkSelfTrust() - if (checkSelfTrust.isVerified()) { - cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey) - setUserKeysAsTrusted(myUserId, true) - } - return checkSelfTrust - } - } - - /** - * - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶ MSK - * │ - * │ │ - * │ SSK │ - * │ │ - * │ │ - * └──▶ USK ────────────┘ - * . - */ - override fun isUserTrusted(otherUserId: String): Boolean { - return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true - } - - override fun isCrossSigningVerified(): Boolean { - return checkSelfTrust().isVerified() - } - - /** - * Will not force a download of the key, but will verify signatures trust chain. - */ - override fun checkUserTrust(otherUserId: String): UserTrustResult { - Timber.v("## CrossSigning checkUserTrust for $otherUserId") - if (otherUserId == myUserId) { - return checkSelfTrust() - } - // I trust a user if I trust his master key - // I can trust the master key if it is signed by my user key - // TODO what if the master key is signed by a device key that i have verified - - // First let's get my user key - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - - return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) - } - - fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { - val myUserKey = myCrossSigningInfo?.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - if (!myCrossSigningInfo.isTrusted()) { - return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) - } - - // Let's get the other user master key - val otherMasterKey = otherInfo?.masterKey() - ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") - - val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") - - if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey") - return UserTrustResult.KeyNotSigned(otherMasterKey) - } - - // Check that Alice USK signature of Bob MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - masterKeySignaturesMadeByMyUserKey, - myUserKey.unpaddedBase64PublicKey, - otherMasterKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) - } - - return UserTrustResult.Success - } - - private fun checkSelfTrust(): UserTrustResult { - // Special case when it's me, - // I have to check that MSK -> USK -> SSK - // and that MSK is trusted (i know the private key, or is signed by a trusted device) - val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId) - - return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId)) - } - - fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { - // Special case when it's me, - // I have to check that MSK -> USK -> SSK - // and that MSK is trusted (i know the private key, or is signed by a trusted device) -// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) - - val myMasterKey = myCrossSigningInfo?.masterKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - // Is the master key trusted - // 1) check if I know the private key - val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() - ?.master - ?.fromBase64() - - var isMaterKeyTrusted = false - if (myMasterKey.trustLevel?.locallyVerified == true) { - isMaterKeyTrusted = true - } else if (masterPrivateKey != null) { - // Check if private match public - var olmPkSigning: OlmPkSigning? = null - try { - olmPkSigning = OlmPkSigning() - val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) - isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK - } catch (failure: Throwable) { - Timber.e(failure) - } - olmPkSigning?.releaseSigning() - } else { - // Maybe it's signed by a locally trusted device? - myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> - val potentialDeviceId = key.removePrefix("ed25519:") - val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) - if (potentialDevice != null && potentialDevice.isVerified) { - // Check signature validity? - try { - crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) - isMaterKeyTrusted = true - return@forEach - } catch (failure: Throwable) { - // log - Timber.w(failure, "Signature not valid?") - } - } - } - } - - if (!isMaterKeyTrusted) { - return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) - } - - val myUserKey = myCrossSigningInfo.userKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") - - if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK") - return UserTrustResult.KeyNotSigned(myUserKey) - } - - // Check that Alice USK signature of Alice MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - userKeySignaturesMadeByMyMasterKey, - myMasterKey.unpaddedBase64PublicKey, - myUserKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) - } - - val mySSKey = myCrossSigningInfo.selfSigningKey() - ?: return UserTrustResult.CrossSigningNotConfigured(myUserId) - - val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures - ?.get(myUserId) // Signatures made by me - ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") - - if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK") - return UserTrustResult.KeyNotSigned(mySSKey) - } - - // Check that Alice USK signature of Alice MSK is valid - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - ssKeySignaturesMadeByMyMasterKey, - myMasterKey.unpaddedBase64PublicKey, - mySSKey.canonicalSignable() - ) - } catch (failure: Throwable) { - return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) - } - - return UserTrustResult.Success - } - - override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return cryptoStore.getCrossSigningInfo(otherUserId) - } - - override fun getLiveCrossSigningKeys(userId: String): LiveData> { - return cryptoStore.getLiveCrossSigningInfo(userId) - } - - override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { - return cryptoStore.getMyCrossSigningInfo() - } - - override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return cryptoStore.getCrossSigningPrivateKeys() - } - - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - return cryptoStore.getLiveCrossSigningPrivateKeys() - } - - override fun canCrossSign(): Boolean { - return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && - cryptoStore.getCrossSigningPrivateKeys()?.user != null - } - - override fun allPrivateKeysKnown(): Boolean { - return checkSelfTrust().isVerified() && - cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse() - } - - override fun trustUser(otherUserId: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - Mark user $otherUserId as trusted ") - // We should have this user keys - val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() - if (otherMasterKeys == null) { - callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) - return@launch - } - val myKeys = getUserCrossSigningKeys(myUserId) - if (myKeys == null) { - callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) - return@launch - } - val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey - if (userPubKey == null || crossSigningOlm.userPkSigning == null) { - callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) - return@launch - } - - // Sign the other MasterKey with our UserSigning key - val newSignature = JsonCanonicalizer.getCanonicalJson( - Map::class.java, - otherMasterKeys.signalableJSONDictionary() - ).let { crossSigningOlm.userPkSigning?.sign(it) } - - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("## CrossSigning - Failed to sign")) - return@launch - } - - cryptoStore.setUserKeysAsTrusted(otherUserId, true) - - Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK") - val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature)) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) - - // Local echo for device cross trust, to avoid having to wait for a notification of key change - cryptoStore.getUserDeviceList(otherUserId)?.forEach { device -> - val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } - } - } - - override fun markMyMasterKeyAsTrusted() { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - checkSelfTrust() - // re-verify all trusts - onUsersDeviceUpdate(listOf(myUserId)) - } - } - - override fun trustDevice(deviceId: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - // This device should be yours - val device = cryptoStore.getUserDevice(myUserId, deviceId) - if (device == null) { - callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) - return@launch - } - - val myKeys = getUserCrossSigningKeys(myUserId) - if (myKeys == null) { - callback.onFailure(Throwable("CrossSigning is not setup for this account")) - return@launch - } - - val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey - if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) { - callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) - return@launch - } - - // Sign with self signing - val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable()) - - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("Failed to sign")) - return@launch - } - val toUpload = device.copy( - signatures = mapOf( - myUserId - to - mapOf( - "ed25519:$ssPubKey" to newSignature - ) - ) - ) - - val uploadQuery = UploadSignatureQueryBuilder() - .withDeviceInfo(toUpload) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) - } - } - - override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { - val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) - ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) - - val myKeys = getUserCrossSigningKeys(myUserId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) - - if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) - - val otherKeys = getUserCrossSigningKeys(otherUserId) - ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId)) - - // TODO should we force verification ? - if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) - - // Check if the trust chain is valid - /* - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶MSK - * │ - * │ │ │ - * │ SSK │ └──▶ SSK ──────────────────┐ - * │ │ │ - * │ │ USK │ - * └──▶ USK ────────────┘ (not visible by │ - * Alice) │ - * ▼ - * ┌──────────────┐ - * │ BOB's Device │ - * └──────────────┘ - */ - - val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") - ?: return legacyFallbackTrust( - locallyTrusted, - DeviceTrustResult.MissingDeviceSignature( - otherDeviceId, otherKeys.selfSigningKey() - ?.unpaddedBase64PublicKey - ?: "" - ) - ) - - // Check bob's device is signed by bob's SSK - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - otherSSKSignature, - otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, - otherDevice.canonicalSignable() - ) - } catch (e: Throwable) { - return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) - } - - return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) - } - - fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { - val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() - myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId)) - - if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) - - otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId)) - - // TODO should we force verification ? - if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) - - // Check if the trust chain is valid - /* - * ┏━━━━━━━━┓ ┏━━━━━━━━┓ - * ┃ ALICE ┃ ┃ BOB ┃ - * ┗━━━━━━━━┛ ┗━━━━━━━━┛ - * MSK ┌────────────▶MSK - * │ - * │ │ │ - * │ SSK │ └──▶ SSK ──────────────────┐ - * │ │ │ - * │ │ USK │ - * └──▶ USK ────────────┘ (not visible by │ - * Alice) │ - * ▼ - * ┌──────────────┐ - * │ BOB's Device │ - * └──────────────┘ - */ - - val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") - ?: return legacyFallbackTrust( - locallyTrusted, - DeviceTrustResult.MissingDeviceSignature( - otherDevice.deviceId, otherKeys.selfSigningKey() - ?.unpaddedBase64PublicKey - ?: "" - ) - ) - - // Check bob's device is signed by bob's SSK - try { - crossSigningOlm.olmUtility.verifyEd25519Signature( - otherSSKSignature, - otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, - otherDevice.canonicalSignable() - ) - } catch (e: Throwable) { - return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e)) - } - - return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) - } - - private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { - return if (locallyTrusted == true) { - DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) - } else { - crossSignTrustFail - } - } - - override fun onUsersDeviceUpdate(userIds: List) { - Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}") - checkTrustAndAffectedRoomShields(userIds) - } - - fun checkTrustAndAffectedRoomShields(userIds: List) { - Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}") - val workerParams = UpdateTrustWorker.Params( - sessionId = sessionId, - filename = updateTrustWorkerDataRepository.createParam(userIds) - ) - val workerData = WorkerParamsFactory.toData(workerParams) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setInputData(workerData) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - } - - private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { - val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() - cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) - // If it's me, recheck trust of all users and devices? - val users = ArrayList() - if (otherUserId == myUserId && currentTrust != trusted) { - // notify key requester - outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) - cryptoStore.updateUsersTrust { - users.add(it) - checkUserTrust(it).isVerified() - } - - users.forEach { - cryptoStore.getUserDeviceList(it)?.forEach { device -> - val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt deleted file mode 100644 index 16098e521..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.crosssigning - -import android.util.Base64 -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import timber.log.Timber - -internal fun CryptoDeviceInfo.canonicalSignable(): String { - return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) -} - -internal fun CryptoCrossSigningKey.canonicalSignable(): String { - return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) -} - -/** - * Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source - */ -internal fun String.fromBase64Safe(): ByteArray? { - return try { - Base64.decode(this, Base64.DEFAULT) - } catch (throwable: Throwable) { - Timber.e(throwable, "Unable to decode base64 string") - null - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index fffc6707d..70c304f7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,41 +19,13 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import android.content.Context import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.kotlin.where -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult -import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified -import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified -import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields -import org.matrix.android.sdk.internal.database.awaitTransaction -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.util.logLimit import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber import javax.inject.Inject +// THis is not used in rust crypto internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { @@ -68,308 +40,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses val filename: String? = null ) : SessionWorkerParams - @Inject lateinit var crossSigningService: DefaultCrossSigningService - - // It breaks the crypto store contract, but we need to batch things :/ - @CryptoDatabase - @Inject lateinit var cryptoRealmConfiguration: RealmConfiguration - - @SessionDatabase - @Inject lateinit var sessionRealmConfiguration: RealmConfiguration - - @UserId - @Inject lateinit var myUserId: String - @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository - // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater - @Inject lateinit var cryptoStore: IMXCryptoStore - override fun injectWith(injector: SessionComponent) { injector.inject(this) } override suspend fun doSafeWork(params: Params): Result { - val userList = params.filename + params.filename ?.let { updateTrustWorkerDataRepository.getParam(it) } ?.userIds ?: params.updatedUserIds.orEmpty() - // List should not be empty, but let's avoid go further in case of empty list - if (userList.isNotEmpty()) { - // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, - // or a new device?) So we check all again :/ - Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}") - updateTrust(userList) - } - cleanup(params) return Result.success() } - private suspend fun updateTrust(userListParam: List) { - var userList = userListParam - var myCrossSigningInfo: MXCrossSigningInfo? = null - - // First we check that the users MSK are trusted by mine - // After that we check the trust chain for each devices of each users - awaitTransaction(cryptoRealmConfiguration) { cryptoRealm -> - // By mapping here to model, this object is not live - // I should update it if needed - myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId) - - var myTrustResult: UserTrustResult? = null - - if (userList.contains(myUserId)) { - Timber.d("## CrossSigning - Clear all trust as a change on my user was detected") - // i am in the list.. but i don't know exactly the delta of change :/ - // If it's my cross signing keys we should refresh all trust - // do it anyway ? - userList = cryptoRealm.where(CrossSigningInfoEntity::class.java) - .findAll() - .mapNotNull { it.userId } - - // check right now my keys and mark it as trusted as other trust depends on it - val myDevices = cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, myUserId) - .findFirst() - ?.devices - ?.map { CryptoMapper.mapToModel(it) } - - myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices) - updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified()) - // update model reference - myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId) - } - - val otherInfos = userList.associateWith { userId -> - getCrossSigningInfo(cryptoRealm, userId) - } - - val trusts = otherInfos.mapValues { entry -> - when (entry.key) { - myUserId -> myTrustResult - else -> { - crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also { - Timber.v("## CrossSigning - user:${entry.key} result:$it") - } - } - } - } - - // TODO! if it's me and my keys has changed... I have to reset trust for everyone! - // i have all the new trusts, update DB - trusts.forEach { - val verified = it.value?.isVerified() == true - Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified") - updateCrossSigningKeysTrust(cryptoRealm, it.key, verified) - } - - // Ok so now we have to check device trust for all these users.. - Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}") - trusts.keys.forEach { userId -> - val devicesEntities = cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - - val trustMap = devicesEntities?.associateWith { device -> - // get up to date from DB has could have been updated - val otherInfo = getCrossSigningInfo(cryptoRealm, userId) - crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device)) - } - - // Update trust if needed - devicesEntities?.forEach { device -> - val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified() - Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}") - if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) { - Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") - // need to save - val trustEntity = device.trustLevelEntity - if (trustEntity == null) { - device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = false - it.crossSignedVerified = crossSignedVerified - } - } else { - trustEntity.crossSignedVerified = crossSignedVerified - } - } - } - } - } - - // So Cross Signing keys trust is updated, device trust is updated - // We can now update room shields? in the session DB? - updateTrustStep2(userList, myCrossSigningInfo) - } - - private suspend fun updateTrustStep2(userList: List, myCrossSigningInfo: MXCrossSigningInfo?) { - Timber.d("## CrossSigning - Updating shields for impacted rooms...") - awaitTransaction(sessionRealmConfiguration) { sessionRealm -> - Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction") - Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm -> - sessionRealm.where(RoomMemberSummaryEntity::class.java) - .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) - .distinct(RoomMemberSummaryEntityFields.ROOM_ID) - .findAll() - .map { it.roomId } - .also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") } - .forEach { roomId -> - RoomSummaryEntity.where(sessionRealm, roomId) - .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true) - .findFirst() - ?.let { roomSummary -> - Timber.v("## CrossSigning - Check shield state for room $roomId") - val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds() - try { - val updatedTrust = computeRoomShield( - myCrossSigningInfo, - cryptoRealm, - allActiveRoomMembers, - roomSummary - ) - if (roomSummary.roomEncryptionTrustLevel != updatedTrust) { - Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust") - roomSummary.roomEncryptionTrustLevel = updatedTrust - } - } catch (failure: Throwable) { - Timber.e(failure) - } - } - } - } - } - Timber.d("## CrossSigning - Updating shields for impacted rooms - END") - } - - private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? { - return cryptoRealm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - ?.let { mapCrossSigningInfoEntity(it) } - } - private fun cleanup(params: Params) { params.filename ?.let { updateTrustWorkerDataRepository.delete(it) } } - private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) { - cryptoRealm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - ?.let { userKeyInfo -> - userKeyInfo - .crossSigningKeys - .forEach { key -> - // optimization to avoid trigger updates when there is no change.. - if (key.trustLevelEntity?.isVerified() != verified) { - Timber.d("## CrossSigning - Trust change for $userId : $verified") - val level = key.trustLevelEntity - if (level == null) { - key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also { - it.locallyVerified = verified - it.crossSignedVerified = verified - } - } else { - level.locallyVerified = verified - level.crossSignedVerified = verified - } - } - } - if (verified) { - userKeyInfo.wasUserVerifiedOnce = true - } - } - } - - private fun computeRoomShield( - myCrossSigningInfo: MXCrossSigningInfo?, - cryptoRealm: Realm, - activeMemberUserIds: List, - roomSummaryEntity: RoomSummaryEntity - ): RoomEncryptionTrustLevel { - Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}") - // The set of “all users” depends on the type of room: - // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room - // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room - val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) { - activeMemberUserIds.filter { it != myUserId } - } else { - activeMemberUserIds - } - - val allTrustedUserIds = listToCheck - .filter { userId -> - getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true - } - - val resetTrust = listToCheck - .filter { userId -> - val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId) - crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true - } - - return if (allTrustedUserIds.isEmpty()) { - if (resetTrust.isEmpty()) { - RoomEncryptionTrustLevel.Default - } else { - RoomEncryptionTrustLevel.Warning - } - } else { - // If one of the verified user as an untrusted device -> warning - // If all devices of all verified users are trusted -> green - // else -> black - allTrustedUserIds - .mapNotNull { userId -> - cryptoRealm.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { CryptoMapper.mapToModel(it) } - } - .flatten() - .let { allDevices -> - Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}") - if (myCrossSigningInfo != null) { - allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } - } else { - // Legacy method - allDevices.any { !it.isVerified } - } - } - .let { hasWarning -> - if (hasWarning) { - RoomEncryptionTrustLevel.Warning - } else { - if (resetTrust.isEmpty()) { - if (listToCheck.size == allTrustedUserIds.size) { - // all users are trusted and all devices are verified - RoomEncryptionTrustLevel.Trusted - } else { - RoomEncryptionTrustLevel.Default - } - } else { - RoomEncryptionTrustLevel.Warning - } - } - } - } - } - - private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { - val userId = xsignInfo.userId ?: "" - return MXCrossSigningInfo( - userId = userId, - crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { - crossSigningKeysMapper.map(userId, it) - }, - wasTrustedOnce = xsignInfo.wasUserVerifiedOnce - ) - } - override fun buildErrorParams(params: Params, message: String): Params { return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt index 0878a9f76..d9207d05b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt @@ -28,7 +28,10 @@ import javax.inject.Inject @JsonClass(generateAdapter = true) internal data class UpdateTrustWorkerData( @Json(name = "userIds") - val userIds: List + val userIds: List, + // When we just need to refresh the room shield (no change on user keys, but a membership change) + @Json(name = "roomIds") + val roomIds: List? = null ) internal class UpdateTrustWorkerDataRepository @Inject constructor( @@ -38,12 +41,12 @@ internal class UpdateTrustWorkerDataRepository @Inject constructor( private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java) // Return the path of the created file - fun createParam(userIds: List): String { + fun createParam(userIds: List, roomIds: List? = null): String { val filename = "${UUID.randomUUID()}.json" workingDirectory.mkdirs() val file = File(workingDirectory, filename) - UpdateTrustWorkerData(userIds = userIds) + UpdateTrustWorkerData(userIds = userIds, roomIds = roomIds) .let { jsonAdapter.toJson(it) } .let { file.writeText(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt deleted file mode 100644 index e8700b780..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ /dev/null @@ -1,1561 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.keysbackup - -import android.os.Handler -import android.os.Looper -import androidx.annotation.UiThread -import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.MatrixConfiguration -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixError -import org.matrix.android.sdk.api.listeners.ProgressListener -import org.matrix.android.sdk.api.listeners.StepProgressListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.util.awaitCallback -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.ObjectSigner -import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm -import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData -import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.foldToCallback -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.TaskThread -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmException -import org.matrix.olm.OlmPkDecryption -import org.matrix.olm.OlmPkEncryption -import org.matrix.olm.OlmPkMessage -import timber.log.Timber -import java.security.InvalidParameterException -import javax.inject.Inject -import kotlin.random.Random - -/** - * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) - * to the user's homeserver. - */ -@SessionScope -internal class DefaultKeysBackupService @Inject constructor( - @UserId private val userId: String, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val olmDevice: MXOlmDevice, - private val objectSigner: ObjectSigner, - private val crossSigningOlm: CrossSigningOlm, - // Actions - private val megolmSessionDataImporter: MegolmSessionDataImporter, - // Tasks - private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, - private val deleteBackupTask: DeleteBackupTask, - private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, - private val getKeysBackupVersionTask: GetKeysBackupVersionTask, - private val getRoomSessionDataTask: GetRoomSessionDataTask, - private val getRoomSessionsDataTask: GetRoomSessionsDataTask, - private val getSessionsDataTask: GetSessionsDataTask, - private val storeSessionDataTask: StoreSessionsDataTask, - private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, - // Task executor - private val taskExecutor: TaskExecutor, - private val matrixConfiguration: MatrixConfiguration, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope -) : KeysBackupService { - - private val uiHandler = Handler(Looper.getMainLooper()) - - private val keysBackupStateManager = KeysBackupStateManager(uiHandler) - - // The backup version - override var keysBackupVersion: KeysVersionResult? = null - private set - - // The backup key being used. - private var backupOlmPkEncryption: OlmPkEncryption? = null - - private var backupAllGroupSessionsCallback: MatrixCallback? = null - - private var keysBackupStateListener: KeysBackupStateListener? = null - - override fun isEnabled(): Boolean = keysBackupStateManager.isEnabled - - override fun isStuck(): Boolean = keysBackupStateManager.isStuck - - override fun getState(): KeysBackupState = keysBackupStateManager.state - - override fun addListener(listener: KeysBackupStateListener) { - keysBackupStateManager.addListener(listener) - } - - override fun removeListener(listener: KeysBackupStateListener) { - keysBackupStateManager.removeListener(listener) - } - - override fun prepareKeysBackupVersion( - password: String?, - progressListener: ProgressListener?, - callback: MatrixCallback - ) { - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - try { - val olmPkDecryption = OlmPkDecryption() - val signalableMegolmBackupAuthData = if (password != null) { - // Generate a private key from the password - val backgroundProgressListener = if (progressListener == null) { - null - } else { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - try { - progressListener.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "prepareKeysBackupVersion: onProgress failure") - } - } - } - } - } - val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) - SignalableMegolmBackupAuthData( - publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), - privateKeySalt = generatePrivateKeyResult.salt, - privateKeyIterations = generatePrivateKeyResult.iterations - ) - } else { - val publicKey = olmPkDecryption.generateKey() - SignalableMegolmBackupAuthData( - publicKey = publicKey - ) - } - - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) - - val signatures = mutableMapOf>() - - val deviceSignature = objectSigner.signObject(canonicalJson) - deviceSignature.forEach { (userID, content) -> - signatures[userID] = content.toMutableMap() - } - - // If we have cross signing add signature, will throw if cross signing not properly configured - try { - val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson) - signatures[credentials.userId]?.putAll(crossSign) - } catch (failure: Throwable) { - // ignore and log - Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys") - } - - val signedMegolmBackupAuthData = MegolmBackupAuthData( - publicKey = signalableMegolmBackupAuthData.publicKey, - privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, - privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, - signatures = signatures - ) - val creationInfo = MegolmBackupCreationInfo( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, - authData = signedMegolmBackupAuthData, - recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) - ) - uiHandler.post { - callback.onSuccess(creationInfo) - } - } catch (failure: Throwable) { - uiHandler.post { - callback.onFailure(failure) - } - } - } - } - - override fun createKeysBackupVersion( - keysBackupCreationInfo: MegolmBackupCreationInfo, - callback: MatrixCallback - ) { - @Suppress("UNCHECKED_CAST") - val createKeysBackupVersionBody = CreateKeysBackupVersionBody( - algorithm = keysBackupCreationInfo.algorithm, - authData = keysBackupCreationInfo.authData.toJsonDict() - ) - - keysBackupStateManager.state = KeysBackupState.Enabling - - createKeysBackupVersionTask - .configureWith(createKeysBackupVersionBody) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: KeysVersion) { - // Reset backup markers. - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - // move tx out of UI thread - cryptoStore.resetBackupMarkers() - } - - val keyBackupVersion = KeysVersionResult( - algorithm = createKeysBackupVersionBody.algorithm, - authData = createKeysBackupVersionBody.authData, - version = data.version, - // We can consider that the server does not have keys yet - count = 0, - hash = "" - ) - - enableKeysBackup(keyBackupVersion) - - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - keysBackupStateManager.state = KeysBackupState.Disabled - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) - } - - override fun deleteBackup(version: String, callback: MatrixCallback?) { - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeysBackupVersion so this is symmetrical). - if (keysBackupVersion != null && version == keysBackupVersion?.version) { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Unknown - } - - deleteBackupTask - .configureWith(DeleteBackupTask.Params(version)) { - this.callback = object : MatrixCallback { - private fun eventuallyRestartBackup() { - // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (getState() == KeysBackupState.Unknown) { - checkAndStartKeysBackup() - } - } - - override fun onSuccess(data: Unit) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onSuccess(Unit) } - } - - override fun onFailure(failure: Throwable) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onFailure(failure) } - } - } - } - .executeBy(taskExecutor) - } - } - - override fun canRestoreKeys(): Boolean { - // Server contains more keys than locally - val totalNumberOfKeysLocally = getTotalNumbersOfKeys() - - val keysBackupData = cryptoStore.getKeysBackupData() - - val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1 - // Not used for the moment - // val hashServer = keysBackupData?.backupLastServerHash - - return when { - totalNumberOfKeysLocally < totalNumberOfKeysServer -> { - // Server contains more keys than this device - true - } - totalNumberOfKeysLocally == totalNumberOfKeysServer -> { - // Same number, compare hash? - // TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment - false - } - else -> false - } - } - - override fun getTotalNumbersOfKeys(): Int { - return cryptoStore.inboundGroupSessionsCount(false) - } - - override fun getTotalNumbersOfBackedUpKeys(): Int { - return cryptoStore.inboundGroupSessionsCount(true) - } - - override fun backupAllGroupSessions( - progressListener: ProgressListener?, - callback: MatrixCallback? - ) { - if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { - callback?.onFailure(Throwable("Backup not enabled")) - return - } - // Get a status right now - getBackupProgress(object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - // Reset previous listeners if any - resetBackupAllGroupSessionsListeners() - Timber.v("backupAllGroupSessions: backupProgress: $progress/$total") - try { - progressListener?.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "backupAllGroupSessions: onProgress failure") - } - - if (progress == total) { - Timber.v("backupAllGroupSessions: complete") - callback?.onSuccess(Unit) - return - } - - backupAllGroupSessionsCallback = callback - - // Listen to `state` change to determine when to call onBackupProgress and onComplete - keysBackupStateListener = object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - getBackupProgress(object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - try { - progressListener?.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "backupAllGroupSessions: onProgress failure 2") - } - - // If backup is finished, notify the main listener - if (getState() === KeysBackupState.ReadyToBackUp) { - backupAllGroupSessionsCallback?.onSuccess(Unit) - resetBackupAllGroupSessionsListeners() - } - } - }) - } - }.also { keysBackupStateManager.addListener(it) } - - backupKeys() - } - }) - } - - override fun getKeysBackupTrust( - keysBackupVersion: KeysVersionResult, - callback: MatrixCallback - ) { - // TODO Validate with François that this is correct - object : Task { - override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust { - return getKeysBackupTrustBg(params) - } - } - .configureWith(keysBackupVersion) { - this.callback = callback - this.executionThread = TaskThread.COMPUTATION - } - .executeBy(taskExecutor) - } - - /** - * Check trust on a key backup version. - * This has to be called on background thread. - * - * @param keysBackupVersion the backup version to check. - * @return a KeysBackupVersionTrust object - */ - @WorkerThread - private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { - val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() - - if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) { - Timber.v("getKeysBackupTrust: Key backup is absent or missing required data") - return KeysBackupVersionTrust(usable = false) - } - - val mySigs = authData.signatures[userId] - if (mySigs.isNullOrEmpty()) { - Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") - return KeysBackupVersionTrust(usable = false) - } - - var keysBackupVersionTrustIsUsable = false - val keysBackupVersionTrustSignatures = mutableListOf() - - for ((keyId, mySignature) in mySigs) { - // XXX: is this how we're supposed to get the device id? - var deviceOrCrossSigningKeyId: String? = null - val components = keyId.split(":") - if (components.size == 2) { - deviceOrCrossSigningKeyId = components[1] - } - - // Let's check if it's my master key - val myMSKPKey = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.unpaddedBase64PublicKey - if (deviceOrCrossSigningKeyId == myMSKPKey) { - // we have to check if we can trust - - var isSignatureValid = false - try { - crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures) - isSignatureValid = true - } catch (failure: Throwable) { - Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK") - } - val mskTrusted = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.isVerified() == true - if (isSignatureValid && mskTrusted) { - keysBackupVersionTrustIsUsable = true - } - val signature = KeysBackupVersionTrustSignature.UserSignature( - keyId = deviceOrCrossSigningKeyId, - cryptoCrossSigningKey = cryptoStore.getMyCrossSigningInfo()?.masterKey(), - valid = isSignatureValid - ) - - keysBackupVersionTrustSignatures.add(signature) - } else if (deviceOrCrossSigningKeyId != null) { - val device = cryptoStore.getUserDevice(userId, deviceOrCrossSigningKeyId) - var isSignatureValid = false - - if (device == null) { - Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId") - } else { - val fingerprint = device.fingerprint() - if (fingerprint != null) { - try { - olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature) - isSignatureValid = true - } catch (e: OlmException) { - Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}") - } - } - - if (isSignatureValid && device.isVerified) { - keysBackupVersionTrustIsUsable = true - } - } - - val signature = KeysBackupVersionTrustSignature.DeviceSignature( - deviceId = deviceOrCrossSigningKeyId, - device = device, - valid = isSignatureValid, - ) - keysBackupVersionTrustSignatures.add(signature) - } - } - - return KeysBackupVersionTrust( - usable = keysBackupVersionTrustIsUsable, - signatures = keysBackupVersionTrustSignatures - ) - } - - override fun trustKeysBackupVersion( - keysBackupVersion: KeysVersionResult, - trust: Boolean, - callback: MatrixCallback - ) { - Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") - - // Get auth data to update it - val authData = getMegolmBackupAuthData(keysBackupVersion) - - if (authData == null) { - Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") - uiHandler.post { - callback.onFailure(IllegalArgumentException("Missing element")) - } - } else { - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { - // Get current signatures, or create an empty set - val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() - - if (trust) { - // Add current device signature - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) - - val deviceSignatures = objectSigner.signObject(canonicalJson) - - deviceSignatures[userId]?.forEach { entry -> - myUserSignatures[entry.key] = entry.value - } - } else { - // Remove current device signature - myUserSignatures.remove("ed25519:${credentials.deviceId}") - } - - // Create an updated version of KeysVersionResult - val newMegolmBackupAuthData = authData.copy() - - val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap() - newSignatures[userId] = myUserSignatures - - val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy( - signatures = newSignatures - ) - - @Suppress("UNCHECKED_CAST") - UpdateKeysBackupVersionBody( - algorithm = keysBackupVersion.algorithm, - authData = newMegolmBackupAuthDataWithNewSignature.toJsonDict(), - version = keysBackupVersion.version - ) - } - - // And send it to the homeserver - updateKeysBackupVersionTask - .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version, updateKeysBackupVersionBody)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - // Relaunch the state machine on this updated backup version - val newKeysBackupVersion = KeysVersionResult( - algorithm = keysBackupVersion.algorithm, - authData = updateKeysBackupVersionBody.authData, - version = keysBackupVersion.version, - hash = keysBackupVersion.hash, - count = keysBackupVersion.count - ) - - checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - - uiHandler.post { - callback.onSuccess(data) - } - } - - override fun onFailure(failure: Throwable) { - uiHandler.post { - callback.onFailure(failure) - } - } - } - } - .executeBy(taskExecutor) - } - } - } - - override fun trustKeysBackupVersionWithRecoveryKey( - keysBackupVersion: KeysVersionResult, - recoveryKey: String, - callback: MatrixCallback - ) { - Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) - - if (!isValid) { - Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") - uiHandler.post { - callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) - } - } else { - trustKeysBackupVersion(keysBackupVersion, true, callback) - } - } - } - - override fun trustKeysBackupVersionWithPassphrase( - keysBackupVersion: KeysVersionResult, - password: String, - callback: MatrixCallback - ) { - Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) - - if (recoveryKey == null) { - Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") - uiHandler.post { - callback.onFailure(IllegalArgumentException("Missing element")) - } - } else { - // Check trust using the recovery key - trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) - } - } - } - - fun onSecretKeyGossip(secret: String) { - Timber.i("## CrossSigning - onSecretKeyGossip") - - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - try { - val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult() - ?: return@launch Unit.also { - Timber.d("Failed to get backup last version") - } - val recoveryKey = computeRecoveryKey(secret.fromBase64()) - if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - // we don't want to start immediately downloading all as it can take very long - withContext(coroutineDispatchers.crypto) { - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) - } - Timber.i("onSecretKeyGossip: saved valid backup key") - } else { - Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") - } - } catch (failure: Throwable) { - Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") - } - } - } - - /** - * Get public key from a Recovery key. - * - * @param recoveryKey the recovery key - * @return the corresponding public key, from Olm - */ - @WorkerThread - private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? { - // Extract the primary key - val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) - - if (privateKey == null) { - Timber.w("pkPublicKeyFromRecoveryKey: private key is null") - - return null - } - - // Built the PK decryption with it - val pkPublicKey: String - - try { - val decryption = OlmPkDecryption() - pkPublicKey = decryption.setPrivateKey(privateKey) - } catch (e: OlmException) { - return null - } - - return pkPublicKey - } - - private fun resetBackupAllGroupSessionsListeners() { - backupAllGroupSessionsCallback = null - - keysBackupStateListener?.let { - keysBackupStateManager.removeListener(it) - } - - keysBackupStateListener = null - } - - override fun getBackupProgress(progressListener: ProgressListener) { - val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) - val total = cryptoStore.inboundGroupSessionsCount(false) - - progressListener.onProgress(backedUpKeys, total) - } - - override fun restoreKeysWithRecoveryKey( - keysVersionResult: KeysVersionResult, - recoveryKey: String, - roomId: String?, - sessionId: String?, - stepProgressListener: StepProgressListener?, - callback: MatrixCallback - ) { - Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - runCatching { - val decryption = withContext(coroutineDispatchers.computation) { - // Check if the recovery is valid before going any further - if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { - Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") - throw InvalidParameterException("Invalid recovery key") - } - // Get a PK decryption instance - pkDecryptionFromRecoveryKey(recoveryKey) - } - if (decryption == null) { - // This should not happen anymore - Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") - throw InvalidParameterException("Invalid recovery key") - } - - // Save for next time and for gossiping - // Save now as it's valid, don't wait for the import as it could take long. - saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) - - stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) - - // Get backed up keys from the homeserver - val data = getKeys(sessionId, roomId, keysVersionResult.version) - - withContext(coroutineDispatchers.computation) { - val sessionsData = ArrayList() - // Restore that data - var sessionsFromHsCount = 0 - for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) { - for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) { - sessionsFromHsCount++ - - val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) - - sessionData?.let { - sessionsData.add(it) - } - } - } - Timber.v( - "restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + - " of $sessionsFromHsCount from the backup store on the homeserver" - ) - - // Do not trigger a backup for them if they come from the backup version we are using - val backUp = keysVersionResult.version != keysBackupVersion?.version - if (backUp) { - Timber.v( - "restoreKeysWithRecoveryKey: Those keys will be backed up" + - " to backup version: ${keysBackupVersion?.version}" - ) - } - - // Import them into the crypto store - val progressListener = if (stepProgressListener != null) { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - // Note: no need to post to UI thread, importMegolmSessionsData() will do it - stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) - } - } - } else { - null - } - - val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener) - - // Do not back up the key if it comes from a backup recovery - if (backUp) { - maybeBackupKeys() - } - result - } - }.foldToCallback(object : MatrixCallback { - override fun onSuccess(data: ImportRoomKeysResult) { - uiHandler.post { - callback.onSuccess(data) - } - } - - override fun onFailure(failure: Throwable) { - uiHandler.post { - callback.onFailure(failure) - } - } - }) - } - } - - override fun restoreKeyBackupWithPassword( - keysBackupVersion: KeysVersionResult, - password: String, - roomId: String?, - sessionId: String?, - stepProgressListener: StepProgressListener?, - callback: MatrixCallback - ) { - Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - - cryptoCoroutineScope.launch(coroutineDispatchers.io) { - runCatching { - val progressListener = if (stepProgressListener != null) { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) - } - } - } - } else { - null - } - - val recoveryKey = withContext(coroutineDispatchers.crypto) { - recoveryKeyFromPassword(password, keysBackupVersion, progressListener) - } - if (recoveryKey == null) { - Timber.v("backupKeys: Invalid configuration") - throw IllegalStateException("Invalid configuration") - } else { - awaitCallback { - restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) - } - } - }.foldToCallback(object : MatrixCallback { - override fun onSuccess(data: ImportRoomKeysResult) { - uiHandler.post { - callback.onSuccess(data) - } - } - - override fun onFailure(failure: Throwable) { - uiHandler.post { - callback.onFailure(failure) - } - } - }) - } - } - - /** - * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable - * parameters and always returns a KeysBackupData object through the Callback. - */ - private suspend fun getKeys( - sessionId: String?, - roomId: String?, - version: String - ): KeysBackupData { - return if (roomId != null && sessionId != null) { - // Get key for the room and for the session - val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) - // Convert to KeysBackupData - KeysBackupData( - mutableMapOf( - roomId to RoomKeysBackupData( - mutableMapOf( - sessionId to data - ) - ) - ) - ) - } else if (roomId != null) { - // Get all keys for the room - val data = withContext(coroutineDispatchers.io) { - getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) - } - // Convert to KeysBackupData - KeysBackupData(mutableMapOf(roomId to data)) - } else { - // Get all keys - withContext(coroutineDispatchers.io) { - getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) - } - } - } - - @VisibleForTesting - @WorkerThread - fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? { - // Extract the primary key - val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) - - // Built the PK decryption with it - var decryption: OlmPkDecryption? = null - if (privateKey != null) { - try { - decryption = OlmPkDecryption() - decryption.setPrivateKey(privateKey) - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - - return decryption - } - - /** - * Do a backup if there are new keys, with a delay. - */ - fun maybeBackupKeys() { - when { - isStuck() -> { - // If not already done, or in error case, check for a valid backup version on the homeserver. - // If there is one, maybeBackupKeys will be called again. - checkAndStartKeysBackup() - } - getState() == KeysBackupState.ReadyToBackUp -> { - keysBackupStateManager.state = KeysBackupState.WillBackUp - - // Wait between 0 and 10 seconds, to avoid backup requests from - // different clients hitting the server all at the same time when a - // new key is sent - val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) - - cryptoCoroutineScope.launch { - delay(delayInMs) - uiHandler.post { backupKeys() } - } - } - else -> { - Timber.v("maybeBackupKeys: Skip it because state: ${getState()}") - } - } - } - - override fun getVersion( - version: String, - callback: MatrixCallback - ) { - getKeysBackupVersionTask - .configureWith(version) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: KeysVersionResult) { - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError && - failure.error.code == MatrixError.M_NOT_FOUND) { - // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup - callback.onSuccess(null) - } else { - // Transmit the error - callback.onFailure(failure) - } - } - } - } - .executeBy(taskExecutor) - } - - override fun getCurrentVersion(callback: MatrixCallback) { - getKeysBackupLastVersionTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun forceUsingLastVersion(callback: MatrixCallback) { - getCurrentVersion(object : MatrixCallback { - override fun onSuccess(data: KeysBackupLastVersionResult) { - val localBackupVersion = keysBackupVersion?.version - when (data) { - KeysBackupLastVersionResult.NoKeysBackup -> { - if (localBackupVersion == null) { - // No backup on the server, and backup is not active - callback.onSuccess(true) - } else { - // No backup on the server, and we are currently backing up, so stop backing up - callback.onSuccess(false) - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Disabled - } - } - is KeysBackupLastVersionResult.KeysBackup -> { - if (localBackupVersion == null) { - // backup on the server, and backup is not active - callback.onSuccess(false) - // Do a check - checkAndStartWithKeysBackupVersion(data.keysVersionResult) - } else { - // Backup on the server, and we are currently backing up, compare version - if (localBackupVersion == data.keysVersionResult.version) { - // We are already using the last version of the backup - callback.onSuccess(true) - } else { - // We are not using the last version, so delete the current version we are using on the server - callback.onSuccess(false) - - // This will automatically check for the last version then - deleteBackup(localBackupVersion, null) - } - } - } - } - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - }) - } - - override fun checkAndStartKeysBackup() { - if (!isStuck()) { - // Try to start or restart the backup only if it is in unknown or bad state - Timber.w("checkAndStartKeysBackup: invalid state: ${getState()}") - - return - } - - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver - - getCurrentVersion(object : MatrixCallback { - override fun onSuccess(data: KeysBackupLastVersionResult) { - checkAndStartWithKeysBackupVersion(data.toKeysVersionResult()) - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") - keysBackupStateManager.state = KeysBackupState.Unknown - } - }) - } - - private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { - Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") - - keysBackupVersion = keyBackupVersion - - if (keyBackupVersion == null) { - Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") - resetKeysBackupData() - keysBackupStateManager.state = KeysBackupState.Disabled - } else { - getKeysBackupTrust(keyBackupVersion, object : MatrixCallback { - override fun onSuccess(data: KeysBackupVersionTrust) { - val versionInStore = cryptoStore.getKeyBackupVersion() - - if (data.usable) { - Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") - // Check the version we used at the previous app run - if (versionInStore != null && versionInStore != keyBackupVersion.version) { - Timber.v(" -> clean the previously used version $versionInStore") - resetKeysBackupData() - } - - Timber.v(" -> enabling key backups") - enableKeysBackup(keyBackupVersion) - } else { - Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") - if (versionInStore != null) { - Timber.v(" -> disabling key backup") - resetKeysBackupData() - } - - keysBackupStateManager.state = KeysBackupState.NotTrusted - } - } - - override fun onFailure(failure: Throwable) { - // Cannot happen - } - }) - } - } - -/* ========================================================================================== - * Private - * ========================================================================================== */ - - /** - * Extract MegolmBackupAuthData data from a backup version. - * - * @param keysBackupData the key backup data - * - * @return the authentication if found and valid, null in other case - */ - private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { - return keysBackupData - .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP } - ?.getAuthDataAsMegolmBackupAuthData() - ?.takeIf { it.publicKey.isNotEmpty() } - } - - /** - * Compute the recovery key from a password and key backup version. - * - * @param password the password. - * @param keysBackupData the backup and its auth data. - * @param progressListener listener to track progress - * - * @return the recovery key if successful, null in other cases - */ - @WorkerThread - private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? { - val authData = getMegolmBackupAuthData(keysBackupData) - - if (authData == null) { - Timber.w("recoveryKeyFromPassword: invalid parameter") - return null - } - - if (authData.privateKeySalt.isNullOrBlank() || - authData.privateKeyIterations == null) { - Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") - - return null - } - - // Extract the recovery key from the passphrase - val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener) - - return computeRecoveryKey(data) - } - - /** - * Check if a recovery key matches key backup authentication data. - * - * @param recoveryKey the recovery key to challenge. - * @param keysBackupData the backup and its auth data. - * - * @return true if successful. - */ - @WorkerThread - private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean { - // Build PK decryption instance with the recovery key - val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey) - - if (publicKey == null) { - Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null") - - return false - } - - val authData = getMegolmBackupAuthData(keysBackupData) - - if (authData == null) { - Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") - - return false - } - - // Compare both - if (publicKey != authData.publicKey) { - Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") - - return false - } - - // Public keys match! - return true - } - - override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { - val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } - - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let { - callback.onSuccess(it) - } - } - } - - override fun computePrivateKey( - passphrase: String, - privateKeySalt: String, - privateKeyIterations: Int, - progressListener: ProgressListener - ): ByteArray { - return deriveKey(passphrase, privateKeySalt, privateKeyIterations, progressListener) - } - - /** - * Enable backing up of keys. - * This method will update the state and will start sending keys in nominal case - * - * @param keysVersionResult backup information object as returned by [getCurrentVersion]. - */ - private fun enableKeysBackup(keysVersionResult: KeysVersionResult) { - val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() - - if (retrievedMegolmBackupAuthData != null) { - keysBackupVersion = keysVersionResult - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.setKeyBackupVersion(keysVersionResult.version) - } - - onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) - - try { - backupOlmPkEncryption = OlmPkEncryption().apply { - setRecipientKey(retrievedMegolmBackupAuthData.publicKey) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - keysBackupStateManager.state = KeysBackupState.Disabled - return - } - - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - - maybeBackupKeys() - } else { - Timber.e("Invalid authentication data") - keysBackupStateManager.state = KeysBackupState.Disabled - } - } - - /** - * Update the DB with data fetch from the server. - */ - private fun onServerDataRetrieved(count: Int?, etag: String?) { - cryptoStore.setKeysBackupData(KeysBackupDataEntity() - .apply { - backupLastServerNumberOfKeys = count - backupLastServerHash = etag - } - ) - } - - /** - * Reset all local key backup data. - * - * Note: This method does not update the state - */ - private fun resetKeysBackupData() { - resetBackupAllGroupSessionsListeners() - - cryptoStore.setKeyBackupVersion(null) - cryptoStore.setKeysBackupData(null) - backupOlmPkEncryption?.releaseEncryption() - backupOlmPkEncryption = null - - // Reset backup markers - cryptoStore.resetBackupMarkers() - } - - /** - * Send a chunk of keys to backup. - */ - @UiThread - private fun backupKeys() { - Timber.v("backupKeys") - - // Sanity check, as this method can be called after a delay, the state may have change during the delay - if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) { - Timber.v("backupKeys: Invalid configuration") - backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) - resetBackupAllGroupSessionsListeners() - return - } - - if (getState() === KeysBackupState.BackingUp) { - // Do nothing if we are already backing up - Timber.v("backupKeys: Invalid state: ${getState()}") - return - } - - // Get a chunk of keys to backup - val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) - - Timber.v("backupKeys: 1 - ${olmInboundGroupSessionWrappers.size} sessions to back up") - - if (olmInboundGroupSessionWrappers.isEmpty()) { - // Backup is up to date - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - - backupAllGroupSessionsCallback?.onSuccess(Unit) - resetBackupAllGroupSessionsListeners() - return - } - - keysBackupStateManager.state = KeysBackupState.BackingUp - - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - Timber.v("backupKeys: 2 - Encrypting keys") - - // Gather data to send to the homeserver - // roomId -> sessionId -> MXKeyBackupData - val keysBackupData = KeysBackupData() - - olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> - val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach - val olmInboundGroupSession = olmInboundGroupSessionWrapper.session - - try { - encryptGroupSession(olmInboundGroupSessionWrapper) - ?.let { - keysBackupData.roomIdToRoomKeysBackupData - .getOrPut(roomId) { RoomKeysBackupData() } - .sessionIdToKeyBackupData[olmInboundGroupSession.sessionIdentifier()] = it - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - - Timber.v("backupKeys: 4 - Sending request") - - // Make the request - val version = keysBackupVersion?.version ?: return@withContext - - storeSessionDataTask - .configureWith(StoreSessionsDataTask.Params(version, keysBackupData)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: BackupKeysResult) { - uiHandler.post { - Timber.v("backupKeys: 5a - Request complete") - - // Mark keys as backed up - cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers) - // we can release the sessions now - olmInboundGroupSessionWrappers.onEach { it.session.releaseSession() } - - if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { - Timber.v("backupKeys: All keys have been backed up") - onServerDataRetrieved(data.count, data.hash) - - // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } else { - Timber.v("backupKeys: Continue to back up keys") - keysBackupStateManager.state = KeysBackupState.WillBackUp - - backupKeys() - } - } - } - - override fun onFailure(failure: Throwable) { - if (failure is Failure.ServerError) { - uiHandler.post { - Timber.e(failure, "backupKeys: backupKeys failed.") - - when (failure.error.code) { - MatrixError.M_NOT_FOUND, - MatrixError.M_WRONG_ROOM_KEYS_VERSION -> { - // Backup has been deleted on the server, or we are not using the last backup version - keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion - backupAllGroupSessionsCallback?.onFailure(failure) - resetBackupAllGroupSessionsListeners() - resetKeysBackupData() - keysBackupVersion = null - - // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver - checkAndStartKeysBackup() - } - else -> - // Come back to the ready state so that we will retry on the next received key - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - } - } - } else { - uiHandler.post { - backupAllGroupSessionsCallback?.onFailure(failure) - resetBackupAllGroupSessionsListeners() - - Timber.e("backupKeys: backupKeys failed.") - - // Retry a bit later - keysBackupStateManager.state = KeysBackupState.ReadyToBackUp - maybeBackupKeys() - } - } - } - } - } - .executeBy(taskExecutor) - } - } - } - - @VisibleForTesting - @WorkerThread - suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? { - olmInboundGroupSessionWrapper.safeSessionId ?: return null - olmInboundGroupSessionWrapper.senderKey ?: return null - // Gather information for each key - val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey) - - // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at - // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format - val sessionData = inboundGroupSessionStore - .getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey) - ?.let { - withContext(coroutineDispatchers.computation) { - it.mutex.withLock { it.wrapper.exportKeys() } - } - } - ?: return null - val sessionBackupData = mapOf( - "algorithm" to sessionData.algorithm, - "sender_key" to sessionData.senderKey, - "sender_claimed_keys" to sessionData.senderClaimedKeys, - "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()), - "session_key" to sessionData.sessionKey, - "org.matrix.msc3061.shared_history" to sessionData.sharedHistory - ) - - val json = MoshiProvider.providesMoshi() - .adapter(Map::class.java) - .toJson(sessionBackupData) - - val encryptedSessionBackupData = try { - withContext(coroutineDispatchers.computation) { - backupOlmPkEncryption?.encrypt(json) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - null - } - ?: return null - - // Build backup data for that key - return KeyBackupData( - firstMessageIndex = try { - olmInboundGroupSessionWrapper.session.firstKnownIndex - } catch (e: OlmException) { - Timber.e(e, "OlmException") - 0L - }, - forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size, - isVerified = device?.isVerified == true, - sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(), - sessionData = mapOf( - "ciphertext" to encryptedSessionBackupData.mCipherText, - "mac" to encryptedSessionBackupData.mMac, - "ephemeral" to encryptedSessionBackupData.mEphemeralKey - ) - ) - } - - /** - * Returns boolean shared key flag, if enabled with respect to matrix configuration. - */ - private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean { - if (!cryptoStore.isShareKeysOnInviteEnabled()) return false - return sessionData.sharedHistory - } - - @VisibleForTesting - @WorkerThread - fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { - var sessionBackupData: MegolmSessionData? = null - - val jsonObject = keyBackupData.sessionData - - val ciphertext = jsonObject["ciphertext"]?.toString() - val mac = jsonObject["mac"]?.toString() - val ephemeralKey = jsonObject["ephemeral"]?.toString() - - if (ciphertext != null && mac != null && ephemeralKey != null) { - val encrypted = OlmPkMessage() - encrypted.mCipherText = ciphertext - encrypted.mMac = mac - encrypted.mEphemeralKey = ephemeralKey - - try { - val decrypted = decryption.decrypt(encrypted) - - val moshi = MoshiProvider.providesMoshi() - val adapter = moshi.adapter(MegolmSessionData::class.java) - - sessionBackupData = adapter.fromJson(decrypted) - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - - if (sessionBackupData != null) { - sessionBackupData = sessionBackupData.copy( - sessionId = sessionId, - roomId = roomId - ) - } - } - - return sessionBackupData - } - - /* ========================================================================================== - * For test only - * ========================================================================================== */ - - // Direct access for test only - @VisibleForTesting - val store - get() = cryptoStore - - @VisibleForTesting - fun createFakeKeysBackupVersion( - keysBackupCreationInfo: MegolmBackupCreationInfo, - callback: MatrixCallback - ) { - @Suppress("UNCHECKED_CAST") - val createKeysBackupVersionBody = CreateKeysBackupVersionBody( - algorithm = keysBackupCreationInfo.algorithm, - authData = keysBackupCreationInfo.authData.toJsonDict() - ) - - createKeysBackupVersionTask - .configureWith(createKeysBackupVersionBody) { - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { - return cryptoStore.getKeyBackupRecoveryKeyInfo() - } - - override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { - cryptoStore.saveBackupRecoveryKey(recoveryKey, version) - } - - companion object { - // Maximum delay in ms in {@link maybeBackupKeys} - private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L - - // Maximum number of keys to send at a time to the homeserver. - private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 - } - -/* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ - - override fun toString() = "KeysBackup for $userId" -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt index 0614eceb1..c6e867156 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import android.os.Handler +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener import timber.log.Timber @@ -33,11 +34,13 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) { field = newState // Notify listeners about the state change, on the ui thread - uiHandler.post { - synchronized(listeners) { - listeners.forEach { + synchronized(listeners) { + listeners.forEach { + uiHandler.post { // Use newState because state may have already changed again - it.onStateChange(newState) + tryOrNull { + it.onStateChange(newState) + } } } } @@ -59,6 +62,7 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) { synchronized(listeners) { listeners.add(listener) } + listener.onStateChange(state) } fun removeListener(listener: KeysBackupStateListener) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt new file mode 100644 index 000000000..4796180cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt @@ -0,0 +1,978 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.listeners.StepProgressListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keysbackup.IBackupRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData +import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.MegolmSessionData +import org.matrix.android.sdk.internal.crypto.MegolmSessionImportManager +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.PerSessionBackupQueryRateLimiter +import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmException +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.RequestType +import org.matrix.rustcomponents.sdk.crypto.SignatureState +import org.matrix.rustcomponents.sdk.crypto.SignatureVerification +import timber.log.Timber +import java.security.InvalidParameterException +import javax.inject.Inject +import kotlin.random.Random + +/** + * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +@SessionScope +internal class RustKeyBackupService @Inject constructor( + private val olmMachine: OlmMachine, + private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val megolmSessionImportManager: MegolmSessionImportManager, + private val cryptoCoroutineScope: CoroutineScope, + private val matrixConfiguration: MatrixConfiguration, + private val backupQueryRateLimiter: dagger.Lazy, +) : KeysBackupService { + companion object { + // Maximum delay in ms in {@link maybeBackupKeys} + private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L + } + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val keysBackupStateManager = KeysBackupStateManager(uiHandler) + + // The backup version + override var keysBackupVersion: KeysVersionResult? = null + private set + + private val importScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + SupervisorJob() + CoroutineName("backupImport")) + + private var keysBackupStateListener: KeysBackupStateListener? = null + + override fun isEnabled() = keysBackupStateManager.isEnabled + + override fun isStuck() = keysBackupStateManager.isStuck + + override fun getState() = keysBackupStateManager.state + + override val currentBackupVersion: String? + get() = keysBackupVersion?.version + + override fun addListener(listener: KeysBackupStateListener) { + keysBackupStateManager.addListener(listener) + } + + override fun removeListener(listener: KeysBackupStateListener) { + keysBackupStateManager.removeListener(listener) + } + + override suspend fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?): MegolmBackupCreationInfo { + return withContext(coroutineDispatchers.computation) { + val key = if (password != null) { + // this might be a bit slow as it's stretching the password + BackupRecoveryKey.newFromPassphrase(password) + } else { + BackupRecoveryKey() + } + + val publicKey = key.megolmV1PublicKey() + val backupAuthData = SignalableMegolmBackupAuthData( + publicKey = publicKey.publicKey, + privateKeySalt = publicKey.privateKeySalt, + privateKeyIterations = publicKey.privateKeyIterations + ) + val canonicalJson = JsonCanonicalizer.getCanonicalJson( + Map::class.java, + backupAuthData.signalableJSONDictionary() + ) + + val signedMegolmBackupAuthData = MegolmBackupAuthData( + publicKey = backupAuthData.publicKey, + privateKeySalt = backupAuthData.privateKeySalt, + privateKeyIterations = backupAuthData.privateKeyIterations, + signatures = olmMachine.sign(canonicalJson) + ) + + MegolmBackupCreationInfo( + algorithm = publicKey.backupAlgorithm, + authData = signedMegolmBackupAuthData, + recoveryKey = key + ) + } + } + + override suspend fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo): KeysVersion { + return withContext(coroutineDispatchers.crypto) { + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = keysBackupCreationInfo.authData.toJsonDict() + ) + + keysBackupStateManager.state = KeysBackupState.Enabling + + try { + val data = withContext(coroutineDispatchers.io) { + sender.createKeyBackup(createKeysBackupVersionBody) + } + // Reset backup markers. + // Don't we need to join the task here? Isn't this a race condition? + olmMachine.disableBackup() + + val keyBackupVersion = KeysVersionResult( + algorithm = createKeysBackupVersionBody.algorithm, + authData = createKeysBackupVersionBody.authData, + version = data.version, + // We can assume that the server does not have keys yet + count = 0, + hash = "" + ) + enableKeysBackup(keyBackupVersion) + data + } catch (failure: Throwable) { + keysBackupStateManager.state = KeysBackupState.Disabled + throw failure + } + } + } + + override fun saveBackupRecoveryKey(recoveryKey: IBackupRecoveryKey?, version: String?) { + cryptoCoroutineScope.launch { + olmMachine.saveRecoveryKey((recoveryKey as? BackupRecoveryKey)?.inner, version) + } + } + + private fun resetBackupAllGroupSessionsListeners() { +// backupAllGroupSessionsCallback = null + + keysBackupStateListener?.let { + keysBackupStateManager.removeListener(it) + } + + keysBackupStateListener = null + } + + /** + * Reset all local key backup data. + * + * Note: This method does not update the state + */ + private fun resetKeysBackupData() { + resetBackupAllGroupSessionsListeners() + olmMachine.disableBackup() + } + + override suspend fun deleteBackup(version: String) { + withContext(coroutineDispatchers.crypto) { + if (keysBackupVersion != null && version == keysBackupVersion?.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } + val state = getState() + + try { + sender.deleteKeyBackup(version) + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } catch (failure: Throwable) { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } + } + } + + override suspend fun canRestoreKeys(): Boolean { + val keyCountOnServer = keysBackupVersion?.count ?: return false + val keyCountLocally = getTotalNumbersOfKeys() + + // TODO is this sensible? We may have the same number of keys, or even more keys locally + // but the set of keys doesn't necessarily overlap + return keyCountLocally < keyCountOnServer + } + + override suspend fun getTotalNumbersOfKeys(): Int { + return olmMachine.roomKeyCounts().total.toInt() + } + + override suspend fun getTotalNumbersOfBackedUpKeys(): Int { + return olmMachine.roomKeyCounts().backedUp.toInt() + } + +// override fun backupAllGroupSessions(progressListener: ProgressListener?, +// callback: MatrixCallback?) { +// // This is only used in tests? While it's fine have methods that are +// // only used for tests, this one has a lot of logic that is nowhere else used. +// TODO() +// } + + private suspend fun checkBackupTrust(algAndData: KeysAlgorithmAndData?): KeysBackupVersionTrust { + if (algAndData == null) return KeysBackupVersionTrust(usable = false) + try { + val authData = olmMachine.checkAuthDataSignature(algAndData) + val signatures = authData.mapRustToAPI() + return KeysBackupVersionTrust(authData.trusted, signatures) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to trust backup") + return KeysBackupVersionTrust(usable = false) + } + } + + private suspend fun SignatureVerification.mapRustToAPI(): List { + val signatures = mutableListOf() + // signature state of own device + val ownDeviceState = this.deviceSignature + if (ownDeviceState != SignatureState.MISSING && ownDeviceState != SignatureState.INVALID) { + // we can add it + signatures.add( + KeysBackupVersionTrustSignature.DeviceSignature( + olmMachine.deviceId(), + olmMachine.getCryptoDeviceInfo(olmMachine.userId(), olmMachine.deviceId()), + ownDeviceState == SignatureState.VALID_AND_TRUSTED + ) + ) + } + // signature state of our own identity + val ownIdentityState = this.userIdentitySignature + if (ownIdentityState != SignatureState.MISSING && ownIdentityState != SignatureState.INVALID) { + // we can add it + val masterKey = olmMachine.getIdentity(olmMachine.userId())?.toMxCrossSigningInfo()?.masterKey() + signatures.add( + KeysBackupVersionTrustSignature.UserSignature( + masterKey?.unpaddedBase64PublicKey, + masterKey, + ownIdentityState == SignatureState.VALID_AND_TRUSTED + ) + ) + } + signatures.addAll( + this.otherDevicesSignatures + .filter { it.value == SignatureState.VALID_AND_TRUSTED || it.value == SignatureState.VALID_BUT_NOT_TRUSTED } + .map { + KeysBackupVersionTrustSignature.DeviceSignature( + it.key, + olmMachine.getCryptoDeviceInfo(olmMachine.userId(), it.key), + ownDeviceState == SignatureState.VALID_AND_TRUSTED + ) + } + ) + return signatures + } + + override suspend fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { + return withContext(coroutineDispatchers.crypto) { + checkBackupTrust(keysBackupVersion) + } + } + + override suspend fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean) { + withContext(coroutineDispatchers.crypto) { + Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") + + // Get auth data to update it + val authData = getMegolmBackupAuthData(keysBackupVersion) + + if (authData == null) { + Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") + throw IllegalArgumentException("Missing element") + } else { + // Get current signatures, or create an empty set + val userId = olmMachine.userId() + val signatures = authData.signatures?.get(userId).orEmpty().toMutableMap() + + if (trust) { + // Add current device signature + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) + val deviceSignature = olmMachine.sign(canonicalJson) + + deviceSignature[userId]?.forEach { entry -> + signatures[entry.key] = entry.value + } + } else { + signatures.remove("ed25519:${olmMachine.deviceId()}") + } + + val newAuthData = authData.copy() + val newSignatures = newAuthData.signatures.orEmpty().toMutableMap() + newSignatures[userId] = signatures + + val body = UpdateKeysBackupVersionBody( + algorithm = keysBackupVersion.algorithm, + authData = newAuthData.copy(signatures = newSignatures).toJsonDict(), + version = keysBackupVersion.version + ) + + withContext(coroutineDispatchers.io) { + sender.updateBackup(keysBackupVersion, body) + } + + val newKeysBackupVersion = KeysVersionResult( + algorithm = keysBackupVersion.algorithm, + authData = body.authData, + version = keysBackupVersion.version, + hash = keysBackupVersion.hash, + count = keysBackupVersion.count + ) + + checkAndStartWithKeysBackupVersion(newKeysBackupVersion) + } + } + } + + // Check that the recovery key matches to the public key that we downloaded from the server. +// If they match, we can trust the public key and enable backups since we have the private key. + private fun checkRecoveryKey(recoveryKey: IBackupRecoveryKey, keysBackupData: KeysVersionResult) { + val backupKey = recoveryKey.megolmV1PublicKey() + val authData = getMegolmBackupAuthData(keysBackupData) + + when { + authData == null -> { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") + throw IllegalArgumentException("Missing element") + } + backupKey.publicKey != authData.publicKey -> { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") + throw IllegalArgumentException("Invalid recovery key or password") + } + else -> { + // This case is fine, the public key on the server matches the public key the + // recovery key produced. + } + } + } + + override suspend fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: IBackupRecoveryKey) { + Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") + withContext(coroutineDispatchers.crypto) { + // This is ~nowhere mentioned, the string here is actually a base58 encoded key. + // This not really supported by the spec for the backup key, the 4S key supports + // base58 encoding and the same method seems to be used here. + checkRecoveryKey(recoveryKey, keysBackupVersion) + trustKeysBackupVersion(keysBackupVersion, true) + } + } + + override suspend fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String) { + withContext(coroutineDispatchers.crypto) { + val key = recoveryKeyFromPassword(password, keysBackupVersion) + checkRecoveryKey(key, keysBackupVersion) + trustKeysBackupVersion(keysBackupVersion, true) + } + } + + override suspend fun onSecretKeyGossip(secret: String) { + Timber.i("## CrossSigning - onSecretKeyGossip") + withContext(coroutineDispatchers.crypto) { + try { + val version = sender.getKeyBackupLastVersion()?.toKeysVersionResult() + Timber.v("Keybackup version: $version") + if (version != null) { + val key = BackupRecoveryKey.fromBase64(secret) + if (isValidRecoveryKey(key, version)) { + // we can save, it's valid + saveBackupRecoveryKey(key, version.version) + importScope.launch { + backupQueryRateLimiter.get().refreshBackupInfoIfNeeded(true) + } + // we don't want to wait for that +// importScope.launch { +// try { +// val importResult = restoreBackup(version, key, null, null, null) +// val recoveredKeys = importResult.successfullyNumberOfImportedKeys +// Timber.i("onSecretKeyGossip: Recovered keys $recoveredKeys out of ${importResult.totalNumberOfKeys}") +// } catch (failure: Throwable) { +// // fail silently.. +// Timber.e(failure, "onSecretKeyGossip: Failed to import keys from backup") +// } +// } + } else { + Timber.d("Invalid recovery key") + } + } else { + Timber.e("onSecretKeyGossip: Failed to import backup recovery key, no backup version was found on the server") + } + } catch (failure: Throwable) { + Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}: $failure") + } + } + } + + override suspend fun getBackupProgress(progressListener: ProgressListener) { + val backedUpKeys = getTotalNumbersOfBackedUpKeys() + val total = getTotalNumbersOfKeys() + + progressListener.onProgress(backedUpKeys, total) + } + + /** + * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable + * parameters and always returns a KeysBackupData object through the Callback. + */ + private suspend fun getKeys(sessionId: String?, roomId: String?, version: String): KeysBackupData { + return when { + roomId != null && sessionId != null -> { + sender.downloadBackedUpKeys(version, roomId, sessionId) + } + roomId != null -> { + sender.downloadBackedUpKeys(version, roomId) + } + else -> { + sender.downloadBackedUpKeys(version) + } + } + } + + @VisibleForTesting + @WorkerThread + fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, key: IBackupRecoveryKey): MegolmSessionData? { + var sessionBackupData: MegolmSessionData? = null + + val jsonObject = keyBackupData.sessionData + + val ciphertext = jsonObject["ciphertext"]?.toString() + val mac = jsonObject["mac"]?.toString() + val ephemeralKey = jsonObject["ephemeral"]?.toString() + + if (ciphertext != null && mac != null && ephemeralKey != null) { + try { + val decrypted = key.decryptV1(ephemeralKey, mac, ciphertext) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(MegolmSessionData::class.java) + + sessionBackupData = adapter.fromJson(decrypted) + } catch (e: Throwable) { + Timber.e(e, "OlmException") + } + + if (sessionBackupData != null) { + sessionBackupData = sessionBackupData.copy( + sessionId = sessionId, + roomId = roomId + ) + } + } + + return sessionBackupData + } + + private suspend fun restoreBackup( + keysVersionResult: KeysVersionResult, + recoveryKey: IBackupRecoveryKey, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + ): ImportRoomKeysResult { + withContext(coroutineDispatchers.crypto) { + // Check if the recovery is valid before going any further + if (!isValidRecoveryKey(recoveryKey, keysVersionResult)) { + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") + throw InvalidParameterException("Invalid recovery key") + } + + // Save for next time and for gossiping + saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) + } + + withContext(coroutineDispatchers.main) { + stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey) + } + + // Get backed up keys from the homeserver + val data = getKeys(sessionId, roomId, keysVersionResult.version) + + return withContext(coroutineDispatchers.computation) { + withContext(coroutineDispatchers.main) { + stepProgressListener?.onStepProgress(StepProgressListener.Step.DecryptingKey(0, data.roomIdToRoomKeysBackupData.size)) + } + // Decrypting by chunk of 500 keys in parallel + // we loose proper progress report but tested 3x faster on big backup + val sessionsData = data.roomIdToRoomKeysBackupData + .mapValues { + it.value.sessionIdToKeyBackupData + } + .flatMap { flat -> + flat.value.entries.map { flat.key to it } + } + .chunked(500) + .map { slice -> + async { + slice.mapNotNull { pair -> + decryptKeyBackupData(pair.second.value, pair.second.key, pair.first, recoveryKey) + } + } + } + .awaitAll() + .flatten() + + withContext(coroutineDispatchers.main) { + val stepProgress = StepProgressListener.Step.DecryptingKey(data.roomIdToRoomKeysBackupData.size, data.roomIdToRoomKeysBackupData.size) + stepProgressListener?.onStepProgress(stepProgress) + } + + Timber.v( + "restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" + + " of ${data.roomIdToRoomKeysBackupData.size} rooms from the backup store on the homeserver" + ) + + // Do not trigger a backup for them if they come from the backup version we are using + val backUp = keysVersionResult.version != keysBackupVersion?.version + if (backUp) { + Timber.v( + "restoreKeysWithRecoveryKey: Those keys will be backed up" + + " to backup version: ${keysBackupVersion?.version}" + ) + } + + // Import them into the crypto store + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + val stepProgress = StepProgressListener.Step.ImportingKey(progress, total) + stepProgressListener.onStepProgress(stepProgress) + } + } + } + } else { + null + } + + val result = olmMachine.importDecryptedKeys(sessionsData, progressListener).also { + sessionsData.onEach { sessionData -> + matrixConfiguration.cryptoAnalyticsPlugin + ?.onRoomKeyImported(sessionData.sessionId.orEmpty(), keysVersionResult.algorithm) + } + megolmSessionImportManager.dispatchKeyImportResults(it) + } + + // Do not back up the key if it comes from a backup recovery + if (backUp) { + maybeBackupKeys() + } + + result + } + } + + override suspend fun restoreKeysWithRecoveryKey( + keysVersionResult: KeysVersionResult, + recoveryKey: IBackupRecoveryKey, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener? + ): ImportRoomKeysResult { + Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") + return restoreBackup(keysVersionResult, recoveryKey, roomId, sessionId, stepProgressListener) + } + + override suspend fun restoreKeyBackupWithPassword( + keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener? + ): ImportRoomKeysResult { + Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") + val recoveryKey = withContext(coroutineDispatchers.crypto) { + recoveryKeyFromPassword(password, keysBackupVersion) + } + return restoreBackup(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener) + } + + override suspend fun getVersion(version: String): KeysVersionResult? { + return sender.getKeyBackupVersion(version) + } + + @Throws + override suspend fun getCurrentVersion(): KeysBackupLastVersionResult? { + return sender.getKeyBackupLastVersion() + } + + override suspend fun forceUsingLastVersion(): Boolean { + val response = withContext(coroutineDispatchers.io) { + sender.getKeyBackupLastVersion()?.toKeysVersionResult() + } + + return withContext(coroutineDispatchers.crypto) { + val serverBackupVersion = response?.version + val localBackupVersion = keysBackupVersion?.version + + Timber.d("BACKUP: $serverBackupVersion") + + if (serverBackupVersion == null) { + if (localBackupVersion == null) { + // No backup on the server, and backup is not active + true + } else { + // No backup on the server, and we are currently backing up, so stop backing up + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Disabled + false + } + } else { + if (localBackupVersion == null) { + // Do a check + checkAndStartWithKeysBackupVersion(response) + // backup on the server, and backup is not active + false + } else { + // Backup on the server, and we are currently backing up, compare version + if (localBackupVersion == serverBackupVersion) { + // We are already using the last version of the backup + true + } else { + // This will automatically check for the last version then + tryOrNull("Failed to automatically check for the last version") { + deleteBackup(localBackupVersion) + } + // We are not using the last version, so delete the current version we are using on the server + false + } + } + } + } + } + + override suspend fun checkAndStartKeysBackup() { + withContext(coroutineDispatchers.crypto) { + if (!isStuck()) { + // Try to start or restart the backup only if it is in unknown or bad state + Timber.w("checkAndStartKeysBackup: invalid state: ${getState()}") + return@withContext + } + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver + try { + val data = getCurrentVersion()?.toKeysVersionResult() + withContext(coroutineDispatchers.crypto) { + checkAndStartWithKeysBackupVersion(data) + } + } catch (failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + withContext(coroutineDispatchers.crypto) { + keysBackupStateManager.state = KeysBackupState.Unknown + } + } + } + } + + private suspend fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { + Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") + + keysBackupVersion = keyBackupVersion + + if (keyBackupVersion == null) { + Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") + resetKeysBackupData() + keysBackupStateManager.state = KeysBackupState.Disabled + } else { + try { + val data = getKeysBackupTrust(keyBackupVersion) + val versionInStore = getKeyBackupRecoveryKeyInfo()?.version + + if (data.usable) { + Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") + // Check the version we used at the previous app run + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Timber.v(" -> clean the previously used version $versionInStore") + resetKeysBackupData() + } + + Timber.v(" -> enabling key backups") + cryptoCoroutineScope.launch { + enableKeysBackup(keyBackupVersion) + } + } else { + Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") + if (versionInStore != null) { + Timber.v(" -> disabling key backup") + resetKeysBackupData() + } + + keysBackupStateManager.state = KeysBackupState.NotTrusted + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to checkAndStartWithKeysBackupVersion $keyBackupVersion") + } + } + } + + private fun isValidRecoveryKey(recoveryKey: IBackupRecoveryKey, version: KeysVersionResult): Boolean { + val publicKey = recoveryKey.megolmV1PublicKey().publicKey + val authData = getMegolmBackupAuthData(version) ?: return false + Timber.v("recoveryKey.megolmV1PublicKey().publicKey $publicKey == getMegolmBackupAuthData(version).publicKey ${authData.publicKey}") + return authData.publicKey == publicKey + } + + override suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: IBackupRecoveryKey): Boolean { + return withContext(coroutineDispatchers.crypto) { + val keysBackupVersion = keysBackupVersion ?: return@withContext false + try { + isValidRecoveryKey(recoveryKey, keysBackupVersion) + } catch (failure: Throwable) { + Timber.i("isValidRecoveryKeyForCurrentVersion: Invalid recovery key") + false + } + } + } + + override fun computePrivateKey(passphrase: String, privateKeySalt: String, privateKeyIterations: Int, progressListener: ProgressListener): ByteArray { + return deriveKey(passphrase, privateKeySalt, privateKeyIterations, progressListener) + } + + override suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + val info = olmMachine.getBackupKeys() ?: return null + val backupRecoveryKey = BackupRecoveryKey(info.recoveryKey()) + return SavedKeyBackupKeyInfo(backupRecoveryKey, info.backupVersion()) + } + + /** + * Compute the recovery key from a password and key backup version. + * + * @param password the password. + * @param keysBackupData the backup and its auth data. + * + * @return the recovery key if successful, null in other cases + */ + @WorkerThread + private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult): BackupRecoveryKey { + val authData = getMegolmBackupAuthData(keysBackupData) + + return when { + authData == null -> { + throw IllegalArgumentException("recoveryKeyFromPassword: invalid parameter") + } + authData.privateKeySalt.isNullOrBlank() || authData.privateKeyIterations == null -> { + throw java.lang.IllegalArgumentException("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") + } + else -> { + BackupRecoveryKey.fromPassphrase(password, authData.privateKeySalt, authData.privateKeyIterations) + } + } + } + + /** + * Extract MegolmBackupAuthData data from a backup version. + * + * @param keysBackupData the key backup data + * + * @return the authentication if found and valid, null in other case + */ + private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { + return keysBackupData + .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP } + ?.getAuthDataAsMegolmBackupAuthData() + ?.takeIf { it.publicKey.isNotEmpty() } + } + + /** + * Enable backing up of keys. + * This method will update the state and will start sending keys in nominal case + * + * @param keysVersionResult backup information object as returned by [getCurrentVersion]. + */ + private suspend fun enableKeysBackup(keysVersionResult: KeysVersionResult) { + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + if (retrievedMegolmBackupAuthData != null) { + try { + olmMachine.enableBackupV1(retrievedMegolmBackupAuthData.publicKey, keysVersionResult.version) + keysBackupVersion = keysVersionResult + } catch (e: OlmException) { + Timber.e(e, "OlmException") + keysBackupStateManager.state = KeysBackupState.Disabled + return + } + + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } else { + Timber.e("Invalid authentication data") + keysBackupStateManager.state = KeysBackupState.Disabled + } + } + + /** + * Do a backup if there are new keys, with a delay. + */ + suspend fun maybeBackupKeys() { + withContext(coroutineDispatchers.crypto) { + when { + isStuck() -> { + // If not already done, or in error case, check for a valid backup version on the homeserver. + // If there is one, maybeBackupKeys will be called again. + checkAndStartKeysBackup() + } + getState() == KeysBackupState.ReadyToBackUp -> { + keysBackupStateManager.state = KeysBackupState.WillBackUp + + // Wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS) + + importScope.launch { + delay(delayInMs) + tryOrNull("AUTO backup failed") { backupKeys() } + } + } + else -> { + Timber.v("maybeBackupKeys: Skip it because state: ${getState()}") + } + } + } + } + + /** + * Send a chunk of keys to backup. + */ + private suspend fun backupKeys(forceRecheck: Boolean = false) { + Timber.v("backupKeys") + withContext(coroutineDispatchers.crypto) { + val isEnabled = isEnabled() + val state = getState() + // Sanity check, as this method can be called after a delay, the state may have change during the delay + if (!isEnabled || !olmMachine.backupEnabled() || keysBackupVersion == null) { + Timber.v("backupKeys: Invalid configuration $isEnabled ${olmMachine.backupEnabled()} $keysBackupVersion") +// backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) + resetBackupAllGroupSessionsListeners() + + return@withContext + } + + if (state === KeysBackupState.BackingUp && !forceRecheck) { + // Do nothing if we are already backing up + Timber.v("backupKeys: Invalid state: $state") + return@withContext + } + + Timber.d("BACKUP: CREATING REQUEST") + + val request = olmMachine.backupRoomKeys() + + Timber.d("BACKUP: GOT REQUEST $request") + + if (request == null) { + // Backup is up to date + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + +// backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + } else { + try { + if (request is Request.KeysBackup) { + keysBackupStateManager.state = KeysBackupState.BackingUp + + Timber.d("BACKUP SENDING REQUEST") + val response = withContext(coroutineDispatchers.io) { sender.backupRoomKeys(request) } + Timber.d("BACKUP GOT RESPONSE $response") + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_BACKUP, response) + Timber.d("BACKUP MARKED REQUEST AS SENT") + + backupKeys(true) + } else { + // Can't happen, do we want to panic? + } + } catch (failure: Throwable) { + if (failure is Failure.ServerError) { + withContext(coroutineDispatchers.main) { + Timber.e(failure, "backupKeys: backupKeys failed.") + + when (failure.error.code) { + MatrixError.M_NOT_FOUND, + MatrixError.M_WRONG_ROOM_KEYS_VERSION -> { + // Backup has been deleted on the server, or we are not using + // the last backup version + keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion +// backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + resetKeysBackupData() + keysBackupVersion = null + + // Do not stay in KeysBackupState.WrongBackUpVersion but check what + // is available on the homeserver + checkAndStartKeysBackup() + } + else -> + // Come back to the ready state so that we will retry on the next received key + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } + } + } else { +// backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + + Timber.e("backupKeys: backupKeys failed: $failure") + + // Retry a bit later + keysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/DefaultKeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/DefaultKeysAlgorithmAndData.kt new file mode 100644 index 000000000..73dea5302 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/DefaultKeysAlgorithmAndData.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +internal data class DefaultKeysAlgorithmAndData( + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined. + */ + @Json(name = "algorithm") + override val algorithm: String, + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2". + * see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + override val authData: JsonDict +) : KeysAlgorithmAndData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt index e5621c0cb..4cd6784f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -33,7 +33,7 @@ internal class DefaultGetKeysBackupLastVersionTask @Inject constructor( override suspend fun execute(params: Unit): KeysBackupLastVersionResult { return try { - val keysVersionResult = executeRequest(globalErrorReceiver) { + val keysVersionResult = executeRequest(globalErrorReceiver, canRetry = true) { roomKeysApi.getKeysBackupLastVersion() } KeysBackupLastVersionResult.KeysBackup(keysVersionResult) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt index fe1ca2979..3f8458238 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -31,7 +31,7 @@ internal class DefaultGetKeysBackupVersionTask @Inject constructor( ) : GetKeysBackupVersionTask { override suspend fun execute(params: String): KeysVersionResult { - return executeRequest(globalErrorReceiver) { + return executeRequest(globalErrorReceiver, canRetry = true) { roomKeysApi.getKeysBackupVersion(params) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt index 47f2578c4..623fc8a6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -37,7 +37,7 @@ internal class DefaultStoreSessionsDataTask @Inject constructor( ) : StoreSessionsDataTask { override suspend fun execute(params: StoreSessionsDataTask.Params): BackupKeysResult { - return executeRequest(globalErrorReceiver) { + return executeRequest(globalErrorReceiver, canRetry = true) { roomKeysApi.storeSessionsData( params.version, params.keysBackupData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt index 2b3d044ab..66f4adf52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -36,7 +36,7 @@ internal class DefaultUpdateKeysBackupVersionTask @Inject constructor( ) : UpdateKeysBackupVersionTask { override suspend fun execute(params: UpdateKeysBackupVersionTask.Params) { - return executeRequest(globalErrorReceiver) { + return executeRequest(globalErrorReceiver, canRetry = true) { roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt deleted file mode 100644 index 85ba1762d..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory - -/** - * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationAccept( - /** - * string to identify the transaction. - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - * Alice’s device should record this ID and use it in future messages in this transaction. - */ - @Json(name = "transaction_id") - override val transactionId: String? = null, - - /** - * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "key_agreement_protocol") - override val keyAgreementProtocol: String? = null, - - /** - * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "hash") - override val hash: String? = null, - - /** - * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - @Json(name = "message_authentication_code") - override val messageAuthenticationCode: String? = null, - - /** - * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device. - */ - @Json(name = "short_authentication_string") - override val shortAuthenticationStrings: List? = null, - - /** - * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) - * and the canonical JSON representation of the m.key.verification.start message. - */ - @Json(name = "commitment") - override var commitment: String? = null -) : SendToDeviceObject, VerificationInfoAccept { - - override fun toSendToDeviceObject() = this - - companion object : VerificationInfoAcceptFactory { - override fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept { - return KeyVerificationAccept( - transactionId = tid, - keyAgreementProtocol = keyAgreementProtocol, - hash = hash, - commitment = commitment, - messageAuthenticationCode = messageAuthenticationCode, - shortAuthenticationStrings = shortAuthenticationStrings - ) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt deleted file mode 100644 index 2858ef3ee..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel - -/** - * To device event sent by either party to cancel a key verification. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationCancel( - /** - * the transaction ID of the verification to cancel. - */ - @Json(name = "transaction_id") - override val transactionId: String? = null, - - /** - * machine-readable reason for cancelling, see #CancelCode. - */ - override val code: String? = null, - - /** - * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. - */ - override val reason: String? = null -) : SendToDeviceObject, VerificationInfoCancel { - - companion object { - fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { - return KeyVerificationCancel( - tid, - cancelCode.value, - cancelCode.humanReadable - ) - } - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt deleted file mode 100644 index e3907914a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationDone( - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoDone { - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt deleted file mode 100644 index a833148b9..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory - -/** - * Sent by both devices to send their ephemeral Curve25519 public key to the other device. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationKey( - /** - * The ID of the transaction that the message is part of. - */ - @Json(name = "transaction_id") override val transactionId: String? = null, - - /** - * The device’s ephemeral public key, as an unpadded base64 string. - */ - @Json(name = "key") override val key: String? = null - -) : SendToDeviceObject, VerificationInfoKey { - - companion object : VerificationInfoKeyFactory { - override fun create(tid: String, pubKey: String): KeyVerificationKey { - return KeyVerificationKey(tid, pubKey) - } - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt deleted file mode 100644 index 5335428c0..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory - -/** - * Sent by both devices to send the MAC of their device key to the other device. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationMac( - @Json(name = "transaction_id") override val transactionId: String? = null, - @Json(name = "mac") override val mac: Map? = null, - @Json(name = "keys") override val keys: String? = null - -) : SendToDeviceObject, VerificationInfoMac { - - override fun toSendToDeviceObject(): SendToDeviceObject? = this - - companion object : VerificationInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { - return KeyVerificationMac(tid, mac, keys) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt deleted file mode 100644 index e6770be9a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationReady( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List?, - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoReady { - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt deleted file mode 100644 index 191d5abb6..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest - -/** - * Requests a key verification with another user's devices. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationRequest( - @Json(name = "from_device") override val fromDevice: String?, - @Json(name = "methods") override val methods: List, - @Json(name = "timestamp") override val timestamp: Long?, - @Json(name = "transaction_id") override val transactionId: String? = null -) : SendToDeviceObject, VerificationInfoRequest { - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt deleted file mode 100644 index f74bad844..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.model.rest - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart -import org.matrix.android.sdk.internal.util.JsonCanonicalizer - -/** - * Sent by Alice to initiate an interactive key verification. - */ -@JsonClass(generateAdapter = true) -internal data class KeyVerificationStart( - @Json(name = "from_device") override val fromDevice: String? = null, - @Json(name = "method") override val method: String? = null, - @Json(name = "transaction_id") override val transactionId: String? = null, - @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List? = null, - @Json(name = "hashes") override val hashes: List? = null, - @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List? = null, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null, - // For QR code verification - @Json(name = "secret") override val sharedSecret: String? = null -) : SendToDeviceObject, VerificationInfoStart { - - override fun toCanonicalJson(): String { - return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) - } - - override fun toSendToDeviceObject() = this -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt index 22f4ce5a5..a61bb5e60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysClaimResponse.kt @@ -24,6 +24,11 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class KeysClaimResponse( + // / If any remote homeservers could not be reached, they are recorded here. + // / The names of the properties are the names of the unreachable servers. + @Json(name = "failures") + val failures: Map? = null, + /** * The requested keys ordered by device by user. * TODO Type does not match spec, should be Map diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt index 363dee9a8..1d1fb3e1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt @@ -46,6 +46,6 @@ internal data class KeysUploadBody( * If the user had previously uploaded a fallback key for a given algorithm, it is replaced. * The server will only keep one fallback key per algorithm for each user. */ - @Json(name = "org.matrix.msc2732.fallback_keys") + @Json(name = "fallback_keys") val fallbackKeys: JsonDict? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt new file mode 100644 index 000000000..9e0301f48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.network + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.sync.handler.ShieldSummaryUpdater +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.RequestType +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("OutgoingRequestsProcessor", LoggerTag.CRYPTO) + +@SessionScope +internal class OutgoingRequestsProcessor @Inject constructor( + private val requestSender: RequestSender, + private val coroutineScope: CoroutineScope, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val shieldSummaryUpdater: ShieldSummaryUpdater, + private val matrixConfiguration: MatrixConfiguration, + private val coroutineDispatchers: MatrixCoroutineDispatchers, +) { + + private val lock: Mutex = Mutex() + + suspend fun processOutgoingRequests(olmMachine: OlmMachine, + filter: (Request) -> Boolean = { true } + ): Boolean { + return lock.withLock { + coroutineScope { + val outgoingRequests = olmMachine.outgoingRequests() + val filteredOutgoingRequests = outgoingRequests.filter(filter) + Timber.v("OutgoingRequests to process: $filteredOutgoingRequests}") + filteredOutgoingRequests.map { + when (it) { + is Request.KeysUpload -> { + async { + uploadKeys(olmMachine, it) + } + } + is Request.KeysQuery -> { + async { + queryKeys(olmMachine, it) + } + } + is Request.ToDevice -> { + async { + sendToDevice(olmMachine, it) + } + } + is Request.KeysClaim -> { + async { + claimKeys(olmMachine, it) + } + } + is Request.RoomMessage -> { + async { + sendRoomMessage(olmMachine, it) + } + } + is Request.SignatureUpload -> { + async { + signatureUpload(olmMachine, it) + } + } + is Request.KeysBackup -> { + async { + // The rust-sdk won't ever produce KeysBackup requests here, + // those only get explicitly created. + true + } + } + } + }.awaitAll().all { it } + } + } + } + + suspend fun processRequestRoomKey(olmMachine: OlmMachine, event: Event) { + val requestPair = olmMachine.requestRoomKey(event) + val cancellation = requestPair.cancellation + val request = requestPair.keyRequest + + when (cancellation) { + is Request.ToDevice -> { + sendToDevice(olmMachine, cancellation) + } + else -> Unit + } + when (request) { + is Request.ToDevice -> { + sendToDevice(olmMachine, request) + } + else -> Unit + } + } + + private suspend fun uploadKeys(olmMachine: OlmMachine, request: Request.KeysUpload): Boolean { + return try { + val response = requestSender.uploadKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_UPLOAD, response) + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## uploadKeys(): error") + false + } + } + + private suspend fun queryKeys(olmMachine: OlmMachine, request: Request.KeysQuery): Boolean { + return try { + val response = requestSender.queryKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response) + shieldSummaryUpdater.refreshShieldsForRoomsWithMembers(request.users) + coroutineScope.markMessageVerificationStatesAsDirty(request.users) + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## queryKeys(): error") + false + } + } + + private fun CoroutineScope.markMessageVerificationStatesAsDirty(userIds: List) = launch(coroutineDispatchers.computation) { + cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds) + } + + private suspend fun sendToDevice(olmMachine: OlmMachine, request: Request.ToDevice): Boolean { + return try { + requestSender.sendToDevice(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}") + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## sendToDevice(): error") + matrixConfiguration.cryptoAnalyticsPlugin?.onFailToSendToDevice(throwable) + false + } + } + + private suspend fun claimKeys(olmMachine: OlmMachine, request: Request.KeysClaim): Boolean { + return try { + val response = requestSender.claimKeys(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response) + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## claimKeys(): error") + false + } + } + + private suspend fun signatureUpload(olmMachine: OlmMachine, request: Request.SignatureUpload): Boolean { + return try { + val response = requestSender.sendSignatureUpload(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.SIGNATURE_UPLOAD, response) + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## signatureUpload(): error") + false + } + } + + private suspend fun sendRoomMessage(olmMachine: OlmMachine, request: Request.RoomMessage): Boolean { + return try { + val response = requestSender.sendRoomMessage(request) + olmMachine.markRequestAsSent(request.requestId, RequestType.ROOM_MESSAGE, response) + true + } catch (throwable: Throwable) { + Timber.tag(loggerTag.value).e(throwable, "## sendRoomMessage(): error") + false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt new file mode 100644 index 000000000..88432c8fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.network + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import dagger.Lazy +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.uia.UiaResult +import org.matrix.android.sdk.internal.auth.registration.handleUIA +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse +import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.rustcomponents.sdk.crypto.OutgoingVerificationRequest +import org.matrix.rustcomponents.sdk.crypto.Request +import org.matrix.rustcomponents.sdk.crypto.SignatureUploadRequest +import org.matrix.rustcomponents.sdk.crypto.UploadSigningKeysRequest +import timber.log.Timber +import javax.inject.Inject + +internal class RequestSender @Inject constructor( + @UserId + private val myUserId: String, + private val sendToDeviceTask: SendToDeviceTask, + private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask, + private val uploadKeysTask: UploadKeysTask, + private val downloadKeysForUsersTask: DownloadKeysForUsersTask, + private val signaturesUploadTask: UploadSignaturesTask, + private val sendVerificationMessageTask: Lazy, + private val uploadSigningKeysTask: UploadSigningKeysTask, + private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, + private val getKeysBackupVersionTask: GetKeysBackupVersionTask, + private val deleteBackupTask: DeleteBackupTask, + private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, + private val backupRoomKeysTask: StoreSessionsDataTask, + private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, + private val getSessionsDataTask: GetSessionsDataTask, + private val getRoomSessionsDataTask: GetRoomSessionsDataTask, + private val getRoomSessionDataTask: GetRoomSessionDataTask, + private val moshi: Moshi, + cryptoCoroutineScope: CoroutineScope, + private val localEchoRepository: LocalEchoRepository, +) { + + private val scope = CoroutineScope( + cryptoCoroutineScope.coroutineContext + SupervisorJob() + CoroutineName("backupRequest") + ) + + suspend fun claimKeys(request: Request.KeysClaim): String { + val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(request.oneTimeKeys) + val response = oneTimeKeysForUsersDeviceTask.execute(claimParams) + val adapter = MoshiProvider + .providesMoshi() + .adapter(KeysClaimResponse::class.java) + return adapter.toJson(response)!! + } + + suspend fun queryKeys(request: Request.KeysQuery): String { + val params = DownloadKeysForUsersTask.Params(request.users, null) + val response = downloadKeysForUsersTask.execute(params) + val adapter = moshi.adapter(KeysQueryResponse::class.java) + return adapter.toJson(response)!! + } + + suspend fun uploadKeys(request: Request.KeysUpload): String { + val body = moshi.adapter(KeysUploadBody::class.java).fromJson(request.body)!! + val params = UploadKeysTask.Params(body) + + val response = uploadKeysTask.execute(params) + val adapter = moshi.adapter(KeysUploadResponse::class.java) + + return adapter.toJson(response)!! + } + + suspend fun sendVerificationRequest(request: OutgoingVerificationRequest, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT) { + when (request) { + is OutgoingVerificationRequest.InRoom -> sendRoomMessage(request, retryCount) + is OutgoingVerificationRequest.ToDevice -> sendToDevice(request, retryCount) + } + } + + private suspend fun sendRoomMessage(request: OutgoingVerificationRequest.InRoom, retryCount: Int): SendResponse { + return sendRoomMessage( + eventType = request.eventType, + roomId = request.roomId, + content = request.content, + transactionId = request.requestId, + retryCount = retryCount + ) + } + + suspend fun sendRoomMessage(request: Request.RoomMessage, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT): String { + val sendResponse = sendRoomMessage(request.eventType, request.roomId, request.content, request.requestId, retryCount) + val responseAdapter = moshi.adapter(SendResponse::class.java) + return responseAdapter.toJson(sendResponse) + } + + suspend fun sendRoomMessage(eventType: String, + roomId: String, + content: String, + transactionId: String, + retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT + ): SendResponse { + val paramsAdapter = moshi.adapter(Map::class.java) + val jsonContent = paramsAdapter.fromJson(content) + val event = Event( + senderId = myUserId, + type = eventType, + eventId = transactionId, + content = jsonContent, + roomId = roomId) + localEchoRepository.createLocalEcho(event) + val params = SendVerificationMessageTask.Params(event, retryCount) + return sendVerificationMessageTask.get().execute(params) + } + + suspend fun sendSignatureUpload(request: Request.SignatureUpload): String { + return sendSignatureUpload(request.body) + } + + suspend fun sendSignatureUpload(request: SignatureUploadRequest): String { + return sendSignatureUpload(request.body) + } + + private suspend fun sendSignatureUpload(body: String): String { + val paramsAdapter = moshi.adapter>>(Map::class.java) + val signatures = paramsAdapter.fromJson(body)!! + val params = UploadSignaturesTask.Params(signatures) + val response = signaturesUploadTask.execute(params) + val responseAdapter = moshi.adapter(SignatureUploadResponse::class.java) + return responseAdapter.toJson(response)!! + } + + suspend fun uploadCrossSigningKeys( + request: UploadSigningKeysRequest, + interactiveAuthInterceptor: UserInteractiveAuthInterceptor? + ) { + val adapter = moshi.adapter(RestKeyInfo::class.java) + val masterKey = adapter.fromJson(request.masterKey)!!.toCryptoModel() + val selfSigningKey = adapter.fromJson(request.selfSigningKey)!!.toCryptoModel() + val userSigningKey = adapter.fromJson(request.userSigningKey)!!.toCryptoModel() + + val uploadSigningKeysParams = UploadSigningKeysTask.Params( + masterKey, + userSigningKey, + selfSigningKey, + null + ) + + try { + uploadSigningKeysTask.execute(uploadSigningKeysParams) + } catch (failure: Throwable) { + if (interactiveAuthInterceptor == null || + handleUIA( + failure = failure, + interceptor = interactiveAuthInterceptor, + retryBlock = { authUpdate -> + uploadSigningKeysTask.execute( + uploadSigningKeysParams.copy(userAuthParam = authUpdate) + ) + } + ) != UiaResult.SUCCESS + ) { + Timber.d("## UIA: propagate failure") + throw failure + } + } + } + + suspend fun sendToDevice(request: Request.ToDevice, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT) { + sendToDevice(request.eventType, request.body, request.requestId, retryCount) + } + + suspend fun sendToDevice(request: OutgoingVerificationRequest.ToDevice, retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT) { + sendToDevice(request.eventType, request.body, request.requestId, retryCount) + } + + private suspend fun sendToDevice(eventType: String, body: String, transactionId: String, retryCount: Int) { + val adapter = moshi + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + .adapter>>(Map::class.java) + val jsonBody = adapter.fromJson(body)!! + + val userMap = MXUsersDevicesMap() + userMap.join(jsonBody) + + val sendToDeviceParams = SendToDeviceTask.Params(eventType, userMap, transactionId, retryCount) + sendToDeviceTask.execute(sendToDeviceParams) + } + + suspend fun getKeyBackupVersion(version: String): KeysVersionResult? = getKeyBackupVersion { + getKeysBackupVersionTask.execute(version) + } + + suspend fun getKeyBackupLastVersion(): KeysBackupLastVersionResult? = getKeyBackupVersion { + getKeysBackupLastVersionTask.execute(Unit) + } + + private inline fun getKeyBackupVersion(block: () -> T?): T? { + return try { + block() + } catch (failure: Throwable) { + if (failure is Failure.ServerError && + failure.error.code == MatrixError.M_NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + null + } else { + throw failure + } + } + } + + suspend fun createKeyBackup(body: CreateKeysBackupVersionBody): KeysVersion { + return createKeysBackupVersionTask.execute(body) + } + + suspend fun deleteKeyBackup(version: String) { + val params = DeleteBackupTask.Params(version) + deleteBackupTask.execute(params) + } + + suspend fun backupRoomKeys(request: Request.KeysBackup): String { + val adapter = MoshiProvider + .providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + .adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + RoomKeysBackupData::class.java + ) + ) + val keys = adapter.fromJson(request.rooms)!! + val params = StoreSessionsDataTask.Params(request.version, KeysBackupData(keys)) + val response = backupRoomKeysTask.execute(params) + val responseAdapter = moshi.adapter(BackupKeysResult::class.java) + return responseAdapter.toJson(response)!! + } + + suspend fun updateBackup(keysBackupVersion: KeysVersionResult, body: UpdateKeysBackupVersionBody) { + val params = UpdateKeysBackupVersionTask.Params(keysBackupVersion.version, body) + updateKeysBackupVersionTask.execute(params) + } + + suspend fun downloadBackedUpKeys(version: String, roomId: String, sessionId: String): KeysBackupData { + val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version)) + + return KeysBackupData( + mutableMapOf( + roomId to RoomKeysBackupData( + mutableMapOf( + sessionId to data + ) + ) + ) + ) + } + + suspend fun downloadBackedUpKeys(version: String, roomId: String): KeysBackupData { + val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + // Convert to KeysBackupData + return KeysBackupData(mutableMapOf(roomId to data)) + } + + suspend fun downloadBackedUpKeys(version: String): KeysBackupData { + return getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index ddb048a91..05b9e14b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -385,7 +385,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return IntegrityResult.Success(keyInfo.content.passphrase != null) } + @Deprecated("Requesting custom secrets not yet support by rust stack, prefer requestMissingSecrets") override suspend fun requestSecret(name: String, myOtherDeviceId: String) { secretShareManager.requestSecretTo(myOtherDeviceId, name) } + + override suspend fun requestMissingSecrets() { + secretShareManager.requestMissingSecrets() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt new file mode 100644 index 000000000..68b002c08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator + +/** + * As a temporary measure rust and kotlin flavor are still using realm to store some crypto + * related information. In the near future rust flavor will complitly stop using realm, as soon + * as the missing bits are store in rust side (like room encryption settings, ..) + * This interface defines what's now used by both flavors. + * The actual implementation are moved in each flavors + */ +interface IMXCommonCryptoStore { + + /** + * Provides the algorithm used in a dedicated room. + * + * @param roomId the room id + * @return the algorithm, null is the room is not encrypted + */ + fun getRoomAlgorithm(roomId: String): String? + + fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? + + fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) + + fun roomWasOnceEncrypted(roomId: String): Boolean + + fun saveMyDevicesInfo(info: List) + + // questionable that it's stored in crypto store + fun getMyDevicesInfo(): List + + // questionable that it's stored in crypto store + fun getLiveMyDevicesInfo(): LiveData> + + // questionable that it's stored in crypto store + fun getLiveMyDevicesInfo(deviceId: String): LiveData> + + /** + * open any existing crypto store. + */ + fun open() + fun tidyUpDataBase() + + /** + * Close the store. + */ + fun close() + + /* + * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. + */ + fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + /** + * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) + * will be shared to new user invites. + * + * @param roomId the room id + * @param shouldShareHistory The boolean flag + */ + fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) + + /** + * Sets a boolean flag that will determine whether or not this device should encrypt Events for + * invited members. + * + * @param roomId the room id + * @param shouldEncryptForInvitedMembers The boolean flag + */ + fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + + /** + * Define if encryption keys should be sent to unverified devices in this room. + * + * @param roomId the roomId + * @param block if true will not send keys to unverified devices + */ + fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + fun getLiveGlobalCryptoConfig(): LiveData + + /** + * @return true to unilaterally blacklist all unverified devices. + */ + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + /** + * A live status regarding sharing keys for unverified devices in this room. + * + * @return Live status + */ + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData + + /** + * Tell if unverified devices should be blacklisted when sending keys. + * + * @return true if should not send keys to unverified devices + */ + fun getBlockUnverifiedDevices(roomId: String): Boolean + + /** + * Retrieve a device by its identity key. + * + * @param userId the device owner + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? + + /** + * Retrieve an inbound group session. + * Used in rust for lazy migration + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt deleted file mode 100644 index 0305f73a7..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ /dev/null @@ -1,602 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmOutboundGroupSession - -/** - * The crypto data store. - */ -internal interface IMXCryptoStore { - - /** - * @return the device id - */ - fun getDeviceId(): String - - /** - * @return the olm account - */ - fun doWithOlmAccount(block: (OlmAccount) -> T): T - - fun getOrCreateOlmAccount(): OlmAccount - - /** - * Retrieve the known inbound group sessions. - * - * @return the list of all known group sessions, to export them. - */ - fun getInboundGroupSessions(): List - - /** - * Retrieve the known inbound group sessions for the specified room. - * - * @param roomId The roomId that the sessions will be returned - * @return the list of all known group sessions, for the provided roomId - */ - fun getInboundGroupSessions(roomId: String): List - - /** - * @return true to unilaterally blacklist all unverified devices. - */ - fun getGlobalBlacklistUnverifiedDevices(): Boolean - - /** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. - * If false, it can still be overridden per-room. - * If true, it overrides the per-room settings. - * - * @param block true to unilaterally blacklist all - */ - fun setGlobalBlacklistUnverifiedDevices(block: Boolean) - - /** - * Enable or disable key gossiping. - * Default is true. - * If set to false this device won't send key_request nor will accept key forwarded - */ - fun enableKeyGossiping(enable: Boolean) - - fun isKeyGossipingEnabled(): Boolean - - /** - * As per MSC3061. - * If true will make it possible to share part of e2ee room history - * on invite depending on the room visibility setting. - */ - fun enableShareKeyOnInvite(enable: Boolean) - - /** - * As per MSC3061. - * If true will make it possible to share part of e2ee room history - * on invite depending on the room visibility setting. - */ - fun isShareKeysOnInviteEnabled(): Boolean - - /** - * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. - * - * @return the room Ids list - */ - fun getRoomsListBlacklistUnverifiedDevices(): List - - /** - * A live status regarding sharing keys for unverified devices in this room. - * - * @return Live status - */ - fun getLiveBlockUnverifiedDevices(roomId: String): LiveData - - /** - * Tell if unverified devices should be blacklisted when sending keys. - * - * @return true if should not send keys to unverified devices - */ - fun getBlockUnverifiedDevices(roomId: String): Boolean - - /** - * Define if encryption keys should be sent to unverified devices in this room. - * - * @param roomId the roomId - * @param block if true will not send keys to unverified devices - */ - fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) - - /** - * Get the current keys backup version. - */ - fun getKeyBackupVersion(): String? - - /** - * Set the current keys backup version. - * - * @param keyBackupVersion the keys backup version or null to delete it - */ - fun setKeyBackupVersion(keyBackupVersion: String?) - - /** - * Get the current keys backup local data. - */ - fun getKeysBackupData(): KeysBackupDataEntity? - - /** - * Set the keys backup local data. - * - * @param keysBackupData the keys backup local data, or null to erase data - */ - fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) - - /** - * @return the devices statuses map (userId -> tracking status) - */ - fun getDeviceTrackingStatuses(): Map - - /** - * Indicate if the store contains data for the passed account. - * - * @return true means that the user enabled the crypto in a previous session - */ - fun hasData(): Boolean - - /** - * Delete the crypto store for the passed credentials. - */ - fun deleteStore() - - /** - * open any existing crypto store. - */ - fun open() - - /** - * Close the store. - */ - fun close() - - /** - * Store the device id. - * - * @param deviceId the device id - */ - fun storeDeviceId(deviceId: String) - - /** - * Store the end to end account for the logged-in user. - */ - fun saveOlmAccount() - - /** - * Retrieve a device for a user. - * - * @param userId the user's id. - * @param deviceId the device id. - * @return the device - */ - fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? - - /** - * Retrieve a device by its identity key. - * - * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) - * @return the device or null if not found - */ - fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? - - /** - * Store the known devices for a user. - * - * @param userId The user's id. - * @param devices A map from device id to 'MXDevice' object for the device. - */ - fun storeUserDevices(userId: String, devices: Map?) - - /** - * Store the cross signing keys for the user userId. - */ - fun storeUserIdentity( - userId: String, - userIdentity: UserIdentity - ) - - /** - * Retrieve the known devices for a user. - * - * @param userId The user's id. - * @return The devices map if some devices are known, else null - */ - fun getUserDevices(userId: String): Map? - - fun getUserDeviceList(userId: String): List? - - fun getLiveDeviceList(userId: String): LiveData> - - fun getLiveDeviceList(userIds: List): LiveData> - - // TODO temp - fun getLiveDeviceList(): LiveData> - - fun getLiveDeviceWithId(deviceId: String): LiveData> - - fun getMyDevicesInfo(): List - - fun getLiveMyDevicesInfo(): LiveData> - - fun getLiveMyDevicesInfo(deviceId: String): LiveData> - - fun saveMyDevicesInfo(info: List) - - /** - * Store the crypto algorithm for a room. - * - * @param roomId the id of the room. - * @param algorithm the algorithm. - */ - fun storeRoomAlgorithm(roomId: String, algorithm: String?) - - /** - * Provides the algorithm used in a dedicated room. - * - * @param roomId the room id - * @return the algorithm, null is the room is not encrypted - */ - fun getRoomAlgorithm(roomId: String): String? - - /** - * This is a bit different than isRoomEncrypted. - * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not). - * But the crypto layer has additional guaranty to ensure that encryption would never been reverted. - * It's defensive coding out of precaution (if ever state is reset). - */ - fun roomWasOnceEncrypted(roomId: String): Boolean - - fun shouldEncryptForInvitedMembers(roomId: String): Boolean - - /** - * Sets a boolean flag that will determine whether or not this device should encrypt Events for - * invited members. - * - * @param roomId the room id - * @param shouldEncryptForInvitedMembers The boolean flag - */ - fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) - - fun shouldShareHistory(roomId: String): Boolean - - /** - * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) - * will be shared to new user invites. - * - * @param roomId the room id - * @param shouldShareHistory The boolean flag - */ - fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) - - /** - * Store a session between the logged-in user and another device. - * - * @param olmSessionWrapper the end-to-end session. - * @param deviceKey the public key of the other device. - */ - fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) - - /** - * Retrieve all end-to-end session ids between our own device and another - * device. - * - * @param deviceKey the public key of the other device. - * @return A set of sessionId, or null if device is not known - */ - fun getDeviceSessionIds(deviceKey: String): List? - - /** - * Retrieve an end-to-end session between our own device and another - * device. - * - * @param sessionId the session Id. - * @param deviceKey the public key of the other device. - * @return The Base64 end-to-end session, or null if not found - */ - fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? - - /** - * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist. - * - * @param deviceKey the public key of the other device. - * @return last used sessionId, or null if not found - */ - fun getLastUsedSessionId(deviceKey: String): String? - - /** - * Store inbound group sessions. - * - * @param sessions the inbound group sessions to store. - */ - fun storeInboundGroupSessions(sessions: List) - - /** - * Retrieve an inbound group session. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return an inbound group session. - */ - fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? - - /** - * Retrieve an inbound group session, filtering shared history. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param sharedHistory filter inbound session with respect to shared history field - * @return an inbound group session. - */ - fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? - - /** - * Get the current outbound group session for this encrypted room. - */ - fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? - - /** - * Get the current outbound group session for this encrypted room. - */ - fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) - - /** - * Remove an inbound group session. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - */ - fun removeInboundGroupSession(sessionId: String, senderKey: String) - - /* ========================================================================================== - * Keys backup - * ========================================================================================== */ - - /** - * Mark all inbound group sessions as not backed up. - */ - fun resetBackupMarkers() - - /** - * Mark inbound group sessions as backed up on the user homeserver. - * - * @param olmInboundGroupSessionWrappers the sessions - */ - fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) - - /** - * Retrieve inbound group sessions that are not yet backed up. - * - * @param limit the maximum number of sessions to return. - * @return an array of non backed up inbound group sessions. - */ - fun inboundGroupSessionsToBackup(limit: Int): List - - /** - * Number of stored inbound group sessions. - * - * @param onlyBackedUp if true, count only session marked as backed up. - * @return a count. - */ - fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int - - /** - * Save the device statuses. - * - * @param deviceTrackingStatuses the device tracking statuses - */ - fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) - - /** - * Get the tracking status of a specified userId devices. - * - * @param userId the user id - * @param defaultValue the default value - * @return the tracking status - */ - fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int - - /** - * Look for an existing outgoing room key request, and if none is found, return null. - * - * @param requestBody the request body - * @return an OutgoingRoomKeyRequest instance or null - */ - fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? - fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? - fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List - - /** - * Look for an existing outgoing room key request, and if none is found, add a new one. - * - * @param requestBody the request - * @param recipients list of recipients - * @param fromIndex start index - * @return either the same instance as passed in, or the existing one. - */ - fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest - fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) - fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) - fun updateOutgoingRoomKeyReply( - roomId: String, - sessionId: String, - algorithm: String, - senderKey: String, - fromDevice: String?, - event: Event - ) - - fun deleteOutgoingRoomKeyRequest(requestId: String) - fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) - - fun saveIncomingKeyRequestAuditTrail( - requestId: String, - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - fromUser: String, - fromDevice: String - ) - - fun saveWithheldAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - code: WithHeldCode, - userId: String, - deviceId: String - ) - - fun saveForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) - - fun saveIncomingForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) - - fun addNewSessionListener(listener: NewSessionListener) - - fun removeSessionListener(listener: NewSessionListener) - - // ============================================= - // CROSS SIGNING - // ============================================= - - /** - * Gets the current crosssigning info. - */ - fun getMyCrossSigningInfo(): MXCrossSigningInfo? - - fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) - - fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? - fun getLiveCrossSigningInfo(userId: String): LiveData> - fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) - - fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) - - fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) - fun storeMSKPrivateKey(msk: String?) - fun storeSSKPrivateKey(ssk: String?) - fun storeUSKPrivateKey(usk: String?) - - fun getCrossSigningPrivateKeys(): PrivateKeysInfo? - fun getLiveCrossSigningPrivateKeys(): LiveData> - - fun getGlobalCryptoConfig(): GlobalCryptoConfig - fun getLiveGlobalCryptoConfig(): LiveData - - fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) - fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? - - fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) - fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) - - fun clearOtherUserTrust() - - fun updateUsersTrust(check: (String) -> Boolean) - - fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) - fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? - - fun markedSessionAsShared( - roomId: String?, - sessionId: String, - userId: String, - deviceId: String, - deviceIdentityKey: String, - chainIndex: Int - ) - - /** - * Query for information on this session sharing history. - * @return SharedSessionResult - * if found is true then this session was initialy shared with that user|device, - * in this case chainIndex is not nullindicates the ratchet position. - * In found is false, chainIndex is null - */ - fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): SharedSessionResult - data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) - - fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap - // Dev tools - - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> - fun getGossipingEvents(): List - - fun setDeviceKeysUploaded(uploaded: Boolean) - fun areDeviceKeysUploaded(): Boolean - fun tidyUpDataBase() - fun getOutgoingRoomKeyRequests(inStates: Set): List - - /** - * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. - */ - fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) - - /** - * Store a bunch of data related to the users. @See [UserDataToStore]. - */ - fun storeData(userDataToStore: UserDataToStore) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt new file mode 100644 index 000000000..bf9a5ebf1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator +import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransaction +import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransactionAsync +import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CryptoRoomInfoMapper +import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.query.getById +import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) + +/** + * In the transition phase, the rust SDK is still using parts to the realm crypto store, + * this should be removed after full migration. + */ +@SessionScope +internal class RustCryptoStore @Inject constructor( + @CryptoDatabase private val realmConfiguration: RealmConfiguration, + private val clock: Clock, + @UserId private val userId: String, + @DeviceId private val deviceId: String, + private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper, + private val olmMachine: dagger.Lazy, + private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, +) : IMXCommonCryptoStore { + + // still needed on rust due to the global crypto settings + init { + // Ensure CryptoMetadataEntity is inserted in DB + doRealmTransaction("init", realmConfiguration) { realm -> + var currentMetadata = realm.where().findFirst() + + var deleteAll = false + + if (currentMetadata != null) { + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + if (currentMetadata.userId != userId || deviceId != currentMetadata.deviceId) { + Timber.w("## open() : Credentials do not match, close this store and delete data") + deleteAll = true + currentMetadata = null + } + } + + if (currentMetadata == null) { + if (deleteAll) { + realm.deleteAll() + } + + // Metadata not found, or database cleaned, create it + realm.createObject(CryptoMetadataEntity::class.java, userId).apply { + deviceId = this@RustCryptoStore.deviceId + } + } + } + } + + /** + * Retrieve a device by its identity key. + * + * @param userId The device owner userId. + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + override fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? { + // XXX make this suspendable? + val knownDevices = runBlocking(matrixCoroutineDispatchers.io) { + olmMachine.get().getUserDevices(userId) + } + return knownDevices + .map { it.toCryptoDeviceInfo() } + .firstOrNull { + it.identityKey() == identityKey + } + } + + /** + * Needed for lazy migration of sessions from the legacy store. + */ + override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.toModel() + } + } + + // ================================================ + // Things that should be migrated to another store than realm + // ================================================ + + private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor() + + private val monarchy = Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .setWriteAsyncExecutor(monarchyWriteAsyncExecutor) + .build() + + override fun open() { + // nop + } + + override fun tidyUpDataBase() { + // These entities are not used in rust actually, but as they are not yet cleaned up, this will do it with time + val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 + doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> + + // Clean the old ones? + realm.where() + .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } + .deleteAllFromRealm() + + // Only keep one month history + + val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L + realm.where() + .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } + .deleteAllFromRealm() + + // Can we do something for WithHeldSessionEntity? + } + } + + override fun close() { + val tasks = monarchyWriteAsyncExecutor.shutdownNow() + Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled") + tryOrNull("Interrupted") { + // Wait 1 minute max + monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) + } + } + + override fun getRoomAlgorithm(roomId: String): String? { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.algorithm + } + } + + override fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? { + return doWithRealm(realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId)?.let { + CryptoRoomInfoMapper.map(it) + } + } + } + + /** + * This is a bit different than isRoomEncrypted. + * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not). + * But the crypto layer has additional guaranty to ensure that encryption would never been reverted. + * It's defensive coding out of precaution (if ever state is reset). + */ + override fun roomWasOnceEncrypted(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false + } + } + + override fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) { + doRealmTransaction("setAlgorithmInfo", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> + entity.algorithm = encryption?.algorithm + // store anyway the new algorithm, but mark the room + // as having been encrypted once whatever, this can never + // go back to false + if (encryption?.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { + entity.wasEncryptedOnce = true + entity.rotationPeriodMs = encryption.rotationPeriodMs + entity.rotationPeriodMsgs = encryption.rotationPeriodMsgs + } + } + } + } + + override fun saveMyDevicesInfo(info: List) { + val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) } + doRealmTransactionAsync(realmConfiguration) { realm -> + realm.where().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + + override fun getMyDevicesInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } + ) + } + + override fun getLiveMyDevicesInfo(deviceId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId) + }, + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } + ) + + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { + if (cryptoStoreAggregator.isEmpty()) { + return + } + doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> + // setShouldShareHistory + cryptoStoreAggregator.setShouldShareHistoryData.forEach { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room ${it.key} is ${it.value}") + CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value + } + // setShouldEncryptForInvitedMembers + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value + } + } + } + + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers + } + ?: false + } + + override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room $roomId is $shouldShareHistory") + doRealmTransaction("setShouldShareHistory", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory + } + } + + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { + doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers + } + } + + override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { + doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId) + ?.blacklistUnverifiedDevices = block + } + } + + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices = block + } + } + + override fun getLiveGlobalCryptoConfig(): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) + } + } + + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices + } ?: false + } + + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + }, + { + it.blacklistUnverifiedDevices + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: false + } + } + + override fun getBlockUnverifiedDevices(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() + ?.blacklistUnverifiedDevices ?: false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt deleted file mode 100644 index b4368467a..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ /dev/null @@ -1,1856 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList -import com.zhuinden.monarchy.Monarchy -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.Sort -import io.realm.kotlin.createObject -import io.realm.kotlin.where -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig -import org.matrix.android.sdk.api.session.crypto.NewSessionListener -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo -import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity -import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo -import org.matrix.android.sdk.api.session.crypto.model.AuditTrail -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo -import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.TrailType -import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.UserDataToStore -import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper -import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey -import org.matrix.android.sdk.internal.crypto.store.db.model.deleteOnCascade -import org.matrix.android.sdk.internal.crypto.store.db.query.create -import org.matrix.android.sdk.internal.crypto.store.db.query.delete -import org.matrix.android.sdk.internal.crypto.store.db.query.get -import org.matrix.android.sdk.internal.crypto.store.db.query.getById -import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate -import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.di.CryptoDatabase -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.extensions.clearWith -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmException -import org.matrix.olm.OlmOutboundGroupSession -import timber.log.Timber -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) - -@SessionScope -internal class RealmCryptoStore @Inject constructor( - @CryptoDatabase private val realmConfiguration: RealmConfiguration, - private val crossSigningKeysMapper: CrossSigningKeysMapper, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val clock: Clock, - private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper, -) : IMXCryptoStore { - - /* ========================================================================================== - * Memory cache, to correctly release JNI objects - * ========================================================================================== */ - - // A realm instance, for faster future getInstance. Do not use it - private var realmLocker: Realm? = null - - // The olm account - private var olmAccount: OlmAccount? = null - - private val newSessionListeners = ArrayList() - - override fun addNewSessionListener(listener: NewSessionListener) { - if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener) - } - - override fun removeSessionListener(listener: NewSessionListener) { - newSessionListeners.remove(listener) - } - - private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor() - - private val monarchy = Monarchy.Builder() - .setRealmConfiguration(realmConfiguration) - .setWriteAsyncExecutor(monarchyWriteAsyncExecutor) - .build() - - init { - // Ensure CryptoMetadataEntity is inserted in DB - doRealmTransaction("init", realmConfiguration) { realm -> - var currentMetadata = realm.where().findFirst() - - var deleteAll = false - - if (currentMetadata != null) { - // Check credentials - // The device id may not have been provided in credentials. - // Check it only if provided, else trust the stored one. - if (currentMetadata.userId != userId || - (deviceId != null && deviceId != currentMetadata.deviceId)) { - Timber.w("## open() : Credentials do not match, close this store and delete data") - deleteAll = true - currentMetadata = null - } - } - - if (currentMetadata == null) { - if (deleteAll) { - realm.deleteAll() - } - - // Metadata not found, or database cleaned, create it - realm.createObject(CryptoMetadataEntity::class.java, userId).apply { - deviceId = this@RealmCryptoStore.deviceId - } - } - } - } - /* ========================================================================================== - * Other data - * ========================================================================================== */ - - override fun hasData(): Boolean { - return doWithRealm(realmConfiguration) { - !it.isEmpty && - // Check if there is a MetaData object - it.where().count() > 0 - } - } - - override fun deleteStore() { - doRealmTransaction("deleteStore", realmConfiguration) { - it.deleteAll() - } - } - - override fun open() { - synchronized(this) { - if (realmLocker == null) { - realmLocker = Realm.getInstance(realmConfiguration) - } - } - } - - override fun close() { - // Ensure no async request will be run later - val tasks = monarchyWriteAsyncExecutor.shutdownNow() - Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled") - tryOrNull("Interrupted") { - // Wait 1 minute max - monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) - } - - olmAccount?.releaseAccount() - - realmLocker?.close() - realmLocker = null - } - - override fun storeDeviceId(deviceId: String) { - doRealmTransaction("storeDeviceId", realmConfiguration) { - it.where().findFirst()?.deviceId = deviceId - } - } - - override fun getDeviceId(): String { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.deviceId - } ?: "" - } - - override fun saveOlmAccount() { - doRealmTransaction("saveOlmAccount", realmConfiguration) { - it.where().findFirst()?.putOlmAccount(olmAccount) - } - } - - /** - * Olm account access should be synchronized. - */ - override fun doWithOlmAccount(block: (OlmAccount) -> T): T { - return olmAccount!!.let { olmAccount -> - synchronized(olmAccount) { - block.invoke(olmAccount) - } - } - } - - @Synchronized - override fun getOrCreateOlmAccount(): OlmAccount { - doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) { - val metaData = it.where().findFirst() - val existing = metaData!!.getOlmAccount() - if (existing == null) { - Timber.d("## Crypto Creating olm account") - val created = OlmAccount() - metaData.putOlmAccount(created) - olmAccount = created - } else { - Timber.d("## Crypto Access existing account") - olmAccount = existing - } - } - return olmAccount!! - } - - override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) - .findFirst() - ?.let { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - } - } - - override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) - .findAll() - .mapNotNull { CryptoMapper.mapToModel(it) } - .firstOrNull { - it.identityKey() == identityKey - } - } - } - - override fun storeUserDevices(userId: String, devices: Map?) { - doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> - storeUserDevices(realm, userId, devices) - } - } - - private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) { - if (devices == null) { - Timber.d("Remove user $userId") - // Remove the user - UserEntity.delete(realm, userId) - } else { - val userEntity = UserEntity.getOrCreate(realm, userId) - // First delete the removed devices - val deviceIds = devices.keys - userEntity.devices.toTypedArray().iterator().let { - while (it.hasNext()) { - val deviceInfoEntity = it.next() - if (deviceInfoEntity.deviceId !in deviceIds) { - Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") - deviceInfoEntity.deleteOnCascade() - } - } - } - // Then update existing devices or add new one - devices.values.forEach { cryptoDeviceInfo -> - val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } - if (existingDeviceInfoEntity == null) { - // Add the device - Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") - val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = clock.epochMillis() - userEntity.devices.add(newEntity) - } else { - // Update the device - Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") - CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) - } - } - } - } - - override fun storeUserIdentity( - userId: String, - userIdentity: UserIdentity, - ) { - doRealmTransaction("storeUserIdentity", realmConfiguration) { realm -> - storeUserIdentity(realm, userId, userIdentity) - } - } - - private fun storeUserIdentity( - realm: Realm, - userId: String, - userIdentity: UserIdentity, - ) { - UserEntity.getOrCreate(realm, userId) - .let { userEntity -> - if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) { - // The user has disabled cross signing? - userEntity.crossSigningInfoEntity?.deleteOnCascade() - userEntity.crossSigningInfoEntity = null - } else { - var shouldResetMyDevicesLocalTrust = false - CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> - // What should we do if we detect a change of the keys? - val existingMaster = signingInfo.getMasterKey() - if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey) - } else { - Timber.d("## CrossSigning MSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey) - signingInfo.setMasterKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my msk has changed! clear my private key - // Could we have some race here? e.g I am the one that did change the keys - // could i get this update to early and clear the private keys? - // -> initializeCrossSigning is guarding for that by storing all at once - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = null - } - } - } - - val existingSelfSigned = signingInfo.getSelfSignedKey() - if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey) - } else { - Timber.d("## CrossSigning SSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey) - signingInfo.setSelfSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my ssk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = null - } - } - } - - // Only for me - if (userIdentity.userSigningKey != null) { - val existingUSK = signingInfo.getUserSigningKey() - if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey) - } else { - Timber.d("## CrossSigning USK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey) - signingInfo.setUserSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my usk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignUserPrivateKey = null - } - } - } - } - - // When my cross signing keys are reset, we consider clearing all existing device trust - if (shouldResetMyDevicesLocalTrust) { - realm.where() - .equalTo(UserEntityFields.USER_ID, this.userId) - .findFirst() - ?.devices?.forEach { - it?.trustLevelEntity?.crossSignedVerified = false - it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId - } - } - userEntity.crossSigningInfoEntity = signingInfo - } - } - } - } - - override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findFirst() - ?.let { - PrivateKeysInfo( - master = it.xSignMasterPrivateKey, - selfSigned = it.xSignSelfSignedPrivateKey, - user = it.xSignUserPrivateKey - ) - } - } - } - - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - }, - { - PrivateKeysInfo( - master = it.xSignMasterPrivateKey, - selfSigned = it.xSignSelfSignedPrivateKey, - user = it.xSignUserPrivateKey - ) - } - ) - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun getGlobalCryptoConfig(): GlobalCryptoConfig { - return doWithRealm(realmConfiguration) { realm -> - realm.where().findFirst() - ?.let { - GlobalCryptoConfig( - globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, - globalEnableKeyGossiping = it.globalEnableKeyGossiping, - enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite - ) - } ?: GlobalCryptoConfig(false, false, false) - } - } - - override fun getLiveGlobalCryptoConfig(): LiveData { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - }, - { - GlobalCryptoConfig( - globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, - globalEnableKeyGossiping = it.globalEnableKeyGossiping, - enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite - ) - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) - } - } - - override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { - Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") - doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = msk - xSignUserPrivateKey = usk - xSignSelfSignedPrivateKey = ssk - } - } - } - - override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { - doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - keyBackupRecoveryKey = recoveryKey - keyBackupRecoveryKeyVersion = version - } - } - } - - override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findFirst() - ?.let { - val key = it.keyBackupRecoveryKey - val version = it.keyBackupRecoveryKeyVersion - if (!key.isNullOrBlank() && !version.isNullOrBlank()) { - SavedKeyBackupKeyInfo(recoveryKey = key, version = version) - } else { - null - } - } - } - } - - override fun storeMSKPrivateKey(msk: String?) { - Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ") - doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = msk - } - } - } - - override fun storeSSKPrivateKey(ssk: String?) { - Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ") - doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = ssk - } - } - } - - override fun storeUSKPrivateKey(usk: String?) { - Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ") - doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm -> - realm.where().findFirst()?.apply { - xSignUserPrivateKey = usk - } - } - } - - override fun getUserDevices(userId: String): Map? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - ?.associateBy { cryptoDevice -> - cryptoDevice.deviceId - } - } - } - - override fun getUserDeviceList(userId: String): List? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.devices - ?.map { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) - } - } - } - - override fun getLiveDeviceList(userId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - .equalTo(UserEntityFields.USER_ID, userId) - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.firstOrNull().orEmpty() - } - } - - override fun getLiveDeviceList(userIds: List): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm - .where() - .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray()) - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.flatten() - } - } - - override fun getLiveDeviceList(): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - }, - { entity -> - entity.devices.map { CryptoMapper.mapToModel(it) } - } - ) - return Transformations.map(liveData) { - it.firstOrNull().orEmpty() - } - } - - override fun getLiveDeviceWithId(deviceId: String): LiveData> { - return Transformations.map(getLiveDeviceList()) { devices -> - devices.firstOrNull { it.deviceId == deviceId }.toOptional() - } - } - - override fun getMyDevicesInfo(): List { - return monarchy.fetchAllCopiedSync { - it.where() - }.map { - DeviceInfo( - deviceId = it.deviceId, - lastSeenIp = it.lastSeenIp, - lastSeenTs = it.lastSeenTs, - displayName = it.displayName - ) - } - } - - override fun getLiveMyDevicesInfo(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - }, - { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } - ) - } - - override fun getLiveMyDevicesInfo(deviceId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId) - }, - { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } - ) - - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun saveMyDevicesInfo(info: List) { - val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) } - doRealmTransactionAsync(realmConfiguration) { realm -> - realm.where().findAll().deleteAllFromRealm() - entities.forEach { - realm.insertOrUpdate(it) - } - } - } - - override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { - doRealmTransaction("storeRoomAlgorithm", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> - entity.algorithm = algorithm - // store anyway the new algorithm, but mark the room - // as having been encrypted once whatever, this can never - // go back to false - if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { - entity.wasEncryptedOnce = true - } - } - } - } - - override fun getRoomAlgorithm(roomId: String): String? { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.algorithm - } - } - - override fun roomWasOnceEncrypted(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false - } - } - - override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers - } - ?: false - } - - override fun shouldShareHistory(roomId: String): Boolean { - if (!isShareKeysOnInviteEnabled()) return false - return doWithRealm(realmConfiguration) { - CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory - } - ?: false - } - - override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { - doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers - } - } - - override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { - Timber.tag(loggerTag.value) - .v("setShouldShareHistory for room $roomId is $shouldShareHistory") - doRealmTransaction("setShouldShareHistory", realmConfiguration) { - CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory - } - } - - override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) { - var sessionIdentifier: String? = null - - try { - sessionIdentifier = olmSessionWrapper.olmSession.sessionIdentifier() - } catch (e: OlmException) { - Timber.e(e, "## storeSession() : sessionIdentifier failed") - } - - if (sessionIdentifier != null) { - val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) - - doRealmTransaction("storeSession", realmConfiguration) { - val realmOlmSession = OlmSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - this.deviceKey = deviceKey - putOlmSession(olmSessionWrapper.olmSession) - lastReceivedMessageTs = olmSessionWrapper.lastReceivedMessageTs - } - - it.insertOrUpdate(realmOlmSession) - } - } - } - - override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? { - val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - } - ?.let { - val olmSession = it.getOlmSession() - if (olmSession != null && it.sessionId != null) { - return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs) - } - null - } - } - - override fun getLastUsedSessionId(deviceKey: String): String? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) - .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) - .findFirst() - ?.sessionId - } - } - - override fun getDeviceSessionIds(deviceKey: String): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) - .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) - .findAll() - .mapNotNull { sessionEntity -> - sessionEntity.sessionId - } - } - } - - override fun storeInboundGroupSessions(sessions: List) { - if (sessions.isEmpty()) { - return - } - - doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm -> - sessions.forEach { wrapper -> - - val sessionIdentifier = try { - wrapper.session.sessionIdentifier() - } catch (e: OlmException) { - Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed") - return@forEach - } - -// val shouldShareHistory = session.roomId?.let { roomId -> -// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory -// } ?: false - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey) - - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - store(wrapper) - backedUp = existing?.backedUp ?: false - } - - Timber.v("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key") - realm.insertOrUpdate(realmOlmInboundGroupSession) - } - } - } - - override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - ?.toModel() - } - } - - override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory) - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - ?.toModel() - } - } - - override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - .findFirst()?.outboundSessionInfo?.let { entity -> - entity.getOutboundGroupSession()?.let { - OutboundGroupSessionWrapper( - it, - entity.creationTime ?: 0, - entity.shouldShareHistory - ) - } - } - } - } - - override fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) { - // we can do this async, as it's just for restoring on next launch - // the olmdevice is caching the active instance - // this is called for each sent message (so not high frequency), thus we can use basic realm async without - // risk of reaching max async operation limit? - doRealmTransactionAsync(realmConfiguration) { realm -> - CryptoRoomEntity.getById(realm, roomId)?.let { entity -> - // we should delete existing outbound session info if any - entity.outboundSessionInfo?.deleteFromRealm() - - if (outboundGroupSession != null) { - val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply { - creationTime = clock.epochMillis() - // Store the room history visibility on the outbound session creation - shouldShareHistory = entity.shouldShareHistory - putOutboundGroupSession(outboundGroupSession) - } - entity.outboundSessionInfo = info - } - } - } - } - -// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean { -// return doWithRealm(realmConfiguration) { realm -> -// CryptoRoomEntity.getById(realm, roomId)?.let { entity -> -// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory -// } -// } ?: false -// } - - /** - * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, - * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management. - */ - override fun getInboundGroupSessions(): List { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun getInboundGroupSessions(roomId: String): List { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId) - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun removeInboundGroupSession(sessionId: String, senderKey: String) { - val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - - doRealmTransaction("removeInboundGroupSession", realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findAll() - .deleteAllFromRealm() - } - } - - /* ========================================================================================== - * Keys backup - * ========================================================================================== */ - - override fun getKeyBackupVersion(): String? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - }?.backupVersion - } - - override fun setKeyBackupVersion(keyBackupVersion: String?) { - doRealmTransaction("setKeyBackupVersion", realmConfiguration) { - it.where().findFirst()?.backupVersion = keyBackupVersion - } - } - - override fun getKeysBackupData(): KeysBackupDataEntity? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where().findFirst() - } - } - - override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { - doRealmTransaction("setKeysBackupData", realmConfiguration) { - if (keysBackupData == null) { - // Clear the table - it.where() - .findAll() - .deleteAllFromRealm() - } else { - // Only one object - it.copyToRealmOrUpdate(keysBackupData) - } - } - } - - override fun resetBackupMarkers() { - doRealmTransaction("resetBackupMarkers", realmConfiguration) { - it.where() - .findAll() - .map { inboundGroupSession -> - inboundGroupSession.backedUp = false - } - } - } - - override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List) { - if (olmInboundGroupSessionWrappers.isEmpty()) { - return - } - - doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm -> - olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> - try { - val sessionIdentifier = - tryOrNull("Failed to get session identifier") { - olmInboundGroupSessionWrapper.session.sessionIdentifier() - } ?: return@forEach - val key = OlmInboundGroupSessionEntity.createPrimaryKey( - sessionIdentifier, - olmInboundGroupSessionWrapper.sessionData.senderKey - ) - - val existing = realm.where() - .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) - .findFirst() - - if (existing != null) { - existing.backedUp = true - } else { - // ... might be in cache but not yet persisted, create a record to persist backedup state - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - store(olmInboundGroupSessionWrapper) - backedUp = true - } - - realm.insertOrUpdate(realmOlmInboundGroupSession) - } - } catch (e: OlmException) { - Timber.e(e, "OlmException") - } - } - } - } - - override fun inboundGroupSessionsToBackup(limit: Int): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) - .limit(limit.toLong()) - .findAll() - .mapNotNull { it.toModel() } - } - } - - override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { - return doWithRealm(realmConfiguration) { - it.where() - .apply { - if (onlyBackedUp) { - equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true) - } - } - .count() - .toInt() - } - } - - override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { - it.where().findFirst()?.globalBlacklistUnverifiedDevices = block - } - } - - override fun enableKeyGossiping(enable: Boolean) { - doRealmTransaction("enableKeyGossiping", realmConfiguration) { - it.where().findFirst()?.globalEnableKeyGossiping = enable - } - } - - override fun isKeyGossipingEnabled(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.globalEnableKeyGossiping - } ?: true - } - - override fun getGlobalBlacklistUnverifiedDevices(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.globalBlacklistUnverifiedDevices - } ?: false - } - - override fun isShareKeysOnInviteEnabled(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.enableKeyForwardingOnInvite - } ?: false - } - - override fun enableShareKeyOnInvite(enable: Boolean) { - doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) { - it.where().findFirst()?.enableKeyForwardingOnInvite = enable - } - } - - override fun setDeviceKeysUploaded(uploaded: Boolean) { - doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) { - it.where().findFirst()?.deviceKeysSentToServer = uploaded - } - } - - override fun areDeviceKeysUploaded(): Boolean { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.deviceKeysSentToServer - } ?: false - } - - override fun getRoomsListBlacklistUnverifiedDevices(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) - .findAll() - .mapNotNull { cryptoRoom -> - cryptoRoom.roomId - } - } - } - - override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - }, - { - it.blacklistUnverifiedDevices - } - ) - return Transformations.map(liveData) { - it.firstOrNull() ?: false - } - } - - override fun getBlockUnverifiedDevices(roomId: String): Boolean { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) - .findFirst() - ?.blacklistUnverifiedDevices ?: false - } - } - - override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { - doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> - CryptoRoomEntity.getById(realm, roomId) - ?.blacklistUnverifiedDevices = block - } - } - - override fun getDeviceTrackingStatuses(): Map { - return doWithRealm(realmConfiguration) { - it.where() - .findAll() - .associateBy { user -> - user.userId!! - } - .mapValues { entry -> - entry.value.deviceTrackingStatus - } - } - } - - override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { - doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) { - deviceTrackingStatuses - .map { entry -> - UserEntity.getOrCreate(it, entry.key) - .deviceTrackingStatus = entry.value - } - } - } - - override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(UserEntityFields.USER_ID, userId) - .findFirst() - ?.deviceTrackingStatus - } - ?: defaultValue - } - - override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) - }.map { - it.toOutgoingKeyRequest() - }.firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm && - it.requestBody?.roomId == requestBody.roomId && - it.requestBody?.senderKey == requestBody.senderKey && - it.requestBody?.sessionId == requestBody.sessionId - } - } - - override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - }.map { - it.toOutgoingKeyRequest() - }.firstOrNull() - } - - override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List { - // TODO this annoying we have to load all - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) - }.map { - it.toOutgoingKeyRequest() - }.filter { - it.requestBody?.algorithm == algorithm && - it.requestBody?.senderKey == senderKey - } - } - - override fun getGossipingEventsTrail(): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) - } - val dataSourceFactory = realmDataSourceFactory.map { - AuditTrailMapper.map(it) - // mm we can't map not null... - ?: createUnknownTrail() - } - return monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - } - - private fun createUnknownTrail() = AuditTrail( - clock.epochMillis(), - TrailType.Unknown, - IncomingKeyRequestInfo( - "", - "", - "", - "", - "", - "", - "", - ) - ) - - override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where() - .equalTo(AuditTrailEntityFields.TYPE, type.name) - .sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) - } - val dataSourceFactory = realmDataSourceFactory.map { entity -> - (AuditTrailMapper.map(entity) - // mm we can't map not null... - ?: createUnknownTrail() - ).let { mapper.invoke(it) } - } - return monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - } - - override fun getGossipingEvents(): List { - return monarchy.fetchAllCopiedSync { realm -> - realm.where() - }.mapNotNull { - AuditTrailMapper.map(it) - } - } - - override fun getOrAddOutgoingRoomKeyRequest( - requestBody: RoomKeyRequestBody, - recipients: Map>, - fromIndex: Int - ): OutgoingKeyRequest { - // Insert the request and return the one passed in parameter - lateinit var request: OutgoingKeyRequest - doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm -> - - val existing = realm.where() - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) - .findAll() - .map { - it.toOutgoingKeyRequest() - }.also { - if (it.size > 1) { - // there should be one or zero but not more, worth warning - Timber.tag(loggerTag.value).w("There should not be more than one active key request per session") - } - } - .firstOrNull { - it.requestBody?.algorithm == requestBody.algorithm && - it.requestBody?.sessionId == requestBody.sessionId && - it.requestBody?.senderKey == requestBody.senderKey && - it.requestBody?.roomId == requestBody.roomId - } - - if (existing == null) { - request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply { - this.requestId = RequestIdHelper.createUniqueRequestId() - this.setRecipients(recipients) - this.requestedIndex = fromIndex - this.requestState = OutgoingRoomKeyRequestState.UNSENT - this.setRequestBody(requestBody) - this.creationTimeStamp = clock.epochMillis() - }.toOutgoingKeyRequest() - } else { - request = existing - } - } - return request - } - - override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { - doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.apply { - this.requestState = newState - if (newState == OutgoingRoomKeyRequestState.UNSENT) { - // clear the old replies - this.replies.deleteAllFromRealm() - } - } - } - } - - override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { - doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.apply { - this.requestedIndex = newIndex - } - } - } - - override fun updateOutgoingRoomKeyReply( - roomId: String, - sessionId: String, - algorithm: String, - senderKey: String, - fromDevice: String?, - event: Event - ) { - doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) - .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) - .findAll().firstOrNull { entity -> - entity.toOutgoingKeyRequest().let { - it.requestBody?.senderKey == senderKey && - it.requestBody?.algorithm == algorithm - } - }?.apply { - event.senderId?.let { addReply(it, fromDevice, event) } - } - } - } - - override fun deleteOutgoingRoomKeyRequest(requestId: String) { - doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst()?.deleteOnCascade() - } - } - - override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { - doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) - .findAll() - // I delete like this because I want to cascade delete replies? - .onEach { it.deleteOnCascade() } - } - } - - override fun saveIncomingKeyRequestAuditTrail( - requestId: String, - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - fromUser: String, - fromDevice: String - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = TrailType.IncomingKeyRequest.name - val info = IncomingKeyRequestInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - userId = fromUser, - deviceId = fromDevice, - requestId = requestId - ) - MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - override fun saveWithheldAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - code: WithHeldCode, - userId: String, - deviceId: String - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = TrailType.OutgoingKeyWithheld.name - val info = WithheldInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - code = code, - userId = userId, - deviceId = deviceId - ) - MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - override fun saveForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) { - saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false) - } - - override fun saveIncomingForwardKeyAuditTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long? - ) { - saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true) - } - - private fun saveForwardKeyTrail( - roomId: String, - sessionId: String, - senderKey: String, - algorithm: String, - userId: String, - deviceId: String, - chainIndex: Long?, - incoming: Boolean - ) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - realm.createObject().apply { - this.ageLocalTs = now - this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name - val info = ForwardInfo( - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey, - alg = algorithm, - userId = userId, - deviceId = deviceId, - chainIndex = chainIndex - ) - MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let { - this.contentJson = it - } - } - } - } - - /* ========================================================================================== - * Cross Signing - * ========================================================================================== */ - override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { - return doWithRealm(realmConfiguration) { - it.where().findFirst()?.userId - }?.let { - getCrossSigningInfo(it) - } - } - - override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { - doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm -> - realm.where().findFirst()?.userId?.let { userId -> - addOrUpdateCrossSigningInfo(realm, userId, info) - } - } - } - - override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { - doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm -> - val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - xInfoEntity?.crossSigningKeys?.forEach { info -> - val level = info.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = trusted - newLevel.crossSignedVerified = trusted - info.trustLevelEntity = newLevel - } else { - level.locallyVerified = trusted - level.crossSignedVerified = trusted - } - } - } - } - - override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { - doRealmTransaction("setDeviceTrust", realmConfiguration) { realm -> - realm.where(DeviceInfoEntity::class.java) - .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) - .findFirst()?.let { deviceInfoEntity -> - val trustEntity = deviceInfoEntity.trustLevelEntity - if (trustEntity == null) { - realm.createObject(TrustLevelEntity::class.java).let { - it.locallyVerified = locallyVerified - it.crossSignedVerified = crossSignedVerified - deviceInfoEntity.trustLevelEntity = it - } - } else { - locallyVerified?.let { trustEntity.locallyVerified = it } - trustEntity.crossSignedVerified = crossSignedVerified - } - } - } - } - - override fun clearOtherUserTrust() { - doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm -> - val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) - .findAll() - xInfoEntities?.forEach { info -> - // Need to ignore mine - if (info.userId != userId) { - info.crossSigningKeys.forEach { - it.trustLevelEntity = null - } - } - } - } - } - - override fun updateUsersTrust(check: (String) -> Boolean) { - doRealmTransaction("updateUsersTrust", realmConfiguration) { realm -> - val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) - .findAll() - xInfoEntities?.forEach { xInfoEntity -> - // Need to ignore mine - if (xInfoEntity.userId == userId) return@forEach - val mapped = mapCrossSigningInfoEntity(xInfoEntity) - val currentTrust = mapped.isTrusted() - val newTrust = check(mapped.userId) - if (currentTrust != newTrust) { - xInfoEntity.crossSigningKeys.forEach { info -> - val level = info.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = newTrust - newLevel.crossSignedVerified = newTrust - info.trustLevelEntity = newLevel - } else { - level.locallyVerified = newTrust - level.crossSignedVerified = newTrust - } - } - } - } - } - } - - override fun getOutgoingRoomKeyRequests(): List { - return monarchy.fetchAllMappedSync({ realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - }, { entity -> - entity.toOutgoingKeyRequest() - }) - .filterNotNull() - } - - override fun getOutgoingRoomKeyRequests(inStates: Set): List { - return monarchy.fetchAllMappedSync({ realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) - }, { entity -> - entity.toOutgoingKeyRequest() - }) - .filterNotNull() - } - - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm - .where(OutgoingKeyRequestEntity::class.java) - } - val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingKeyRequest() - } - val trail = monarchy.findAllPagedWithChanges( - realmDataSourceFactory, - LivePagedListBuilder( - dataSourceFactory, - PagedList.Config.Builder() - .setPageSize(20) - .setEnablePlaceholders(false) - .setPrefetchDistance(1) - .build() - ) - ) - return trail - } - - override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { - return doWithRealm(realmConfiguration) { realm -> - val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) - .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) - .findFirst() - if (crossSigningInfo == null) { - null - } else { - mapCrossSigningInfoEntity(crossSigningInfo) - } - } - } - - private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { - val userId = xsignInfo.userId ?: "" - return MXCrossSigningInfo( - userId = userId, - crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { - crossSigningKeysMapper.map(userId, it) - }, - wasTrustedOnce = xsignInfo.wasUserVerifiedOnce - ) - } - - override fun getLiveCrossSigningInfo(userId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm: Realm -> - realm.where() - .equalTo(UserEntityFields.USER_ID, userId) - }, - { mapCrossSigningInfoEntity(it) } - ) - return Transformations.map(liveData) { - it.firstOrNull().toOptional() - } - } - - override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { - doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm -> - addOrUpdateCrossSigningInfo(realm, userId, info) - } - } - - override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { - doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm -> - realm.where().findFirst()?.userId?.let { myUserId -> - CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> - val level = xInfoEntity.trustLevelEntity - if (level == null) { - val newLevel = realm.createObject(TrustLevelEntity::class.java) - newLevel.locallyVerified = trusted - xInfoEntity.trustLevelEntity = newLevel - } else { - level.locallyVerified = trusted - } - } - } - } - } - - private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { - if (info == null) { - // Delete known if needed - CrossSigningInfoEntity.get(realm, userId)?.deleteFromRealm() - return null - // TODO notify, we might need to untrust things? - } else { - // Just override existing, caller should check and untrust id needed - val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) - existing.crossSigningKeys.clearWith { it.deleteOnCascade() } - existing.crossSigningKeys.addAll( - info.crossSigningKeys.map { - crossSigningKeysMapper.map(it) - } - ) - return existing - } - } - - override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) { - val roomId = withHeldContent.roomId ?: return - val sessionId = withHeldContent.sessionId ?: return - if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return - doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm -> - WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { - it.code = withHeldContent.code - it.senderKey = withHeldContent.senderKey - it.reason = withHeldContent.reason - } - } - } - - override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? { - return doWithRealm(realmConfiguration) { realm -> - WithHeldSessionEntity.get(realm, roomId, sessionId)?.let { - RoomKeyWithHeldContent( - roomId = roomId, - sessionId = sessionId, - algorithm = it.algorithm, - codeString = it.codeString, - reason = it.reason, - senderKey = it.senderKey - ) - } - } - } - - override fun markedSessionAsShared( - roomId: String?, - sessionId: String, - userId: String, - deviceId: String, - deviceIdentityKey: String, - chainIndex: Int - ) { - doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm -> - SharedSessionEntity.create( - realm = realm, - roomId = roomId, - sessionId = sessionId, - userId = userId, - deviceId = deviceId, - deviceIdentityKey = deviceIdentityKey, - chainIndex = chainIndex - ) - } - } - - override fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): IMXCryptoStore.SharedSessionResult { - return doWithRealm(realmConfiguration) { realm -> - SharedSessionEntity.get( - realm = realm, - roomId = roomId, - sessionId = sessionId, - userId = deviceInfo.userId, - deviceId = deviceInfo.deviceId, - deviceIdentityKey = deviceInfo.identityKey() - )?.let { - IMXCryptoStore.SharedSessionResult(true, it.chainIndex) - } ?: IMXCryptoStore.SharedSessionResult(false, null) - } - } - - override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { - return doWithRealm(realmConfiguration) { realm -> - val result = MXUsersDevicesMap() - SharedSessionEntity.get(realm, roomId, sessionId) - .groupBy { it.userId } - .forEach { (userId, shared) -> - shared.forEach { - result.setObject(userId, it.deviceId, it.chainIndex) - } - } - - result - } - } - - /** - * Some entries in the DB can get a bit out of control with time. - * So we need to tidy up a bit. - */ - override fun tidyUpDataBase() { - val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 - doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> - - // Clean the old ones? - realm.where() - .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } - .deleteAllFromRealm() - - // Only keep one month history - - val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L - realm.where() - .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } - .deleteAllFromRealm() - - // Can we do something for WithHeldSessionEntity? - } - } - - override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { - if (cryptoStoreAggregator.isEmpty()) { - return - } - doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> - // setShouldShareHistory - cryptoStoreAggregator.setShouldShareHistoryData.forEach { - CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value - } - // setShouldEncryptForInvitedMembers - cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { - CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value - } - } - } - - override fun storeData(userDataToStore: UserDataToStore) { - doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm -> - userDataToStore.userDevices.forEach { - storeUserDevices(realm, it.key, it.value) - } - userDataToStore.userIdentities.forEach { - storeUserIdentity(realm, it.key, it.value) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 9129453c8..99734f654 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -37,6 +37,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo020 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo021 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo022 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -49,9 +51,10 @@ import javax.inject.Inject */ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, + private val rustMigrationInfoProvider: RustMigrationInfoProvider, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 20L, + schemaVersion = 22L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -81,5 +84,12 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 18) MigrateCryptoTo018(realm).perform() if (oldVersion < 19) MigrateCryptoTo019(realm).perform() if (oldVersion < 20) MigrateCryptoTo020(realm).perform() + if (oldVersion < 21) MigrateCryptoTo021(realm).perform() + if (oldVersion < 22) MigrateCryptoTo022( + realm, + rustMigrationInfoProvider.rustDirectory, + rustMigrationInfoProvider.rustEncryptionConfiguration, + rustMigrationInfoProvider.migrateMegolmGroupSessions + ).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt similarity index 53% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt index a50ac8ca8..667990468 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RustMigrationInfoProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,18 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto.algorithms.olm +package org.matrix.android.sdk.internal.crypto.store.db -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.crypto.RustEncryptionConfiguration +import org.matrix.android.sdk.internal.di.SessionRustFilesDirectory +import java.io.File import javax.inject.Inject -internal class MXOlmDecryptionFactory @Inject constructor( - private val olmDevice: MXOlmDevice, - @UserId private val userId: String +internal class RustMigrationInfoProvider @Inject constructor( + @SessionRustFilesDirectory + val rustDirectory: File, + val rustEncryptionConfiguration: RustEncryptionConfiguration ) { - fun create(): MXOlmDecryption { - return MXOlmDecryption( - olmDevice, - userId - ) - } + var migrateMegolmGroupSessions: Boolean = false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CryptoRoomInfoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CryptoRoomInfoMapper.kt new file mode 100644 index 000000000..ef4d30ad4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CryptoRoomInfoMapper.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.mapper + +import org.matrix.android.sdk.api.crypto.MEGOLM_DEFAULT_ROTATION_MSGS +import org.matrix.android.sdk.api.crypto.MEGOLM_DEFAULT_ROTATION_PERIOD_MS +import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity + +internal object CryptoRoomInfoMapper { + + fun map(entity: CryptoRoomEntity): CryptoRoomInfo? { + val algorithm = entity.algorithm ?: return null + return CryptoRoomInfo( + algorithm = algorithm, + shouldEncryptForInvitedMembers = entity.shouldEncryptForInvitedMembers ?: false, + blacklistUnverifiedDevices = entity.blacklistUnverifiedDevices, + shouldShareHistory = entity.shouldShareHistory, + wasEncryptedOnce = entity.wasEncryptedOnce ?: false, + rotationPeriodMsgs = entity.rotationPeriodMsgs ?: MEGOLM_DEFAULT_ROTATION_MSGS, + rotationPeriodMs = entity.rotationPeriodMs ?: MEGOLM_DEFAULT_ROTATION_PERIOD_MS + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo021.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo021.kt new file mode 100644 index 000000000..a90614e53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo021.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.api.crypto.MEGOLM_DEFAULT_ROTATION_MSGS +import org.matrix.android.sdk.api.crypto.MEGOLM_DEFAULT_ROTATION_PERIOD_MS +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * This migration stores the rotation parameters for megolm oubound sessions. + */ +internal class MigrateCryptoTo021(realm: DynamicRealm) : RealmMigrator(realm, 21) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("CryptoRoomEntity") + ?.addField(CryptoRoomEntityFields.ROTATION_PERIOD_MS, Long::class.java) + ?.setNullable(CryptoRoomEntityFields.ROTATION_PERIOD_MS, true) + ?.addField(CryptoRoomEntityFields.ROTATION_PERIOD_MSGS, Long::class.java) + ?.setNullable(CryptoRoomEntityFields.ROTATION_PERIOD_MSGS, true) + ?.transform { + // As a migration we set the default (will be on par with existing code) + // A clear cache will have the correct values. + it.setLong(CryptoRoomEntityFields.ROTATION_PERIOD_MS, MEGOLM_DEFAULT_ROTATION_PERIOD_MS) + it.setLong(CryptoRoomEntityFields.ROTATION_PERIOD_MSGS, MEGOLM_DEFAULT_ROTATION_MSGS) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt new file mode 100644 index 000000000..5d1119778 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo022.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.RustEncryptionConfiguration +import org.matrix.android.sdk.internal.session.MigrateEAtoEROperation +import org.matrix.android.sdk.internal.util.database.RealmMigrator +import java.io.File + +/** + * This migration creates the rust database and migrates from legacy crypto. + */ +internal class MigrateCryptoTo022( + realm: DynamicRealm, + private val rustDirectory: File, + private val rustEncryptionConfiguration: RustEncryptionConfiguration, + private val migrateMegolmGroupSessions: Boolean = false +) : RealmMigrator( + realm, + 22 +) { + override fun doMigrate(realm: DynamicRealm) { + // Migrate to rust! + val migrateOperation = MigrateEAtoEROperation(migrateMegolmGroupSessions) + migrateOperation.dynamicExecute(realm, rustDirectory, rustEncryptionConfiguration.getDatabasePassphrase()) + + // wa can't delete all for now, but we can do some cleaning + realm.schema.get("OlmSessionEntity")?.transform { + it.deleteFromRealm() + } + + // a future migration will clean the rest + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt similarity index 62% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt index 6b1c67f7c..fb4bd1c8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataFailure.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.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 -) +package org.matrix.android.sdk.internal.crypto.store.db.migration.rust + +data class ExtractMigrationDataFailure(override val cause: Throwable) : + java.lang.RuntimeException("Can't proceed with migration, crypto store is empty or some necessary data is missing.", cause) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt new file mode 100644 index 000000000..3dae9a6b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/ExtractMigrationDataUseCase.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration.rust + +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.olm.OlmUtility +import org.matrix.rustcomponents.sdk.crypto.MigrationData +import timber.log.Timber +import kotlin.system.measureTimeMillis + +internal class ExtractMigrationDataUseCase(private val migrateGroupSessions: Boolean = false) { + + fun extractData(realm: RealmToMigrate, importPartial: ((MigrationData) -> Unit)) { + return try { + extract(realm, importPartial) + } catch (failure: Throwable) { + throw ExtractMigrationDataFailure(failure) + } + } + + fun hasExistingData(realmConfiguration: RealmConfiguration): Boolean { + return Realm.getInstance(realmConfiguration).use { realm -> + !realm.isEmpty && + // Check if there is a MetaData object + realm.where().count() > 0 && + realm.where().findFirst()?.olmAccountData != null + } + } + + private fun extract(realm: RealmToMigrate, importPartial: ((MigrationData) -> Unit)) { + val pickleKey = OlmUtility.getRandomKey() + + val baseExtract = realm.getPickledAccount(pickleKey) + // import the account asap + importPartial(baseExtract) + + val chunkSize = 500 + realm.trackedUsersChunk(500) { + importPartial( + baseExtract.copy(trackedUsers = it) + ) + } + + var migratedOlmSessionCount = 0 + var writeTime = 0L + measureTimeMillis { + realm.pickledOlmSessions(pickleKey, chunkSize) { pickledSessions -> + migratedOlmSessionCount += pickledSessions.size + measureTimeMillis { + importPartial( + baseExtract.copy(sessions = pickledSessions) + ) + }.also { writeTime += it } + } + }.also { + Timber.i("Migration: took $it ms to migrate $migratedOlmSessionCount olm sessions") + Timber.i("Migration: rust import time $writeTime") + } + + // We don't migrate outbound session by default directly after migration + // We are going to do it lazyly when decryption fails + if (migrateGroupSessions) { + var migratedInboundGroupSessionCount = 0 + measureTimeMillis { + realm.pickledOlmGroupSessions(pickleKey, chunkSize) { pickledSessions -> + migratedInboundGroupSessionCount += pickledSessions.size + measureTimeMillis { + importPartial( + baseExtract.copy(inboundGroupSessions = pickledSessions) + ) + }.also { writeTime += it } + } + }.also { + Timber.i("Migration: took $it ms to migrate $migratedInboundGroupSessionCount group sessions") + Timber.i("Migration: rust import time $writeTime") + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt new file mode 100644 index 000000000..d99403fe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/rust/RealmToMigrate.kt @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration.rust + +import io.realm.kotlin.where +import okhttp3.internal.toImmutableList +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData +import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmInboundGroupSession +import org.matrix.olm.OlmSession +import org.matrix.rustcomponents.sdk.crypto.CrossSigningKeyExport +import org.matrix.rustcomponents.sdk.crypto.MigrationData +import org.matrix.rustcomponents.sdk.crypto.PickledAccount +import org.matrix.rustcomponents.sdk.crypto.PickledInboundGroupSession +import org.matrix.rustcomponents.sdk.crypto.PickledSession +import timber.log.Timber +import java.nio.charset.Charset + +sealed class RealmToMigrate { + data class DynamicRealm(val realm: io.realm.DynamicRealm) : RealmToMigrate() + data class ClassicRealm(val realm: io.realm.Realm) : RealmToMigrate() +} + +fun RealmToMigrate.hasExistingData(): Boolean { + return when (this) { + is RealmToMigrate.ClassicRealm -> { + !this.realm.isEmpty && + // Check if there is a MetaData object + this.realm.where().count() > 0 && + this.realm.where().findFirst()?.olmAccountData != null + } + is RealmToMigrate.DynamicRealm -> { + return true + } + } +} + +@Throws +fun RealmToMigrate.getPickledAccount(pickleKey: ByteArray): MigrationData { + return when (this) { + is RealmToMigrate.ClassicRealm -> { + val metadataEntity = realm.where().findFirst() + ?: throw java.lang.IllegalArgumentException("Rust db migration: No existing metadataEntity") + + val masterKey = metadataEntity.xSignMasterPrivateKey + val userKey = metadataEntity.xSignUserPrivateKey + val selfSignedKey = metadataEntity.xSignSelfSignedPrivateKey + + Timber.i("## Migration: has private MSK ${masterKey.isNullOrBlank().not()}") + Timber.i("## Migration: has private USK ${userKey.isNullOrBlank().not()}") + Timber.i("## Migration: has private SSK ${selfSignedKey.isNullOrBlank().not()}") + + val userId = metadataEntity.userId + ?: throw java.lang.IllegalArgumentException("Rust db migration: userId is null") + val deviceId = metadataEntity.deviceId + ?: throw java.lang.IllegalArgumentException("Rust db migration: deviceID is null") + + val backupVersion = metadataEntity.backupVersion + val backupRecoveryKey = metadataEntity.keyBackupRecoveryKey + + Timber.i("## Migration: has private backup key ${backupRecoveryKey != null} for version $backupVersion") + + val isOlmAccountShared = metadataEntity.deviceKeysSentToServer + + val olmAccount = metadataEntity.getOlmAccount() + ?: throw java.lang.IllegalArgumentException("Rust db migration: No existing account") + val pickledOlmAccount = olmAccount.pickle(pickleKey, StringBuffer()).asString() + + val pickledAccount = PickledAccount( + userId = userId, + deviceId = deviceId, + pickle = pickledOlmAccount, + shared = isOlmAccountShared, + uploadedSignedKeyCount = 50 + ) + MigrationData( + account = pickledAccount, + pickleKey = pickleKey.map { it.toUByte() }, + crossSigning = CrossSigningKeyExport( + masterKey = masterKey, + selfSigningKey = selfSignedKey, + userSigningKey = userKey + ), + sessions = emptyList(), + backupRecoveryKey = backupRecoveryKey, + trackedUsers = emptyList(), + inboundGroupSessions = emptyList(), + backupVersion = backupVersion, + // TODO import room settings from legacy DB + roomSettings = emptyMap() + ) + } + is RealmToMigrate.DynamicRealm -> { + val cryptoMetadataEntitySchema = realm.schema.get("CryptoMetadataEntity") + ?: throw java.lang.IllegalStateException("Missing Metadata entity") + + var migrationData: MigrationData? = null + cryptoMetadataEntitySchema.transform { dynMetaData -> + + val serializedOlmAccount = dynMetaData.getString(CryptoMetadataEntityFields.OLM_ACCOUNT_DATA) + + val masterKey = dynMetaData.getString(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY) + val userKey = dynMetaData.getString(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY) + val selfSignedKey = dynMetaData.getString(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY) + + val userId = dynMetaData.getString(CryptoMetadataEntityFields.USER_ID) + ?: throw java.lang.IllegalArgumentException("Rust db migration: userId is null") + val deviceId = dynMetaData.getString(CryptoMetadataEntityFields.DEVICE_ID) + ?: throw java.lang.IllegalArgumentException("Rust db migration: deviceID is null") + + val backupVersion = dynMetaData.getString(CryptoMetadataEntityFields.BACKUP_VERSION) + val backupRecoveryKey = dynMetaData.getString(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY) + + val isOlmAccountShared = dynMetaData.getBoolean(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER) + + val olmAccount = deserializeFromRealm(serializedOlmAccount) + ?: throw java.lang.IllegalArgumentException("Rust db migration: No existing account") + + val pickledOlmAccount = olmAccount.pickle(pickleKey, StringBuffer()).asString() + + val pickledAccount = PickledAccount( + userId = userId, + deviceId = deviceId, + pickle = pickledOlmAccount, + shared = isOlmAccountShared, + uploadedSignedKeyCount = 50 + ) + + migrationData = MigrationData( + account = pickledAccount, + pickleKey = pickleKey.map { it.toUByte() }, + crossSigning = CrossSigningKeyExport( + masterKey = masterKey, + selfSigningKey = selfSignedKey, + userSigningKey = userKey + ), + sessions = emptyList(), + backupRecoveryKey = backupRecoveryKey, + trackedUsers = emptyList(), + inboundGroupSessions = emptyList(), + backupVersion = backupVersion, + // TODO import room settings from legacy DB + roomSettings = emptyMap() + ) + } + migrationData!! + } + } +} + +fun RealmToMigrate.trackedUsersChunk(chunkSize: Int, onChunk: ((List) -> Unit)) { + when (this) { + is RealmToMigrate.ClassicRealm -> { + realm.where() + .findAll() + .chunked(chunkSize) + .onEach { + onChunk(it.mapNotNull { it.userId }) + } + } + is RealmToMigrate.DynamicRealm -> { + val userList = mutableListOf() + realm.schema.get("UserEntity")?.transform { + val userId = it.getString(UserEntityFields.USER_ID) + // should we check the tracking status? + userList.add(userId) + if (userList.size > chunkSize) { + onChunk(userList.toImmutableList()) + userList.clear() + } + } + if (userList.isNotEmpty()) { + onChunk(userList) + } + } + } +} + +fun RealmToMigrate.pickledOlmSessions(pickleKey: ByteArray, chunkSize: Int, onChunk: ((List) -> Unit)) { + when (this) { + is RealmToMigrate.ClassicRealm -> { + realm.where().findAll() + .chunked(chunkSize) { chunk -> + val export = chunk.map { it.toPickledSession(pickleKey) } + onChunk(export) + } + } + is RealmToMigrate.DynamicRealm -> { + val pickledSessions = mutableListOf() + realm.schema.get("OlmSessionEntity")?.transform { + val sessionData = it.getString(OlmSessionEntityFields.OLM_SESSION_DATA) + val deviceKey = it.getString(OlmSessionEntityFields.DEVICE_KEY) + val lastReceivedMessageTs = it.getLong(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS) + val olmSession = deserializeFromRealm(sessionData)!! + val pickle = olmSession.pickle(pickleKey, StringBuffer()).asString() + val pickledSession = PickledSession( + pickle = pickle, + senderKey = deviceKey, + createdUsingFallbackKey = false, + creationTime = lastReceivedMessageTs.toString(), + lastUseTime = lastReceivedMessageTs.toString() + ) + // should we check the tracking status? + pickledSessions.add(pickledSession) + if (pickledSessions.size > chunkSize) { + onChunk(pickledSessions.toImmutableList()) + pickledSessions.clear() + } + } + if (pickledSessions.isNotEmpty()) { + onChunk(pickledSessions) + } + } + } +} + +private val sessionDataAdapter = MoshiProvider.providesMoshi() + .adapter(InboundGroupSessionData::class.java) +fun RealmToMigrate.pickledOlmGroupSessions(pickleKey: ByteArray, chunkSize: Int, onChunk: ((List) -> Unit)) { + when (this) { + is RealmToMigrate.ClassicRealm -> { + realm.where() + .findAll() + .chunked(chunkSize) { chunk -> + val export = chunk.mapNotNull { it.toPickledInboundGroupSession(pickleKey) } + onChunk(export) + } + } + is RealmToMigrate.DynamicRealm -> { + val pickledSessions = mutableListOf() + realm.schema.get("OlmInboundGroupSessionEntity")?.transform { + val senderKey = it.getString(OlmInboundGroupSessionEntityFields.SENDER_KEY) + val roomId = it.getString(OlmInboundGroupSessionEntityFields.ROOM_ID) + val backedUp = it.getBoolean(OlmInboundGroupSessionEntityFields.BACKED_UP) + val serializedOlmInboundGroupSession = it.getString(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION) + val inboundSession = deserializeFromRealm(serializedOlmInboundGroupSession) ?: return@transform Unit.also { + Timber.w("Rust db migration: Failed to migrated group session, no meta data") + } + val sessionData = it.getString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON).let { json -> + sessionDataAdapter.fromJson(json) + } ?: return@transform Unit.also { + Timber.w("Rust db migration: Failed to migrated group session, no meta data") + } + val pickle = inboundSession.pickle(pickleKey, StringBuffer()).asString() + val pickledSession = PickledInboundGroupSession( + pickle = pickle, + senderKey = senderKey, + signingKey = sessionData.keysClaimed.orEmpty(), + roomId = roomId, + forwardingChains = sessionData.forwardingCurve25519KeyChain.orEmpty(), + imported = sessionData.trusted.orFalse().not(), + backedUp = backedUp + ) + // should we check the tracking status? + pickledSessions.add(pickledSession) + if (pickledSessions.size > chunkSize) { + onChunk(pickledSessions.toImmutableList()) + pickledSessions.clear() + } + } + if (pickledSessions.isNotEmpty()) { + onChunk(pickledSessions) + } + } + } +} + +private fun OlmInboundGroupSessionEntity.toPickledInboundGroupSession(pickleKey: ByteArray): PickledInboundGroupSession? { + val senderKey = this.senderKey ?: return null + val backedUp = this.backedUp + val olmInboundGroupSession = this.getOlmGroupSession() ?: return null.also { + Timber.w("Rust db migration: Failed to migrated group session $sessionId") + } + val data = this.getData() ?: return null.also { + Timber.w("Rust db migration: Failed to migrated group session $sessionId, no meta data") + } + val roomId = data.roomId ?: return null.also { + Timber.w("Rust db migration: Failed to migrated group session $sessionId, no roomId") + } + val pickledInboundGroupSession = olmInboundGroupSession.pickle(pickleKey, StringBuffer()).asString() + return PickledInboundGroupSession( + pickle = pickledInboundGroupSession, + senderKey = senderKey, + signingKey = data.keysClaimed.orEmpty(), + roomId = roomId, + forwardingChains = data.forwardingCurve25519KeyChain.orEmpty(), + imported = data.trusted.orFalse().not(), + backedUp = backedUp + ) +} +private fun OlmSessionEntity.toPickledSession(pickleKey: ByteArray): PickledSession { + val deviceKey = this.deviceKey ?: "" + val lastReceivedMessageTs = this.lastReceivedMessageTs + val olmSessionStr = this.olmSessionData + val olmSession = deserializeFromRealm(olmSessionStr)!! + val pickledOlmSession = olmSession.pickle(pickleKey, StringBuffer()).asString() + return PickledSession( + pickle = pickledOlmSession, + senderKey = deviceKey, + createdUsingFallbackKey = false, + creationTime = lastReceivedMessageTs.toString(), + lastUseTime = lastReceivedMessageTs.toString() + ) +} + +private val charset = Charset.forName("UTF-8") +private fun ByteArray.asString() = String(this, charset) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt index be5758616..dce47860c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt @@ -32,7 +32,12 @@ internal open class CryptoRoomEntity( var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null, // a security to ensure that a room will never revert to not encrypted // even if a new state event with empty encryption, or state is reset somehow - var wasEncryptedOnce: Boolean? = false + var wasEncryptedOnce: Boolean? = false, + + // How long the session should be used before changing it. 604800000 (a week) is the recommended default. + var rotationPeriodMs: Long? = null, + // How many messages should be sent before changing the session. 100 is the recommended default. + var rotationPeriodMsgs: Long? = null, ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt index 96848a264..3474f0af4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -16,20 +16,18 @@ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.model.MXKey import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody +import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task -import timber.log.Timber import javax.inject.Inject -internal interface ClaimOneTimeKeysForUsersDeviceTask : Task> { +internal interface ClaimOneTimeKeysForUsersDeviceTask : Task { data class Params( // a list of users, devices and key types to retrieve keys for. - val usersDevicesKeyTypesMap: MXUsersDevicesMap + val usersDevicesKeyTypesMap: Map> ) } @@ -38,26 +36,11 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver ) : ClaimOneTimeKeysForUsersDeviceTask { - override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): MXUsersDevicesMap { - val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) + override suspend fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): KeysClaimResponse { + val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap) - val keysClaimResponse = executeRequest(globalErrorReceiver) { + return executeRequest(globalErrorReceiver, canRetry = true) { cryptoApi.claimOneTimeKeysForUsersDevices(body) } - val map = MXUsersDevicesMap() - keysClaimResponse.oneTimeKeys?.let { oneTimeKeys -> - for ((userId, mapByUserId) in oneTimeKeys) { - for ((deviceId, deviceKey) in mapByUserId) { - val mxKey = MXKey.from(deviceKey) - - if (mxKey != null) { - map.setObject(userId, deviceId, mxKey) - } else { - Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey") - } - } - } - } - return map } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt index 86f02866a..70af859dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -97,7 +97,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor( ) } else { // No need to chunk, direct request - executeRequest(globalErrorReceiver) { + executeRequest(globalErrorReceiver, canRetry = true) { cryptoApi.downloadKeysForUsers( KeysQueryBody( deviceKeys = params.userIds.associateWith { emptyList() }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 5d2797a6a..0f5f9f64c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -18,13 +18,12 @@ package org.matrix.android.sdk.internal.crypto.tasks import dagger.Lazy import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult +import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.task.Task @@ -57,48 +56,45 @@ internal class DefaultEncryptEventTask @Inject constructor( localMutableContent.remove(it) } -// try { // let it throws - awaitCallback { - cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) - }.let { result -> - val modifiedContent = HashMap(result.eventContent) - params.keepKeys?.forEach { toKeep -> - localEvent.content?.get(toKeep)?.let { - // put it back in the encrypted thing - modifiedContent[toKeep] = it - } - } - val safeResult = result.copy(eventContent = modifiedContent) - // Better handling of local echo, to avoid decrypting transition on remote echo - // Should I only do it for text messages? - val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { - MXEventDecryptionResult( - clearEvent = Event( - type = localEvent.type, - content = localEvent.content, - roomId = localEvent.roomId - ).toContent(), - forwardingCurve25519KeyChain = emptyList(), - senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(), - isSafe = true - ) - } else { - null - } + val result = cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId) - localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho -> - localEcho.type = EventType.ENCRYPTED - localEcho.content = ContentMapper.map(modifiedContent) - decryptionLocalEcho?.also { - localEcho.setDecryptionResult(it) - } + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it } - return localEvent.copy( - type = safeResult.eventType, - content = safeResult.eventContent + } + val safeResult = result.copy(eventContent = modifiedContent) + // Better handling of local echo, to avoid decrypting transition on remote echo + // Should I only do it for text messages? + val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + MXEventDecryptionResult( + clearEvent = Event( + type = localEvent.type, + content = localEvent.content, + roomId = localEvent.roomId + ).toContent(), + forwardingCurve25519KeyChain = emptyList(), + senderCurve25519Key = result.eventContent["sender_key"] as? String, + claimedEd25519Key = cryptoService.get().getMyCryptoDevice().fingerprint(), + messageVerificationState = MessageVerificationState.VERIFIED ) + } else { + null + } + + localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho -> + localEcho.type = EventType.ENCRYPTED + localEcho.content = ContentMapper.map(modifiedContent) + decryptionLocalEcho?.also { + localEcho.setDecryptionResult(it) + } } + return localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt deleted file mode 100644 index 53190c43f..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.tasks - -import dagger.Lazy -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey -import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage -import org.matrix.android.sdk.api.session.uia.UiaResult -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.auth.registration.handleUIA -import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable -import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.olm.OlmPkSigning -import timber.log.Timber -import javax.inject.Inject - -internal interface InitializeCrossSigningTask : Task { - data class Params( - val interactiveAuthInterceptor: UserInteractiveAuthInterceptor? - ) - - data class Result( - val masterKeyPK: String, - val userKeyPK: String, - val selfSigningKeyPK: String, - val masterKeyInfo: CryptoCrossSigningKey, - val userKeyInfo: CryptoCrossSigningKey, - val selfSignedKeyInfo: CryptoCrossSigningKey - ) -} - -internal class DefaultInitializeCrossSigningTask @Inject constructor( - @UserId private val userId: String, - private val olmDevice: MXOlmDevice, - private val myDeviceInfoHolder: Lazy, - private val uploadSigningKeysTask: UploadSigningKeysTask, - private val uploadSignaturesTask: UploadSignaturesTask -) : InitializeCrossSigningTask { - - override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result { - var masterPkOlm: OlmPkSigning? = null - var userSigningPkOlm: OlmPkSigning? = null - var selfSigningPkOlm: OlmPkSigning? = null - - try { - // ================= - // MASTER KEY - // ================= - - masterPkOlm = OlmPkSigning() - val masterKeyPrivateKey = OlmPkSigning.generateSeed() - val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) - - Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") - - // ================= - // USER KEY - // ================= - userSigningPkOlm = OlmPkSigning() - val uskPrivateKey = OlmPkSigning.generateSeed() - val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) - - Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") - - // Sign userSigningKey with master - val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .build() - .canonicalSignable() - .let { masterPkOlm.sign(it) } - - // ================= - // SELF SIGNING KEY - // ================= - selfSigningPkOlm = OlmPkSigning() - val sskPrivateKey = OlmPkSigning.generateSeed() - val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) - - Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") - - // Sign selfSigningKey with master - val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .build() - .canonicalSignable() - .let { masterPkOlm.sign(it) } - - // I need to upload the keys - val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) - .key(masterPublicKey) - .build() - val uploadSigningKeysParams = UploadSigningKeysTask.Params( - masterKey = mskCrossSigningKeyInfo, - userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) - .key(uskPublicKey) - .signature(userId, masterPublicKey, signedUSK) - .build(), - selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) - .key(sskPublicKey) - .signature(userId, masterPublicKey, signedSSK) - .build(), - userAuthParam = null -// userAuthParam = params.authParams - ) - - try { - uploadSigningKeysTask.execute(uploadSigningKeysParams) - } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null || - handleUIA( - failure = failure, - interceptor = params.interactiveAuthInterceptor, - retryBlock = { authUpdate -> - uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) - } - ) != UiaResult.SUCCESS - ) { - Timber.d("## UIA: propagate failure") - throw failure - } - } - - // Sign the current device with SSK - val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() - - val myDevice = myDeviceInfoHolder.get().myDevice - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) - val signedDevice = selfSigningPkOlm.sign(canonicalJson) - val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()) - .also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) - } - myDevice.copy(signatures = updateSignatures).let { - uploadSignatureQueryBuilder.withDeviceInfo(it) - } - - // sign MSK with device key (migration) and upload signatures - val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()) - olmDevice.signMessage(message)?.let { sign -> - val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() - ?: HashMap()).also { - it[userId] = (it[userId] - ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) - } - mskCrossSigningKeyInfo.copy( - signatures = mskUpdatedSignatures - ).let { - uploadSignatureQueryBuilder.withSigningKeyInfo(it) - } - } - - // TODO should we ignore failure of that? - uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) - - return InitializeCrossSigningTask.Result( - masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(), - userKeyPK = uskPrivateKey.toBase64NoPadding(), - selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(), - masterKeyInfo = uploadSigningKeysParams.masterKey, - userKeyInfo = uploadSigningKeysParams.userKey, - selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey - ) - } finally { - masterPkOlm?.releaseSigning() - userSigningPkOlm?.releaseSigning() - selfSigningPkOlm?.releaseSigning() - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt index b060748a6..01d59a8c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt @@ -30,7 +30,7 @@ internal interface RedactEventTask : Task { val roomId: String, val eventId: String, val reason: String?, - val withRelations: List?, + val withRelTypes: List?, ) } @@ -41,9 +41,9 @@ internal class DefaultRedactEventTask @Inject constructor( ) : RedactEventTask { override suspend fun execute(params: RedactEventTask.Params): String { - val withRelations = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactEventWithRelations.orFalse() && - !params.withRelations.isNullOrEmpty()) { - params.withRelations + val withRelTypes = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canRedactRelatedEvents.orFalse() && + !params.withRelTypes.isNullOrEmpty()) { + params.withRelTypes } else { null } @@ -55,7 +55,7 @@ internal class DefaultRedactEventTask @Inject constructor( eventId = params.eventId, body = EventRedactBody( reason = params.reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 405757e3b..51bb322c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -74,6 +74,7 @@ internal class DefaultSendEventTask @Inject constructor( eventType = event.type ?: "" ) } + Timber.d("Event sent to ${event.roomId} with event id ${response.eventId}") localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) return response.eventId.also { Timber.d("Event: $it just sent in ${params.event.roomId}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt index a7e93202e..294196279 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody +import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -41,6 +42,8 @@ internal interface SendToDeviceTask : Task { val contentMap: MXUsersDevicesMap, // the transactionId. If not provided, a transactionId will be created by the task val transactionId: String? = null, + // Number of retry before failing + val retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT, // add tracing id, notice that to device events that do signature on content might be broken by it val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType), ) @@ -71,7 +74,7 @@ internal class DefaultSendToDeviceTask @Inject constructor( return executeRequest( globalErrorReceiver, canRetry = true, - maxRetriesCount = 3 + maxRetriesCount = params.retryCount ) { cryptoApi.sendToDevice( eventType = params.eventType, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 944f41d48..2a8d12248 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -18,17 +18,22 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider +import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.toMatrixErrorStr import javax.inject.Inject -internal interface SendVerificationMessageTask : Task { +internal interface SendVerificationMessageTask : Task { data class Params( - val event: Event + // The event to sent + val event: Event, + // Number of retry before failing + val retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT ) } @@ -40,13 +45,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver ) : SendVerificationMessageTask { - override suspend fun execute(params: SendVerificationMessageTask.Params): String { + override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse { val event = handleEncryption(params) val localId = event.eventId!! - try { localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) - val response = executeRequest(globalErrorReceiver) { + val response = executeRequest(globalErrorReceiver, canRetry = true, maxRetriesCount = params.retryCount) { roomAPI.send( txId = localId, roomId = event.roomId ?: "", @@ -55,7 +59,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( ) } localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT) - return response.eventId + return response } catch (e: Throwable) { localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr()) throw e diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt index 30de8e871..cc58e2a06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt @@ -16,9 +16,8 @@ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -29,11 +28,7 @@ import javax.inject.Inject internal interface UploadKeysTask : Task { data class Params( - // the device keys to send. - val deviceKeys: DeviceKeys?, - // the one-time keys to send. - val oneTimeKeys: JsonDict?, - val fallbackKeys: JsonDict? + val body: KeysUploadBody, ) } @@ -43,16 +38,11 @@ internal class DefaultUploadKeysTask @Inject constructor( ) : UploadKeysTask { override suspend fun execute(params: UploadKeysTask.Params): KeysUploadResponse { - val body = KeysUploadBody( - deviceKeys = params.deviceKeys, - oneTimeKeys = params.oneTimeKeys, - fallbackKeys = params.fallbackKeys - ) - - Timber.i("## Uploading device keys -> $body") - - return executeRequest(globalErrorReceiver) { - cryptoApi.uploadKeys(body) + Timber.v("## Uploading device keys -> ${params.body}") + return executeRequest(globalErrorReceiver, canRetry = true) { + cryptoApi.uploadKeys( + params.body.toContent() + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt index 18d8b2655..516a1ef4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -16,12 +16,13 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.matrix.android.sdk.internal.crypto.api.CryptoApi +import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface UploadSignaturesTask : Task { +internal interface UploadSignaturesTask : Task { data class Params( val signatures: Map> ) @@ -32,7 +33,7 @@ internal class DefaultUploadSignaturesTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver ) : UploadSignaturesTask { - override suspend fun execute(params: UploadSignaturesTask.Params) { + override suspend fun execute(params: UploadSignaturesTask.Params): SignatureUploadResponse { val response = executeRequest( globalErrorReceiver, canRetry = true, @@ -40,8 +41,10 @@ internal class DefaultUploadSignaturesTask @Inject constructor( ) { cryptoApi.uploadSignatures(params.signatures) } + // TODO should we still throw here, looks like rust & kotlin does not work the same way if (response.failures?.isNotEmpty() == true) { throw Throwable(response.failures.toString()) } + return response } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt index e539867a0..d8596fcac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -60,7 +60,7 @@ internal class DefaultUploadSigningKeysTask @Inject constructor( } private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { - val keysQueryResponse = executeRequest(globalErrorReceiver) { + val keysQueryResponse = executeRequest(globalErrorReceiver, canRetry = true) { cryptoApi.uploadSigningKeys(uploadQuery) } if (keysQueryResponse.failures?.isNotEmpty() == true) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt deleted file mode 100644 index 6b3bb1e64..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import android.util.Base64 -import org.matrix.android.sdk.BuildConfig -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasMode -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber - -internal class DefaultIncomingSASDefaultVerificationTransaction( - setDeviceVerificationAction: SetDeviceVerificationAction, - override val userId: String, - override val deviceId: String?, - private val cryptoStore: IMXCryptoStore, - crossSigningService: CrossSigningService, - outgoingKeyRequestManager: OutgoingKeyRequestManager, - secretShareManager: SecretShareManager, - deviceFingerprint: String, - transactionId: String, - otherUserID: String, - private val autoAccept: Boolean = false -) : SASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - deviceFingerprint, - transactionId, - otherUserID, - null, - isIncoming = true -), - IncomingSasVerificationTransaction { - - override val uxState: IncomingSasVerificationTransaction.UxState - get() { - return when (val immutableState = state) { - is VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT - is VerificationTxState.SendingAccept, - is VerificationTxState.Accepted, - is VerificationTxState.OnKeyReceived, - is VerificationTxState.SendingKey, - is VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT - is VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS - is VerificationTxState.ShortCodeAccepted, - is VerificationTxState.SendingMac, - is VerificationTxState.MacSent, - is VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION - is VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED - is VerificationTxState.Cancelled -> { - if (immutableState.byMe) { - IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME - } else { - IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER - } - } - else -> IncomingSasVerificationTransaction.UxState.UNKNOWN - } - } - - override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { - Timber.v("## SAS I: received verification request from state $state") - if (state != VerificationTxState.None) { - Timber.e("## SAS I: received verification request from invalid state") - // should I cancel?? - throw IllegalStateException("Interactive Key verification already started") - } - this.startReq = startReq - state = VerificationTxState.OnStarted - this.otherDeviceId = startReq.fromDevice - - if (autoAccept) { - performAccept() - } - } - - override fun performAccept() { - if (state != VerificationTxState.OnStarted) { - Timber.e("## SAS Cannot perform accept from state $state") - return - } - - // Select a key agreement protocol, a hash algorithm, a message authentication code, - // and short authentication string methods out of the lists given in requester's message. - val agreedProtocol = startReq!!.keyAgreementProtocols.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) } - val agreedHash = startReq!!.hashes.firstOrNull { KNOWN_HASHES.contains(it) } - val agreedMac = startReq!!.messageAuthenticationCodes.firstOrNull { KNOWN_MACS.contains(it) } - val agreedShortCode = startReq!!.shortAuthenticationStrings.filter { KNOWN_SHORT_CODES.contains(it) } - - // No common key sharing/hashing/hmac/SAS methods. - // If a device is unable to complete the verification because the devices are unable to find a common key sharing, - // hashing, hmac, or SAS method, then it should send a m.key.verification.cancel message - if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } || - agreedShortCode.isNullOrEmpty()) { - // Failed to find agreement - Timber.e("## SAS Failed to find agreement ") - cancel(CancelCode.UnknownMethod) - return - } - - // Bob’s device ensures that it has a copy of Alice’s device key. - val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!) - - if (mxDeviceInfo?.fingerprint() == null) { - Timber.e("## SAS Failed to find device key ") - // TODO force download keys!! - // would be probably better to download the keys - // for now I cancel - cancel(CancelCode.User) - } else { - // val otherKey = info.identityKey() - // need to jump back to correct thread - val accept = transport.createAccept( - tid = transactionId, - keyAgreementProtocol = agreedProtocol!!, - hash = agreedHash!!, - messageAuthenticationCode = agreedMac!!, - shortAuthenticationStrings = agreedShortCode, - commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT) - ) - doAccept(accept) - } - } - - private fun doAccept(accept: VerificationInfoAccept) { - this.accepted = accept.asValidObject() - Timber.v("## SAS incoming accept request id:$transactionId") - - // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, - // concatenated with the canonical JSON representation of the content of the m.key.verification.start message - val concat = getSAS().publicKey + startReq!!.canonicalJson - accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" - // we need to send this to other device now - state = VerificationTxState.SendingAccept - sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, VerificationTxState.Accepted, CancelCode.User) { - if (state == VerificationTxState.SendingAccept) { - // It is possible that we receive the next event before this one :/, in this case we should keep state - state = VerificationTxState.Accepted - } - } - } - - override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { - Timber.v("## SAS invalid message for incoming request id:$transactionId") - cancel(CancelCode.UnexpectedMessage) - } - - override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { - Timber.v("## SAS received key for request id:$transactionId") - if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { - Timber.e("## SAS received key from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - otherKey = vKey.key - // Upon receipt of the m.key.verification.key message from Alice’s device, - // Bob’s device replies with a to_device message with type set to m.key.verification.key, - // sending Bob’s public key QB - val pubKey = getSAS().publicKey - - val keyToDevice = transport.createKey(transactionId, pubKey) - // we need to send this to other device now - state = VerificationTxState.SendingKey - this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { - if (state == VerificationTxState.SendingKey) { - // It is possible that we receive the next event before this one :/, in this case we should keep state - state = VerificationTxState.KeySent - } - } - - // Alice’s and Bob’s devices perform an Elliptic-curve Diffie-Hellman - // (calculate the point (x,y)=dAQB=dBQA and use x as the result of the ECDH), - // using the result as the shared secret. - - getSAS().setTheirPublicKey(otherKey) - - shortCodeBytes = calculateSASBytes() - - if (BuildConfig.LOG_PRIVATE_DATA) { - Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") - Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") - } - - state = VerificationTxState.ShortCodeReady - } - - private fun calculateSASBytes(): ByteArray { - when (accepted?.keyAgreementProtocol) { - KEY_AGREEMENT_V1 -> { - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - the Matrix ID of the user who sent the m.key.verification.accept message, - // - he device ID of the device that sent the m.key.verification.accept message - // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" - - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - return getSAS().generateShortCode(sasInfo, 6) - } - KEY_AGREEMENT_V2 -> { - // Adds the SAS public key, and separate by | - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId" - return getSAS().generateShortCode(sasInfo, 6) - } - else -> { - // Protocol has been checked earlier - throw IllegalArgumentException() - } - } - } - - override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { - Timber.v("## SAS I: received mac for request id:$transactionId") - // Check for state? - if (state != VerificationTxState.SendingKey && - state != VerificationTxState.KeySent && - state != VerificationTxState.ShortCodeReady && - state != VerificationTxState.ShortCodeAccepted && - state != VerificationTxState.SendingMac && - state != VerificationTxState.MacSent) { - Timber.e("## SAS I: received key from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - theirMac = vMac - - // Do I have my Mac? - if (myMac != null) { - // I can check - verifyMacs(vMac) - } - // Wait for ShortCode Accepted - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt deleted file mode 100644 index f1cf1b754..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber - -internal class DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction: SetDeviceVerificationAction, - userId: String, - deviceId: String?, - cryptoStore: IMXCryptoStore, - crossSigningService: CrossSigningService, - outgoingKeyRequestManager: OutgoingKeyRequestManager, - secretShareManager: SecretShareManager, - deviceFingerprint: String, - transactionId: String, - otherUserId: String, - otherDeviceId: String -) : SASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - deviceFingerprint, - transactionId, - otherUserId, - otherDeviceId, - isIncoming = false -), - OutgoingSasVerificationTransaction { - - override val uxState: OutgoingSasVerificationTransaction.UxState - get() { - return when (val immutableState = state) { - is VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START - is VerificationTxState.SendingStart, - is VerificationTxState.Started, - is VerificationTxState.OnAccepted, - is VerificationTxState.SendingKey, - is VerificationTxState.KeySent, - is VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT - is VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS - is VerificationTxState.ShortCodeAccepted, - is VerificationTxState.SendingMac, - is VerificationTxState.MacSent, - is VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION - is VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED - is VerificationTxState.Cancelled -> { - if (immutableState.byMe) { - OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER - } else { - OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME - } - } - else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN - } - } - - override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) { - Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") - cancel(CancelCode.UnexpectedMessage) - } - - fun start() { - if (state != VerificationTxState.None) { - Timber.e("## SAS O: start verification from invalid state") - // should I cancel?? - throw IllegalStateException("Interactive Key verification already started") - } - - val startMessage = transport.createStartForSas( - deviceId ?: "", - transactionId, - KNOWN_AGREEMENT_PROTOCOLS, - KNOWN_HASHES, - KNOWN_MACS, - KNOWN_SHORT_CODES - ) - - startReq = startMessage.asValidObject() as? ValidVerificationInfoStart.SasVerificationInfoStart - state = VerificationTxState.SendingStart - - sendToOther( - EventType.KEY_VERIFICATION_START, - startMessage, - VerificationTxState.Started, - CancelCode.User, - null - ) - } - -// fun request() { -// if (state != VerificationTxState.None) { -// Timber.e("## start verification from invalid state") -// // should I cancel?? -// throw IllegalStateException("Interactive Key verification already started") -// } -// -// val requestMessage = KeyVerificationRequest( -// fromDevice = session.sessionParams.deviceId ?: "", -// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), -// timestamp = clock.epochMillis().toInt(), -// transactionId = transactionId -// ) -// -// sendToOther( -// EventType.KEY_VERIFICATION_REQUEST, -// requestMessage, -// VerificationTxState.None, -// CancelCode.User, -// null -// ) -// } - - override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { - Timber.v("## SAS O: onVerificationAccept id:$transactionId") - if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) { - Timber.e("## SAS O: received accept request from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - // Check that the agreement is correct - if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) || - !KNOWN_HASHES.contains(accept.hash) || - !KNOWN_MACS.contains(accept.messageAuthenticationCode) || - accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { - Timber.e("## SAS O: received invalid accept") - cancel(CancelCode.UnknownMethod) - return - } - - // Upon receipt of the m.key.verification.accept message from Bob’s device, - // Alice’s device stores the commitment value for later use. - accepted = accept - state = VerificationTxState.OnAccepted - - // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), - // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA - val pubKey = getSAS().publicKey - - val keyToDevice = transport.createKey(transactionId, pubKey) - // we need to send this to other device now - state = VerificationTxState.SendingKey - sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { - // It is possible that we receive the next event before this one :/, in this case we should keep state - if (state == VerificationTxState.SendingKey) { - state = VerificationTxState.KeySent - } - } - } - - override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) { - Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") - if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { - Timber.e("## received key from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - otherKey = vKey.key - // Upon receipt of the m.key.verification.key message from Bob’s device, - // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept - // message is the same as the expected value based on the value of the key property received - // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. - - // check commitment - val concat = vKey.key + startReq!!.canonicalJson - val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" - - if (accepted!!.commitment.equals(otherCommitment)) { - getSAS().setTheirPublicKey(otherKey) - shortCodeBytes = calculateSASBytes() - state = VerificationTxState.ShortCodeReady - } else { - // bad commitment - cancel(CancelCode.MismatchedCommitment) - } - } - - private fun calculateSASBytes(): ByteArray { - when (accepted?.keyAgreementProtocol) { - KEY_AGREEMENT_V1 -> { - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - the Matrix ID of the user who sent the m.key.verification.accept message, - // - he device ID of the device that sent the m.key.verification.accept message - // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" - - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - return getSAS().generateShortCode(sasInfo, 6) - } - KEY_AGREEMENT_V2 -> { - // Adds the SAS public key, and separate by | - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId" - return getSAS().generateShortCode(sasInfo, 6) - } - else -> { - // Protocol has been checked earlier - throw IllegalArgumentException() - } - } - } - - override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { - Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") - // There is starting to be a huge amount of state / race here :/ - if (state != VerificationTxState.OnKeyReceived && - state != VerificationTxState.ShortCodeReady && - state != VerificationTxState.ShortCodeAccepted && - state != VerificationTxState.KeySent && - state != VerificationTxState.SendingMac && - state != VerificationTxState.MacSent) { - Timber.e("## SAS O: received mac from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - theirMac = vMac - - // Do I have my Mac? - if (myMac != null) { - // I can check - verifyMacs(vMac) - } - // Wait for ShortCode Accepted - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt deleted file mode 100644 index 5b400aa63..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ /dev/null @@ -1,1532 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import android.os.Handler -import android.os.Looper -import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest -import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone -import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.crypto.model.rest.toValue -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction -import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData -import org.matrix.android.sdk.internal.crypto.verification.qrcode.generateSharedSecretV2 -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import java.util.UUID -import javax.inject.Inject -import kotlin.collections.set - -@SessionScope -internal class DefaultVerificationService @Inject constructor( - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val cryptoStore: IMXCryptoStore, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val secretShareManager: SecretShareManager, - private val myDeviceInfoHolder: Lazy, - private val deviceListManager: DeviceListManager, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, - private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, - private val crossSigningService: CrossSigningService, - private val cryptoCoroutineScope: CoroutineScope, - private val taskExecutor: TaskExecutor, - private val clock: Clock, -) : DefaultVerificationTransaction.Listener, VerificationService { - - private val uiHandler = Handler(Looper.getMainLooper()) - - // map [sender : [transaction]] - private val txMap = HashMap>() - - // we need to keep track of finished transaction - // It will be used for gossiping (to send request after request is completed and 'done' by other) - private val pastTransactions = HashMap>() - - /** - * Map [sender: [PendingVerificationRequest]] - * For now we keep all requests (even terminated ones) during the lifetime of the app. - */ - private val pendingRequests = HashMap>() - - // Event received from the sync - fun onToDeviceEvent(event: Event) { - Timber.d("## SAS onToDeviceEvent ${event.getClearType()}") - cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - onStartRequestReceived(event) - } - EventType.KEY_VERIFICATION_CANCEL -> { - onCancelReceived(event) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - onAcceptReceived(event) - } - EventType.KEY_VERIFICATION_KEY -> { - onKeyReceived(event) - } - EventType.KEY_VERIFICATION_MAC -> { - onMacReceived(event) - } - EventType.KEY_VERIFICATION_READY -> { - onReadyReceived(event) - } - EventType.KEY_VERIFICATION_DONE -> { - onDoneReceived(event) - } - MessageType.MSGTYPE_VERIFICATION_REQUEST -> { - onRequestReceived(event) - } - else -> { - // ignore - } - } - } - } - - fun onRoomEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - onRoomStartRequestReceived(event) - } - EventType.KEY_VERIFICATION_CANCEL -> { - // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device - onRoomCancelReceived(event) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - onRoomAcceptReceived(event) - } - EventType.KEY_VERIFICATION_KEY -> { - onRoomKeyRequestReceived(event) - } - EventType.KEY_VERIFICATION_MAC -> { - onRoomMacReceived(event) - } - EventType.KEY_VERIFICATION_READY -> { - onRoomReadyReceived(event) - } - EventType.KEY_VERIFICATION_DONE -> { - onRoomDoneReceived(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { - onRoomRequestReceived(event) - } - } - else -> { - // ignore - } - } - } - } - - private var listeners = ArrayList() - - override fun addListener(listener: VerificationService.Listener) { - uiHandler.post { - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - } - - override fun removeListener(listener: VerificationService.Listener) { - uiHandler.post { - listeners.remove(listener) - } - } - - private fun dispatchTxAdded(tx: VerificationTransaction) { - uiHandler.post { - listeners.forEach { - try { - it.transactionCreated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - private fun dispatchTxUpdated(tx: VerificationTransaction) { - uiHandler.post { - listeners.forEach { - try { - it.transactionUpdated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - private fun dispatchRequestAdded(tx: PendingVerificationRequest) { - Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}") - uiHandler.post { - listeners.forEach { - try { - it.verificationRequestCreated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { - uiHandler.post { - listeners.forEach { - try { - it.verificationRequestUpdated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle( - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), - userId, - deviceID - ) - - listeners.forEach { - try { - it.markedAsManuallyVerified(userId, deviceID) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - - fun onRoomRequestHandledByOtherDevice(event: Event) { - val requestInfo = event.content.toModel() - ?: return - val requestId = requestInfo.relatesTo?.eventId ?: return - getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { - updatePendingRequest( - it.copy( - handledByOtherSession = true - ) - ) - } - } - - private fun onRequestReceived(event: Event) { - val validRequestInfo = event.getClearContent().toModel()?.asValidObject() - - if (validRequestInfo == null) { - // ignore - Timber.e("## SAS Received invalid key request") - return - } - val senderId = event.senderId ?: return - - // We don't want to block here - val otherDeviceId = validRequestInfo.fromDevice - - Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}") - - cryptoCoroutineScope.launch { - if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { - Timber.e("## Verification device $otherDeviceId is not known") - } - } - Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched") - - // Remember this request - val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } - - val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), - isIncoming = true, - otherUserId = senderId, // requestInfo.toUserId, - roomId = null, - transactionId = validRequestInfo.transactionId, - localId = validRequestInfo.transactionId, - requestInfo = validRequestInfo - ) - requestsForUser.add(pendingVerificationRequest) - dispatchRequestAdded(pendingVerificationRequest) - } - - suspend fun onRoomRequestReceived(event: Event) { - Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") - val requestInfo = event.getClearContent().toModel() ?: return - val validRequestInfo = requestInfo - // copy the EventId to the transactionId - .copy(transactionId = event.eventId) - .asValidObject() ?: return - - val senderId = event.senderId ?: return - - if (requestInfo.toUserId != userId) { - // I should ignore this, it's not for me - Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") - return - } - - // We don't want to block here - taskExecutor.executorScope.launch { - if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) { - Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known") - } - } - - // Remember this request - val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } - - val pendingVerificationRequest = PendingVerificationRequest( - ageLocalTs = event.ageLocalTs ?: clock.epochMillis(), - isIncoming = true, - otherUserId = senderId, // requestInfo.toUserId, - roomId = event.roomId, - transactionId = event.eventId, - localId = event.eventId!!, - requestInfo = validRequestInfo - ) - requestsForUser.add(pendingVerificationRequest) - dispatchRequestAdded(pendingVerificationRequest) - - /* - * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event - * to begin the verification. - * If both parties send an m.key.verification.start event, and they both specify the same verification method, - * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start - * event is ignored. - * In the case of a single user verifying two of their devices, the device ID is compared instead. - * If both parties send an m.key.verification.start event, but they specify different verification methods, - * the verification should be cancelled with a code of m.unexpected_message. - */ - } - - override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { - // When Should/Can we cancel?? - val relationContent = event.content.toModel()?.relatesTo - if (relationContent?.type == RelationType.REFERENCE) { - val relatedId = relationContent.eventId ?: return - // at least if request was sent by me, I can safely cancel without interfering - pendingRequests[event.senderId]?.firstOrNull { - it.transactionId == relatedId && !it.isIncoming - }?.let { pr -> - verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) - .cancelTransaction( - relatedId, - event.senderId ?: "", - event.getSenderKey() ?: "", - CancelCode.InvalidMessage - ) - updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) - } - } - } - - private suspend fun onRoomStartRequestReceived(event: Event) { - val startReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - - val validStartReq = startReq?.asValidObject() - - val otherUserId = event.senderId - if (validStartReq == null) { - Timber.e("## received invalid verification request") - if (startReq?.transactionId != null) { - verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) - .cancelTransaction( - startReq.transactionId ?: "", - otherUserId!!, - startReq.fromDevice ?: event.getSenderKey()!!, - CancelCode.UnknownMethod - ) - } - return - } - - handleStart(otherUserId, validStartReq) { - it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) - }?.let { - verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) - .cancelTransaction( - validStartReq.transactionId, - otherUserId!!, - validStartReq.fromDevice, - it - ) - } - } - - private suspend fun onStartRequestReceived(event: Event) { - Timber.e("## SAS received Start request ${event.eventId}") - val startReq = event.getClearContent().toModel() - val validStartReq = startReq?.asValidObject() - Timber.v("## SAS received Start request $startReq") - - val otherUserId = event.senderId!! - if (validStartReq == null) { - Timber.e("## SAS received invalid verification request") - if (startReq?.transactionId != null) { - verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( - startReq.transactionId, - otherUserId, - startReq.fromDevice ?: event.getSenderKey()!!, - CancelCode.UnknownMethod - ) - } - return - } - // Download device keys prior to everything - handleStart(otherUserId, validStartReq) { - it.transport = verificationTransportToDeviceFactory.createTransport(it) - }?.let { - verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( - validStartReq.transactionId, - otherUserId, - validStartReq.fromDevice, - it - ) - } - } - - /** - * Return a CancelCode to make the caller cancel the verification. Else return null - */ - private suspend fun handleStart( - otherUserId: String?, - startReq: ValidVerificationInfoStart, - txConfigure: (DefaultVerificationTransaction) -> Unit - ): CancelCode? { - Timber.d("## SAS onStartRequestReceived $startReq") - if (otherUserId?.let { checkKeysAreDownloaded(it, startReq.fromDevice) } != null) { - val tid = startReq.transactionId - var existing = getExistingTransaction(otherUserId, tid) - - // After the m.key.verification.ready event is sent, either party can send an - // m.key.verification.start event to begin the verification. If both parties - // send an m.key.verification.start event, and they both specify the same - // verification method, then the event sent by the user whose user ID is the - // smallest is used, and the other m.key.verification.start event is ignored. - // In the case of a single user verifying two of their devices, the device ID is - // compared instead . - if (existing is DefaultOutgoingSASDefaultVerificationTransaction) { - val readyRequest = getExistingVerificationRequest(otherUserId, tid) - if (readyRequest?.isReady == true) { - if (isOtherPrioritary(otherUserId, existing.otherDeviceId ?: "")) { - Timber.d("## SAS concurrent start isOtherPrioritary, clear") - // The other is prioritary! - // I should replace my outgoing with an incoming - removeTransaction(otherUserId, tid) - existing = null - } else { - Timber.d("## SAS concurrent start i am prioritary, ignore") - // i am prioritary, ignore this start event! - return null - } - } - } - - when (startReq) { - is ValidVerificationInfoStart.SasVerificationInfoStart -> { - when (existing) { - is SasVerificationTransaction -> { - // should cancel both! - Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionId}") - existing.cancel(CancelCode.UnexpectedMessage) - // Already cancelled, so return null - return null - } - is QrCodeVerificationTransaction -> { - // Nothing to do? - } - null -> { - getExistingTransactionsForUser(otherUserId) - ?.filterIsInstance(SasVerificationTransaction::class.java) - ?.takeIf { it.isNotEmpty() } - ?.also { - // Multiple keyshares between two devices: - // any two devices may only have at most one key verification in flight at a time. - Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionId}") - } - ?.forEach { - it.cancel(CancelCode.UnexpectedMessage) - } - ?.also { - return CancelCode.UnexpectedMessage - } - } - } - - // Ok we can create a SAS transaction - Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionId}") - // If there is a corresponding request, we can auto accept - // as we are the one requesting in first place (or we accepted the request) - // I need to check if the pending request was related to this device also - val autoAccept = getExistingVerificationRequests(otherUserId).any { - it.transactionId == startReq.transactionId && - (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) - } - val tx = DefaultIncomingSASDefaultVerificationTransaction( -// this, - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - startReq.transactionId, - otherUserId, - autoAccept - ).also { txConfigure(it) } - addTransaction(tx) - tx.onVerificationStart(startReq) - return null - } - is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> { - // Other user has scanned my QR code - if (existing is DefaultQrCodeVerificationTransaction) { - existing.onStartReceived(startReq) - return null - } else { - Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId} / $existing") - return CancelCode.UnexpectedMessage - } - } - } - } else { - return CancelCode.UnexpectedMessage - } - } - - private fun isOtherPrioritary(otherUserId: String, otherDeviceId: String): Boolean { - if (userId < otherUserId) { - return false - } else if (userId > otherUserId) { - return true - } else { - return otherDeviceId < deviceId ?: "" - } - } - - // TODO Refacto: It could just return a boolean - private suspend fun checkKeysAreDownloaded( - otherUserId: String, - otherDeviceId: String - ): MXUsersDevicesMap? { - return try { - var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) - if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) { - return keys - } else { - // force download - keys = deviceListManager.downloadKeys(listOf(otherUserId), true) - return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true } - } - } catch (e: Exception) { - null - } - } - - private fun onRoomCancelReceived(event: Event) { - val cancelReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - - val validCancelReq = cancelReq?.asValidObject() - - if (validCancelReq == null) { - // ignore - Timber.e("## SAS Received invalid cancel request") - // TODO should we cancel? - return - } - getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let { - updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code))) - // Should we remove it from the list? - } - handleOnCancel(event.senderId!!, validCancelReq) - } - - private fun onCancelReceived(event: Event) { - Timber.v("## SAS onCancelReceived") - val cancelReq = event.getClearContent().toModel()?.asValidObject() - - if (cancelReq == null) { - // ignore - Timber.e("## SAS Received invalid cancel request") - return - } - val otherUserId = event.senderId!! - - handleOnCancel(otherUserId, cancelReq) - } - - private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) { - Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}") - - val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId) - val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId) - - if (existingRequest != null) { - // Mark this request as cancelled - updatePendingRequest( - existingRequest.copy( - cancelConclusion = safeValueOf(cancelReq.code) - ) - ) - } - - existingTransaction?.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) - } - - private fun onRoomAcceptReceived(event: Event) { - Timber.d("## SAS Received Accept via DM $event") - val accept = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?: return - - val validAccept = accept.asValidObject() ?: return - - handleAccept(validAccept, event.senderId!!) - } - - private fun onAcceptReceived(event: Event) { - Timber.d("## SAS Received Accept $event") - val acceptReq = event.getClearContent().toModel()?.asValidObject() ?: return - handleAccept(acceptReq, event.senderId!!) - } - - private fun handleAccept(acceptReq: ValidVerificationInfoAccept, senderId: String) { - val otherUserId = senderId - val existing = getExistingTransaction(otherUserId, acceptReq.transactionId) - if (existing == null) { - Timber.e("## SAS Received invalid accept request") - return - } - - if (existing is SASDefaultVerificationTransaction) { - existing.onVerificationAccept(acceptReq) - } else { - // not other types now - } - } - - private fun onRoomKeyRequestReceived(event: Event) { - val keyReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - if (keyReq == null) { - // ignore - Timber.e("## SAS Received invalid key request") - // TODO should we cancel? - return - } - handleKeyReceived(event, keyReq) - } - - private fun onKeyReceived(event: Event) { - val keyReq = event.getClearContent().toModel()?.asValidObject() - - if (keyReq == null) { - // ignore - Timber.e("## SAS Received invalid key request") - return - } - handleKeyReceived(event, keyReq) - } - - private fun handleKeyReceived(event: Event, keyReq: ValidVerificationInfoKey) { - Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") - val otherUserId = event.senderId!! - val existing = getExistingTransaction(otherUserId, keyReq.transactionId) - if (existing == null) { - Timber.e("## SAS Received invalid key request") - return - } - if (existing is SASDefaultVerificationTransaction) { - existing.onKeyVerificationKey(keyReq) - } else { - // not other types now - } - } - - private fun onRoomMacReceived(event: Event) { - val macReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - if (macReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid mac request") - // TODO should we cancel? - return - } - handleMacReceived(event.senderId, macReq) - } - - private suspend fun onRoomReadyReceived(event: Event) { - val readyReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - if (readyReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid ready request") - // TODO should we cancel? - return - } - if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { - Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") - // TODO cancel? - return - } - - val roomId = event.roomId - if (roomId == null) { - Timber.e("## SAS Verification missing roomId for event") - // TODO cancel? - return - } - - handleReadyReceived(event.senderId, readyReq) { - verificationTransportRoomMessageFactory.createTransport(roomId, it) - } - } - - private suspend fun onReadyReceived(event: Event) { - val readyReq = event.getClearContent().toModel()?.asValidObject() - Timber.v("## SAS onReadyReceived $readyReq") - - if (readyReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid ready request") - // TODO should we cancel? - return - } - if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) { - Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") - // TODO cancel? - return - } - - handleReadyReceived(event.senderId, readyReq) { - verificationTransportToDeviceFactory.createTransport(it) - } - } - - private fun onDoneReceived(event: Event) { - Timber.v("## onDoneReceived") - val doneReq = event.getClearContent().toModel()?.asValidObject() - if (doneReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid done request") - return - } - - handleDoneReceived(event.senderId, doneReq) - - if (event.senderId == userId) { - // We only send gossiping request when the other sent us a done - // We can ask without checking too much thinks (like trust), because we will check validity of secret on reception - getExistingTransaction(userId, doneReq.transactionId) - ?: getOldTransaction(userId, doneReq.transactionId) - ?.let { vt -> - val otherDeviceId = vt.otherDeviceId ?: return@let - if (!crossSigningService.canCrossSign()) { - cryptoCoroutineScope.launch { - secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) - secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) - } - } - } - } - } - - private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) { - Timber.v("## SAS Done received $doneReq") - val existing = getExistingTransaction(senderId, doneReq.transactionId) - if (existing == null) { - Timber.e("## SAS Received invalid Done request") - return - } - if (existing is DefaultQrCodeVerificationTransaction) { - existing.onDoneReceived() - } else { - // SAS do not care for now? - } - - // Now transactions are updated, let's also update Requests - val existingRequest = getExistingVerificationRequests(senderId).find { it.transactionId == doneReq.transactionId } - if (existingRequest == null) { - Timber.e("## SAS Received Done for unknown request txId:${doneReq.transactionId}") - return - } - updatePendingRequest(existingRequest.copy(isSuccessful = true)) - } - - private fun onRoomDoneReceived(event: Event) { - val doneReq = event.getClearContent().toModel() - ?.copy( - // relates_to is in clear in encrypted payload - relatesTo = event.content.toModel()?.relatesTo - ) - ?.asValidObject() - - if (doneReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid Done request") - // TODO should we cancel? - return - } - - handleDoneReceived(event.senderId, doneReq) - } - - private fun onMacReceived(event: Event) { - val macReq = event.getClearContent().toModel()?.asValidObject() - - if (macReq == null || event.senderId == null) { - // ignore - Timber.e("## SAS Received invalid mac request") - return - } - handleMacReceived(event.senderId, macReq) - } - - private fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) { - Timber.v("## SAS Received $macReq") - val existing = getExistingTransaction(senderId, macReq.transactionId) - if (existing == null) { - Timber.e("## SAS Received invalid Mac request") - return - } - if (existing is SASDefaultVerificationTransaction) { - existing.onKeyVerificationMac(macReq) - } else { - // not other types known for now - } - } - - private fun handleReadyReceived( - senderId: String, - readyReq: ValidVerificationInfoReady, - transportCreator: (DefaultVerificationTransaction) -> VerificationTransport - ) { - val existingRequest = getExistingVerificationRequests(senderId).find { it.transactionId == readyReq.transactionId } - if (existingRequest == null) { - Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionId} fromDevice ${readyReq.fromDevice}") - return - } - - val qrCodeData = readyReq.methods - // Check if other user is able to scan QR code - .takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } - ?.let { - createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice) - } - - if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) { - // Create the pending transaction - val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction = setDeviceVerificationAction, - transactionId = readyReq.transactionId, - otherUserId = senderId, - otherDeviceId = readyReq.fromDevice, - crossSigningService = crossSigningService, - outgoingKeyRequestManager = outgoingKeyRequestManager, - secretShareManager = secretShareManager, - cryptoStore = cryptoStore, - qrCodeData = qrCodeData, - userId = userId, - deviceId = deviceId ?: "", - isIncoming = false - ) - - tx.transport = transportCreator.invoke(tx) - - addTransaction(tx) - } - - updatePendingRequest( - existingRequest.copy( - readyInfo = readyReq - ) - ) - - notifyOthersOfAcceptance(readyReq.transactionId, readyReq.fromDevice) - } - - /** - * Gets a list of device ids excluding the current one. - */ - private fun getMyOtherDeviceIds(): List = cryptoStore.getUserDevices(userId)?.keys?.filter { it != deviceId }.orEmpty() - - /** - * Notifies other devices that the current verification transaction is being handled by [acceptedByDeviceId]. - */ - private fun notifyOthersOfAcceptance(transactionId: String, acceptedByDeviceId: String) { - val deviceIds = getMyOtherDeviceIds().filter { it != acceptedByDeviceId } - val transport = verificationTransportToDeviceFactory.createTransport(null) - transport.cancelTransaction(transactionId, userId, deviceIds, CancelCode.AcceptedByAnotherDevice) - } - - private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { - requestId ?: run { - Timber.w("## Unknown requestId") - return null - } - - return when { - userId != otherUserId -> - createQrCodeDataForDistinctUser(requestId, otherUserId) - crossSigningService.isCrossSigningVerified() -> - // This is a self verification and I am the old device (Osborne2) - createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) - else -> - // This is a self verification and I am the new device (Dynabook) - createQrCodeDataForUnVerifiedDevice(requestId) - } - } - - private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? { - val myMasterKey = crossSigningService.getMyCrossSigningKeys() - ?.masterKey() - ?.unpaddedBase64PublicKey - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId) - ?.masterKey() - ?.unpaddedBase64PublicKey - ?: run { - Timber.w("## Unable to get other user master key") - return null - } - - return QrCodeData.VerifyingAnotherUser( - transactionId = requestId, - userMasterCrossSigningPublicKey = myMasterKey, - otherUserMasterCrossSigningPublicKey = otherUserMasterKey, - sharedSecret = generateSharedSecretV2() - ) - } - - // Create a QR code to display on the old device (Osborne2) - private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? { - val myMasterKey = crossSigningService.getMyCrossSigningKeys() - ?.masterKey() - ?.unpaddedBase64PublicKey - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val otherDeviceKey = otherDeviceId - ?.let { - cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() - } - ?: run { - Timber.w("## Unable to get other device data") - return null - } - - return QrCodeData.SelfVerifyingMasterKeyTrusted( - transactionId = requestId, - userMasterCrossSigningPublicKey = myMasterKey, - otherDeviceKey = otherDeviceKey, - sharedSecret = generateSharedSecretV2() - ) - } - - // Create a QR code to display on the new device (Dynabook) - private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? { - val myMasterKey = crossSigningService.getMyCrossSigningKeys() - ?.masterKey() - ?.unpaddedBase64PublicKey - ?: run { - Timber.w("## Unable to get my master key") - return null - } - - val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() - ?: run { - Timber.w("## Unable to get my fingerprint") - return null - } - - return QrCodeData.SelfVerifyingMasterKeyNotTrusted( - transactionId = requestId, - deviceKey = myDeviceKey, - userMasterCrossSigningPublicKey = myMasterKey, - sharedSecret = generateSharedSecretV2() - ) - } - -// private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) { -// val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId } -// if (existingRequest == null) { -// Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}") -// return -// } -// updatePendingRequest(existingRequest.copy(isSuccessful = true)) -// } - - // TODO All this methods should be delegated to a TransactionStore - override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { - synchronized(lock = txMap) { - return txMap[otherUserId]?.get(tid) - } - } - - override fun getExistingVerificationRequests(otherUserId: String): List { - synchronized(lock = pendingRequests) { - return pendingRequests[otherUserId].orEmpty() - } - } - - override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { - synchronized(lock = pendingRequests) { - return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } } - } - } - - override fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? { - synchronized(lock = pendingRequests) { - return tid?.let { tid -> - pendingRequests.flatMap { entry -> - entry.value.filter { it.roomId == roomId && it.transactionId == tid } - }.firstOrNull() - } - } - } - - private fun getExistingTransactionsForUser(otherUser: String): Collection? { - synchronized(txMap) { - return txMap[otherUser]?.values - } - } - - private fun removeTransaction(otherUser: String, tid: String) { - synchronized(txMap) { - txMap[otherUser]?.remove(tid)?.also { - it.removeListener(this) - } - }?.let { - rememberOldTransaction(it) - } - } - - private fun addTransaction(tx: DefaultVerificationTransaction) { - synchronized(txMap) { - val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } - txInnerMap[tx.transactionId] = tx - dispatchTxAdded(tx) - tx.addListener(this) - } - } - - private fun rememberOldTransaction(tx: DefaultVerificationTransaction) { - synchronized(pastTransactions) { - pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx - } - } - - private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? { - return tid?.let { - synchronized(pastTransactions) { - pastTransactions[userId]?.get(it) - } - } - } - - override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { - val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) - // should check if already one (and cancel it) - require(method == VerificationMethod.SAS) { "Unknown verification method" } - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - txID, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportToDeviceFactory.createTransport(tx) - addTransaction(tx) - - tx.start() - return txID - } - - override fun requestKeyVerificationInDMs( - methods: List, - otherUserId: String, - roomId: String, - localId: String? - ): PendingVerificationRequest { - Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") - - val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } - - val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) - - // Cancel existing pending requests? - requestsForUser.toList().forEach { existingRequest -> - existingRequest.transactionId?.let { tid -> - if (!existingRequest.isFinished) { - Timber.d("## SAS, cancelling pending requests to start a new one") - updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) - transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) - } - } - } - - val validLocalId = localId ?: LocalEcho.createLocalEchoId() - - val verificationRequest = PendingVerificationRequest( - ageLocalTs = clock.epochMillis(), - isIncoming = false, - roomId = roomId, - localId = validLocalId, - otherUserId = otherUserId - ) - - // We can SCAN or SHOW QR codes only if cross-signing is verified - val methodValues = if (crossSigningService.isCrossSigningVerified()) { - // Add reciprocate method if application declares it can scan or show QR codes - // Not sure if it ok to do that (?) - val reciprocateMethod = methods - .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } - ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() - methods.map { it.toValue() } + reciprocateMethod - } else { - // Filter out SCAN and SHOW qr code method - methods - .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } - .map { it.toValue() } - } - .distinct() - - requestsForUser.add(verificationRequest) - transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> - // We need to update with the syncedID - updatePendingRequest( - verificationRequest.copy( - transactionId = syncedId, - // localId stays different - requestInfo = info - ) - ) - } - - dispatchRequestAdded(verificationRequest) - - return verificationRequest - } - - override fun cancelVerificationRequest(request: PendingVerificationRequest) { - if (request.roomId != null) { - val transport = verificationTransportRoomMessageFactory.createTransport(request.roomId, null) - transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, null, CancelCode.User) - } else { - val transport = verificationTransportToDeviceFactory.createTransport(null) - request.targetDevices?.forEach { deviceId -> - transport.cancelTransaction(request.transactionId ?: "", request.otherUserId, deviceId, CancelCode.User) - } - } - } - - override fun requestKeyVerification(methods: List, otherUserId: String, otherDevices: List?): PendingVerificationRequest { - // TODO refactor this with the DM one - Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") - - val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId) - ?.values?.map { it.deviceId }.orEmpty() - - val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } - - val transport = verificationTransportToDeviceFactory.createTransport(null) - - // Cancel existing pending requests? - requestsForUser.toList().forEach { existingRequest -> - existingRequest.transactionId?.let { tid -> - if (!existingRequest.isFinished) { - Timber.d("## SAS, cancelling pending requests to start a new one") - updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) - existingRequest.targetDevices?.forEach { - transport.cancelTransaction(tid, existingRequest.otherUserId, it, CancelCode.User) - } - } - } - } - - val localId = LocalEcho.createLocalEchoId() - - val verificationRequest = PendingVerificationRequest( - transactionId = localId, - ageLocalTs = clock.epochMillis(), - isIncoming = false, - roomId = null, - localId = localId, - otherUserId = otherUserId, - targetDevices = targetDevices - ) - - // We can SCAN or SHOW QR codes only if cross-signing is enabled - val methodValues = if (crossSigningService.isCrossSigningInitialized()) { - // Add reciprocate method if application declares it can scan or show QR codes - // Not sure if it ok to do that (?) - val reciprocateMethod = methods - .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } - ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() - methods.map { it.toValue() } + reciprocateMethod - } else { - // Filter out SCAN and SHOW qr code method - methods - .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } - .map { it.toValue() } - } - .distinct() - - transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info -> - // Nothing special to do in to device mode - updatePendingRequest( - verificationRequest.copy( - // localId stays different - requestInfo = info - ) - ) - } - - requestsForUser.add(verificationRequest) - dispatchRequestAdded(verificationRequest) - - return verificationRequest - } - - override fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { - verificationTransportRoomMessageFactory.createTransport(roomId, null) - .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) - - getExistingVerificationRequest(otherUserId, transactionId)?.let { - updatePendingRequest( - it.copy( - cancelConclusion = CancelCode.User - ) - ) - } - } - - private fun updatePendingRequest(updated: PendingVerificationRequest) { - val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() } - val index = requestsForUser.indexOfFirst { - it.transactionId == updated.transactionId || - it.transactionId == null && it.localId == updated.localId - } - if (index != -1) { - requestsForUser.removeAt(index) - } - requestsForUser.add(updated) - dispatchRequestUpdated(updated) - } - - override fun beginKeyVerificationInDMs( - method: VerificationMethod, - transactionId: String, - roomId: String, - otherUserId: String, - otherDeviceId: String - ): String { - require(method == VerificationMethod.SAS) { "Unknown verification method" } - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - transactionId, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) - addTransaction(tx) - - tx.start() - return transactionId - } - - override fun readyPendingVerificationInDMs( - methods: List, - otherUserId: String, - roomId: String, - transactionId: String - ): Boolean { - Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") - // Let's find the related request - val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) - if (existingRequest != null) { - // we need to send a ready event, with matching methods - val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) - val computedMethods = computeReadyMethods( - transactionId, - otherUserId, - existingRequest.requestInfo?.fromDevice ?: "", - existingRequest.requestInfo?.methods, - methods - ) { - verificationTransportRoomMessageFactory.createTransport(roomId, it) - } - if (methods.isNullOrEmpty()) { - Timber.i("Cannot ready this request, no common methods found txId:$transactionId") - // TODO buttons should not be shown in this case? - return false - } - // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? - val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) - transport.sendToOther( - EventType.KEY_VERIFICATION_READY, - readyMsg, - VerificationTxState.None, - CancelCode.User, - null // TODO handle error? - ) - updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) - return true - } else { - Timber.e("## SAS readyPendingVerificationInDMs Verification not found") - // :/ should not be possible... unless live observer very slow - return false - } - } - - override fun readyPendingVerification( - methods: List, - otherUserId: String, - transactionId: String - ): Boolean { - Timber.v("## SAS readyPendingVerification $otherUserId tx:$transactionId") - // Let's find the related request - val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) - if (existingRequest != null) { - // we need to send a ready event, with matching methods - val transport = verificationTransportToDeviceFactory.createTransport(null) - val computedMethods = computeReadyMethods( - transactionId, - otherUserId, - existingRequest.requestInfo?.fromDevice ?: "", - existingRequest.requestInfo?.methods, - methods - ) { - verificationTransportToDeviceFactory.createTransport(it) - } - if (methods.isNullOrEmpty()) { - Timber.i("Cannot ready this request, no common methods found txId:$transactionId") - // TODO buttons should not be shown in this case? - return false - } - // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? - val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) - transport.sendVerificationReady( - readyMsg, - otherUserId, - existingRequest.requestInfo?.fromDevice ?: "", - null // TODO handle error? - ) - updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject())) - return true - } else { - Timber.e("## SAS readyPendingVerification Verification not found") - // :/ should not be possible... unless live observer very slow - return false - } - } - - private fun computeReadyMethods( - transactionId: String, - otherUserId: String, - otherDeviceId: String, - otherUserMethods: List?, - methods: List, - transportCreator: (DefaultVerificationTransaction) -> VerificationTransport - ): List { - if (otherUserMethods.isNullOrEmpty()) { - return emptyList() - } - - val result = mutableSetOf() - - if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { - // Other can do SAS and so do I - result.add(VERIFICATION_METHOD_SAS) - } - - if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { - // Other user wants to verify using QR code. Cross-signing has to be setup - val qrCodeData = createQrCodeData(transactionId, otherUserId, otherDeviceId) - - if (qrCodeData != null) { - if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { - // Other can Scan and I can show QR code - result.add(VERIFICATION_METHOD_QR_CODE_SHOW) - result.add(VERIFICATION_METHOD_RECIPROCATE) - } - if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { - // Other can show and I can scan QR code - result.add(VERIFICATION_METHOD_QR_CODE_SCAN) - result.add(VERIFICATION_METHOD_RECIPROCATE) - } - } - - if (VERIFICATION_METHOD_RECIPROCATE in result) { - // Create the pending transaction - val tx = DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction = setDeviceVerificationAction, - transactionId = transactionId, - otherUserId = otherUserId, - otherDeviceId = otherDeviceId, - crossSigningService = crossSigningService, - outgoingKeyRequestManager = outgoingKeyRequestManager, - secretShareManager = secretShareManager, - cryptoStore = cryptoStore, - qrCodeData = qrCodeData, - userId = userId, - deviceId = deviceId ?: "", - isIncoming = false - ) - - tx.transport = transportCreator.invoke(tx) - - addTransaction(tx) - } - } - - return result.toList() - } - - /** - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - */ - private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String { - return buildString { - append(userId).append("|") - append(deviceId).append("|") - append(otherUserId).append("|") - append(otherDeviceID).append("|") - append(UUID.randomUUID().toString()) - } - } - - override fun transactionUpdated(tx: VerificationTransaction) { - dispatchTxUpdated(tx) - if (tx.state is VerificationTxState.TerminalTxState) { - // remove - this.removeTransaction(tx.otherUserId, tx.transactionId) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt deleted file mode 100644 index 9d19fd137..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import timber.log.Timber - -/** - * Generic interactive key verification transaction. - */ -internal abstract class DefaultVerificationTransaction( - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val crossSigningService: CrossSigningService, - private val outgoingKeyRequestManager: OutgoingKeyRequestManager, - private val secretShareManager: SecretShareManager, - private val userId: String, - override val transactionId: String, - override val otherUserId: String, - override var otherDeviceId: String? = null, - override val isIncoming: Boolean -) : VerificationTransaction { - - lateinit var transport: VerificationTransport - - interface Listener { - fun transactionUpdated(tx: VerificationTransaction) - } - - protected var listeners = ArrayList() - - fun addListener(listener: Listener) { - if (!listeners.contains(listener)) listeners.add(listener) - } - - fun removeListener(listener: Listener) { - listeners.remove(listener) - } - - protected fun trust( - canTrustOtherUserMasterKey: Boolean, - toVerifyDeviceIds: List, - eventuallyMarkMyMasterKeyAsTrusted: Boolean, - autoDone: Boolean = true - ) { - Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds") - Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted") - - // TODO what if the otherDevice is not in this list? and should we - toVerifyDeviceIds.forEach { - setDeviceVerified(otherUserId, it) - } - - // If not me sign his MSK and upload the signature - if (canTrustOtherUserMasterKey) { - // we should trust this master key - // And check verification MSK -> SSK? - if (otherUserId != userId) { - crossSigningService.trustUser(otherUserId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## Verification: Failed to trust User $otherUserId") - } - }) - } else { - // Notice other master key is mine because other is me - if (eventuallyMarkMyMasterKeyAsTrusted) { - // Mark my keys as trusted locally - crossSigningService.markMyMasterKeyAsTrusted() - } - } - } - - if (otherUserId == userId) { - secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!) - - // If me it's reasonable to sign and upload the device signature - // Notice that i might not have the private keys, so may not be able to do it - crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.w("## Verification: Failed to sign new device $otherDeviceId, ${failure.localizedMessage}") - } - }) - } - - if (autoDone) { - state = VerificationTxState.Verified - transport.done(transactionId) {} - } - } - - private fun setDeviceVerified(userId: String, deviceId: String) { - // TODO should not override cross sign status - setDeviceVerificationAction.handle( - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), - userId, - deviceId - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt new file mode 100644 index 000000000..ba769e52c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -0,0 +1,386 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.OwnUserIdentity +import org.matrix.android.sdk.internal.crypto.UserIdentity +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.toValue +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState +import timber.log.Timber +import javax.inject.Inject + +/** + * A helper class to deserialize to-device `m.key.verification.*` events to fetch the transaction id out. + */ +@JsonClass(generateAdapter = true) +internal data class ToDeviceVerificationEvent( + @Json(name = "sender") val sender: String?, + @Json(name = "transaction_id") val transactionId: String +) + +/** Helper method to fetch the unique ID of the verification event. */ +private fun getFlowId(event: Event): String? { + return if (event.eventId != null) { + event.getRelationContent()?.eventId + } else { + val content = event.getClearContent().toModel() ?: return null + content.transactionId + } +} + +/** Convert a list of VerificationMethod into a list of strings that can be passed to the Rust side. */ +internal fun prepareMethods(methods: List): List { + val stringMethods: MutableList = methods.map { it.toValue() }.toMutableList() + + if (stringMethods.contains(VERIFICATION_METHOD_QR_CODE_SHOW) || + stringMethods.contains(VERIFICATION_METHOD_QR_CODE_SCAN)) { + stringMethods.add(VERIFICATION_METHOD_RECIPROCATE) + } + + return stringMethods +} + +@SessionScope +internal class RustVerificationService @Inject constructor( + private val olmMachine: OlmMachine, + private val verificationListenersHolder: VerificationListenersHolder) : VerificationService { + + override fun requestEventFlow() = verificationListenersHolder.eventFlow + + /** + * + * All verification related events should be forwarded through this method to + * the verification service. + * + * This method mainly just fetches the appropriate rust object that will be created or updated by the event and + * dispatches updates to our listeners. + */ + internal suspend fun onEvent(roomId: String?, event: Event) { + if (roomId != null && event.unsignedData?.transactionId == null) { + if (isVerificationEvent(event)) { + try { + val clearEvent = if (event.isEncrypted()) { + event.copy( + content = event.getDecryptedContent(), + type = event.getDecryptedType(), + roomId = roomId + ) + } else { + event + } + olmMachine.receiveVerificationEvent(roomId, clearEvent) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to receiveUnencryptedVerificationEvent ${failure.message}") + } + } + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_REQUEST -> onRequest(event, fromRoomMessage = false) + EventType.KEY_VERIFICATION_START -> onStart(event) + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE -> onUpdate(event) + EventType.MESSAGE -> onRoomMessage(event) + else -> Unit + } + } + + private fun isVerificationEvent(event: Event): Boolean { + val eventType = event.getClearType() + val eventContent = event.getClearContent() ?: return false + return EventType.isVerificationEvent(eventType) || + (eventType == EventType.MESSAGE && + eventContent[MessageContent.MSG_TYPE_JSON_KEY] == MessageType.MSGTYPE_VERIFICATION_REQUEST) + } + + private suspend fun onRoomMessage(event: Event) { + val messageContent = event.getClearContent()?.toModel() ?: return + if (messageContent.msgType == MessageType.MSGTYPE_VERIFICATION_REQUEST) { + onRequest(event, fromRoomMessage = true) + } + } + + /** Dispatch updates after a verification event has been received. */ + private suspend fun onUpdate(event: Event) { + Timber.v("[${olmMachine.userId().take(6)}] Verification on event ${event.getClearType()}") + val sender = event.senderId ?: return + val flowId = getFlowId(event) ?: return Unit.also { + Timber.w("onUpdate for unknown flowId senderId ${event.getClearType()}") + } + + val verificationRequest = olmMachine.getVerificationRequest(sender, flowId) + if (event.getClearType() == EventType.KEY_VERIFICATION_READY) { + // we start the qr here in order to display the code + verificationRequest?.startQrCode() + } + } + + /** Check if the start event created new verification objects and dispatch updates. */ + private suspend fun onStart(event: Event) { + if (event.unsignedData?.transactionId != null) return // remote echo + val sender = event.senderId ?: return + val flowId = getFlowId(event) ?: return + + // The events have already been processed by the sdk + // The transaction are already created, we are just reacting here + val transaction = getExistingTransaction(sender, flowId) ?: return Unit.also { + Timber.w("onStart for unknown flowId $flowId senderId $sender") + } + + val request = olmMachine.getVerificationRequest(sender, flowId) + Timber.d("## Verification: matching request $request") + + if (request != null) { + // If this is a SAS verification originating from a `m.key.verification.request` + // event, we auto-accept here considering that we either initiated the request or + // accepted the request. If it's a QR code verification, just dispatch an update. + if (request.innerState() is VerificationRequestState.Ready && transaction is SasVerification) { + // accept() will dispatch an update, no need to do it twice. + Timber.d("## Verification: Auto accepting SAS verification with $sender") + transaction.accept() + } + + Timber.d("## Verification: start for $sender") + // update the request as the start updates it's state + verificationListenersHolder.dispatchRequestUpdated(request) + verificationListenersHolder.dispatchTxUpdated(transaction) + } else { + // This didn't originate from a request, so tell our listeners that + // this is a new verification. + verificationListenersHolder.dispatchTxAdded(transaction) + // The IncomingVerificationRequestHandler seems to only listen to updates + // so let's trigger an update after the addition as well. + verificationListenersHolder.dispatchTxUpdated(transaction) + } + } + + /** Check if the request event created a nev verification request object and dispatch that it dis so. */ + private suspend fun onRequest(event: Event, fromRoomMessage: Boolean) { + val flowId = if (fromRoomMessage) { + event.eventId + } else { + event.getClearContent().toModel()?.transactionId + } ?: return + val sender = event.senderId ?: return + val request = olmMachine.getVerificationRequest(sender, flowId) ?: return + + verificationListenersHolder.dispatchRequestAdded(request) + } + + override suspend fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + olmMachine.getDevice(userId, deviceID)?.markAsTrusted() + } + + override suspend fun getExistingTransaction( + otherUserId: String, + tid: String, + ): VerificationTransaction? { + return olmMachine.getVerification(otherUserId, tid) + } + + override suspend fun getExistingVerificationRequests( + otherUserId: String + ): List { + return olmMachine.getVerificationRequests(otherUserId).map { + it.toPendingVerificationRequest() + } + } + + override suspend fun getExistingVerificationRequest( + otherUserId: String, + tid: String? + ): PendingVerificationRequest? { + return if (tid != null) { + olmMachine.getVerificationRequest(otherUserId, tid)?.toPendingVerificationRequest() + } else { + null + } + } + + override suspend fun getExistingVerificationRequestInRoom( + roomId: String, + tid: String + ): PendingVerificationRequest? { + // This is only used in `RoomDetailViewModel` to resume the verification. + // + // Is this actually useful? SAS and QR code verifications ephemeral nature + // due to the usage of ephemeral secrets. In the case of SAS verification, the + // ephemeral key can't be stored due to libolm missing support for it, I would + // argue that the ephemeral secret for QR verifications shouldn't be persisted either. + // + // This means that once we transition from a verification request into an actual + // verification flow (SAS/QR) we won't be able to resume. In other words resumption + // is only supported before both sides agree to verify. + // + // We would either need to remember if the request transitioned into a flow and only + // support resumption if we didn't, otherwise we would risk getting different emojis + // or secrets in the QR code, not to mention that the flows could be interrupted in + // any non-starting state. + // + // In any case, we don't support resuming in the rust-sdk, so let's return null here. + return null + } + + override suspend fun requestSelfKeyVerification(methods: List): PendingVerificationRequest { + val verification = when (val identity = olmMachine.getIdentity(olmMachine.userId())) { + is OwnUserIdentity -> identity.requestVerification(methods) + is UserIdentity -> throw IllegalArgumentException("This method doesn't support verification of other users devices") + null -> throw IllegalArgumentException("Cross signing has not been bootstrapped for our own user") + } + return verification.toPendingVerificationRequest() + } + + override suspend fun requestKeyVerificationInDMs( + methods: List, + otherUserId: String, + roomId: String, + localId: String? + ): PendingVerificationRequest { + Timber.w("verification: requestKeyVerificationInDMs in room $roomId with $otherUserId") + olmMachine.ensureUsersKeys(listOf(otherUserId), true) + val verification = when (val identity = olmMachine.getIdentity(otherUserId)) { + is UserIdentity -> identity.requestVerification(methods, roomId, localId!!) + is OwnUserIdentity -> throw IllegalArgumentException("This method doesn't support verification of our own user") + null -> throw IllegalArgumentException("The user that we wish to verify doesn't support cross signing") + } + + return verification.toPendingVerificationRequest() + } + + override suspend fun requestDeviceVerification(methods: List, + otherUserId: String, + otherDeviceId: String?): PendingVerificationRequest { + // how do we send request to several devices in rust? + olmMachine.ensureUsersKeys(listOf(otherUserId)) + val request = if (otherDeviceId == null) { + // Todo + when (val identity = olmMachine.getIdentity(otherUserId)) { + is OwnUserIdentity -> identity.requestVerification(methods) + is UserIdentity -> { + throw IllegalArgumentException("to_device request only allowed for own user $otherUserId") + } + null -> throw IllegalArgumentException("Unknown identity") + } + } else { + val otherDevice = olmMachine.getDevice(otherUserId, otherDeviceId) + otherDevice?.requestVerification(methods) ?: throw IllegalArgumentException("Unknown device $otherDeviceId") + } + return request.toPendingVerificationRequest() + } + + override suspend fun readyPendingVerification( + methods: List, + otherUserId: String, + transactionId: String + ): Boolean { + val request = olmMachine.getVerificationRequest(otherUserId, transactionId) + return if (request != null) { + request.acceptWithMethods(methods) + request.startQrCode() + request.innerState() is VerificationRequestState.Ready + } else { + false + } + } + + override suspend fun startKeyVerification(method: VerificationMethod, otherUserId: String, requestId: String): String? { + return if (method == VerificationMethod.SAS) { + val request = olmMachine.getVerificationRequest(otherUserId, requestId) + ?: throw UnsupportedOperationException("Unknown request with id: $requestId") + + val sas = request.startSasVerification() + + if (sas != null) { + verificationListenersHolder.dispatchTxAdded(sas) + // we need to update the request as the state mapping depends on the + // sas or qr beeing started + verificationListenersHolder.dispatchRequestUpdated(request) + sas.transactionId + } else { + Timber.w("Failed to start verification with method $method") + null + } + } else { + throw UnsupportedOperationException("Unknown verification method") + } + } + + override suspend fun reciprocateQRVerification(otherUserId: String, requestId: String, scannedData: String): String? { + val matchingRequest = olmMachine.getVerificationRequest(otherUserId, requestId) + ?: return null + val qrVerification = matchingRequest.scanQrCode(scannedData) + ?: return null + verificationListenersHolder.dispatchTxAdded(qrVerification) + // we need to update the request as the state mapping depends on the + // sas or qr beeing started + verificationListenersHolder.dispatchRequestUpdated(matchingRequest) + return qrVerification.transactionId + } + + override suspend fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // not available in rust + } + + override suspend fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { + cancelVerificationRequest(otherUserId, transactionId) + } + +// override suspend fun beginDeviceVerification(otherUserId: String, otherDeviceId: String): String? { +// // This starts the short SAS flow, the one that doesn't start with +// // a `m.key.verification.request`, Element web stopped doing this, might +// // be wise do do so as well +// // DeviceListBottomSheetViewModel triggers this, interestingly the method that +// // triggers this is called `manuallyVerify()` +// val otherDevice = olmMachine.getDevice(otherUserId, otherDeviceId) +// val verification = otherDevice?.startVerification() +// return if (verification != null) { +// verificationListenersHolder.dispatchTxAdded(verification) +// verification.transactionId +// } else { +// null +// } +// } + + override suspend fun cancelVerificationRequest(request: PendingVerificationRequest) { + cancelVerificationRequest(request.otherUserId, request.transactionId) + } + + override suspend fun cancelVerificationRequest(otherUserId: String, transactionId: String) { + val verificationRequest = olmMachine.getVerificationRequest(otherUserId, transactionId) + verificationRequest?.cancel() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt deleted file mode 100644 index 29b416bb8..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation -import org.matrix.android.sdk.api.session.crypto.verification.SasMode -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.extensions.toUnsignedInt -import org.matrix.olm.OlmSAS -import org.matrix.olm.OlmUtility -import timber.log.Timber -import java.util.Locale - -/** - * Represents an ongoing short code interactive key verification between two devices. - */ -internal abstract class SASDefaultVerificationTransaction( - setDeviceVerificationAction: SetDeviceVerificationAction, - open val userId: String, - open val deviceId: String?, - private val cryptoStore: IMXCryptoStore, - crossSigningService: CrossSigningService, - outgoingKeyRequestManager: OutgoingKeyRequestManager, - secretShareManager: SecretShareManager, - private val deviceFingerprint: String, - transactionId: String, - otherUserId: String, - otherDeviceId: String?, - isIncoming: Boolean -) : DefaultVerificationTransaction( - setDeviceVerificationAction, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - userId, - transactionId, - otherUserId, - otherDeviceId, - isIncoming -), - SasVerificationTransaction { - - companion object { - const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" - const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" - - // Deprecated maybe removed later, use V2 - const val KEY_AGREEMENT_V1 = "curve25519" - const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256" - - // ordered by preferred order - val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1) - - // ordered by preferred order - val KNOWN_HASHES = listOf("sha256") - - // ordered by preferred order - val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) - - // older devices have limited support of emoji but SDK offers images for the 64 verification emojis - // so always send that we support EMOJI - val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL) - - /** - * decimal: generate five bytes by using HKDF. - * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive), - * and add 1000 (resulting in a number between 1000 and 9191 inclusive). - * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers. - * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000, - * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000. - * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic, - * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.) - * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers, - * or with the three numbers on separate lines. - */ - fun getDecimalCodeRepresentation(byteArray: ByteArray, separator: String = " "): String { - val b0 = byteArray[0].toUnsignedInt() // need unsigned byte - val b1 = byteArray[1].toUnsignedInt() // need unsigned byte - val b2 = byteArray[2].toUnsignedInt() // need unsigned byte - val b3 = byteArray[3].toUnsignedInt() // need unsigned byte - val b4 = byteArray[4].toUnsignedInt() // need unsigned byte - // (B0 << 5 | B1 >> 3) + 1000 - val first = (b0.shl(5) or b1.shr(3)) + 1000 - // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 - val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000 - // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000 - val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000 - return "$first$separator$second$separator$third" - } - } - - override var state: VerificationTxState = VerificationTxState.None - set(newState) { - field = newState - - listeners.forEach { - try { - it.transactionUpdated(this) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - - if (newState is VerificationTxState.TerminalTxState) { - releaseSAS() - } - } - - private var olmSas: OlmSAS? = null - - // Visible for test - var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null - - // Visible for test - var accepted: ValidVerificationInfoAccept? = null - protected var otherKey: String? = null - protected var shortCodeBytes: ByteArray? = null - - protected var myMac: ValidVerificationInfoMac? = null - protected var theirMac: ValidVerificationInfoMac? = null - - protected fun getSAS(): OlmSAS { - if (olmSas == null) olmSas = OlmSAS() - return olmSas!! - } - - // To override finalize(), all you need to do is simply declare it, without using the override keyword: - protected fun finalize() { - releaseSAS() - } - - private fun releaseSAS() { - // finalization logic - olmSas?.releaseSas() - olmSas = null - } - - /** - * To be called by the client when the user has verified that - * both short codes do match. - */ - override fun userHasVerifiedShortCode() { - Timber.v("## SAS short code verified by user for id:$transactionId") - if (state != VerificationTxState.ShortCodeReady) { - // ignore and cancel? - Timber.e("## Accepted short code from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - state = VerificationTxState.ShortCodeAccepted - // Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated, - // sorted list of the key IDs that they wish the other user to verify, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_MAC”, - // - the Matrix ID of the user whose key is being MAC-ed, - // - the device ID of the device sending the MAC, - // - the Matrix ID of the other user, - // - the device ID of the device receiving the MAC, - // - the transaction ID, and - // - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs. - val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId" - - // Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key. - // It should now contain both the device key and the MSK. - // So when Alice and Bob verify with SAS, the verification will verify the MSK. - - val keyMap = HashMap() - - val keyId = "ed25519:$deviceId" - val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId) - - if (macString.isNullOrBlank()) { - // Should not happen - Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") - cancel(CancelCode.UnexpectedMessage) - return - } - - keyMap[keyId] = macString - - cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() } - ?.masterKey() - ?.unpaddedBase64PublicKey - ?.let { masterPublicKey -> - val crossSigningKeyId = "ed25519:$masterPublicKey" - macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { mskMacString -> - keyMap[crossSigningKeyId] = mskMacString - } - } - - val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS") - - if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) { - // Should not happen - Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") - cancel(CancelCode.UnexpectedMessage) - return - } - - val macMsg = transport.createMac(transactionId, keyMap, keyStrings) - myMac = macMsg.asValidObject() - state = VerificationTxState.SendingMac - sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) { - if (state == VerificationTxState.SendingMac) { - // It is possible that we receive the next event before this one :/, in this case we should keep state - state = VerificationTxState.MacSent - } - } - - // Do I already have their Mac? - theirMac?.let { verifyMacs(it) } - // if not wait for it - } - - override fun shortCodeDoesNotMatch() { - Timber.v("## SAS short code do not match for id:$transactionId") - cancel(CancelCode.MismatchedSas) - } - - override fun isToDeviceTransport(): Boolean { - return transport is VerificationTransportToDevice - } - - abstract fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) - - abstract fun onVerificationAccept(accept: ValidVerificationInfoAccept) - - abstract fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) - - abstract fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) - - protected fun verifyMacs(theirMacSafe: ValidVerificationInfoMac) { - Timber.v("## SAS verifying macs for id:$transactionId") - state = VerificationTxState.Verifying - - // Keys have been downloaded earlier in process - val otherUserKnownDevices = cryptoStore.getUserDevices(otherUserId) - - // Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID), - // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. - // Bob’s device compares these with the HMAC values given in the m.key.verification.mac message. - // If everything matches, then consider Alice’s device keys as verified. - val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$otherUserId$otherDeviceId$userId$deviceId$transactionId" - - val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",") - - val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") - if (theirMacSafe.keys != keyStrings) { - // WRONG! - cancel(CancelCode.MismatchedKeys) - return - } - - val verifiedDevices = ArrayList() - - // cannot be empty because it has been validated - theirMacSafe.mac.keys.forEach { - val keyIDNoPrefix = it.removePrefix("ed25519:") - val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() - if (otherDeviceKey == null) { - Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") - // just ignore and continue - return@forEach - } - val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) - if (mac != theirMacSafe.mac[it]) { - // WRONG! - Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") - cancel(CancelCode.MismatchedKeys) - return - } - verifiedDevices.add(keyIDNoPrefix) - } - - var otherMasterKeyIsVerified = false - val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() - val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey - if (otherCrossSigningMasterKeyPublic != null) { - // Did the user signed his master key - theirMacSafe.mac.keys.forEach { - val keyIDNoPrefix = it.removePrefix("ed25519:") - if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { - // Check the signature - val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) - if (mac != theirMacSafe.mac[it]) { - // WRONG! - Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") - cancel(CancelCode.MismatchedKeys) - return - } else { - otherMasterKeyIsVerified = true - } - } - } - } - - // if none of the keys could be verified, then error because the app - // should be informed about that - if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) { - Timber.e("## SAS Verification: No devices verified") - cancel(CancelCode.MismatchedKeys) - return - } - - trust( - otherMasterKeyIsVerified, - verifiedDevices, - eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false - ) - } - - override fun cancel() { - cancel(CancelCode.User) - } - - override fun cancel(code: CancelCode) { - state = VerificationTxState.Cancelled(code, true) - transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) - } - - protected fun sendToOther( - type: String, - keyToDevice: VerificationInfo, - nextState: VerificationTxState, - onErrorReason: CancelCode, - onDone: (() -> Unit)? - ) { - transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) - } - - fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { - if (shortCodeBytes == null) { - return null - } - when (shortAuthenticationStringMode) { - SasMode.DECIMAL -> { - if (shortCodeBytes!!.size < 5) return null - return getDecimalCodeRepresentation(shortCodeBytes!!) - } - SasMode.EMOJI -> { - if (shortCodeBytes!!.size < 6) return null - return getEmojiCodeRepresentation(shortCodeBytes!!).joinToString(" ") { it.emoji } - } - else -> return null - } - } - - override fun supportsEmoji(): Boolean { - return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI).orFalse() - } - - override fun supportsDecimal(): Boolean { - return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL).orFalse() - } - - protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256" == accepted?.hash?.lowercase(Locale.ROOT)) { - val olmUtil = OlmUtility() - val hashBytes = olmUtil.sha256(toHash) - olmUtil.releaseUtility() - return hashBytes - } - return null - } - - private fun macUsingAgreedMethod(message: String, info: String): String? { - return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) { - SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) - SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) - else -> null - } - } - - override fun getDecimalCodeRepresentation(): String { - return getDecimalCodeRepresentation(shortCodeBytes!!) - } - - override fun getEmojiCodeRepresentation(): List { - return getEmojiCodeRepresentation(shortCodeBytes!!) - } - - /** - * emoji: generate six bytes by using HKDF. - * Split the first 42 bits into 7 groups of 6 bits, as one would do when creating a base64 encoding. - * For each group of 6 bits, look up the emoji from Appendix A corresponding - * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) - */ - private fun getEmojiCodeRepresentation(byteArray: ByteArray): List { - val b0 = byteArray[0].toUnsignedInt() - val b1 = byteArray[1].toUnsignedInt() - val b2 = byteArray[2].toUnsignedInt() - val b3 = byteArray[3].toUnsignedInt() - val b4 = byteArray[4].toUnsignedInt() - val b5 = byteArray[5].toUnsignedInt() - return listOf( - getEmojiForCode((b0 and 0xFC).shr(2)), - getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), - getEmojiForCode((b1 and 0xF).shl(2) or (b2 and 0xC0).shr(6)), - getEmojiForCode((b2 and 0x3F)), - getEmojiForCode((b3 and 0xFC).shr(2)), - getEmojiForCode((b3 and 0x3).shl(4) or (b4 and 0xF0).shr(4)), - getEmojiForCode((b4 and 0xF).shl(2) or (b5 and 0xC0).shr(6)) - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt new file mode 100644 index 000000000..9c8e327cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SasVerification.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import org.matrix.rustcomponents.sdk.crypto.Sas +import org.matrix.rustcomponents.sdk.crypto.SasListener +import org.matrix.rustcomponents.sdk.crypto.SasState + +/** Class representing a short auth string verification flow. */ +internal class SasVerification @AssistedInject constructor( + @Assisted private var inner: Sas, +// private val olmMachine: OlmMachine, + private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationListenersHolder: VerificationListenersHolder, +) : + SasVerificationTransaction, SasListener { + + init { + inner.setChangesListener(this) + } + + var innerState: SasState = SasState.Started + + @AssistedFactory + interface Factory { + fun create(inner: Sas): SasVerification + } + + /** The user ID of the other user that is participating in this verification flow. */ + override val otherUserId: String = inner.otherUserId() + + /** Get the device id of the other user's device participating in this verification flow. */ + override val otherDeviceId: String + get() = inner.otherDeviceId() + + /** Did the other side initiate this verification flow. */ + override val isIncoming: Boolean + get() = !inner.weStarted() + + private var decimals: List? = null + private var emojis: List? = null + + override fun state(): SasTransactionState { + return when (val state = innerState) { + SasState.Started -> SasTransactionState.SasStarted + SasState.Accepted -> SasTransactionState.SasAccepted + is SasState.KeysExchanged -> { + this.decimals = state.decimals + this.emojis = state.emojis + SasTransactionState.SasShortCodeReady + } + SasState.Confirmed -> SasTransactionState.SasMacSent + SasState.Done -> SasTransactionState.Done(true) + is SasState.Cancelled -> SasTransactionState.Cancelled(safeValueOf(state.cancelInfo.cancelCode), state.cancelInfo.cancelledByUs) + } + } + + /** Get the unique id of this verification. */ + override val transactionId: String + get() = inner.flowId() + + /** Cancel the verification flow. + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to m.user. + * + * Cancelling the verification request will also cancel the parent VerificationRequest. + * + * The method turns into a noop, if the verification flow has already been cancelled. + * */ + override suspend fun cancel() { + cancelHelper(CancelCode.User) + } + + /** Cancel the verification flow. + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to the given CancelCode. + * + * Cancelling the verification request will also cancel the parent VerificationRequest. + * + * The method turns into a noop, if the verification flow has already been cancelled. + * + * @param code The cancel code that should be given as the reason for the cancellation. + * */ + override suspend fun cancel(code: CancelCode) { + cancelHelper(code) + } + + /** Cancel the verification flow. + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to the m.mismatched_sas cancel code. + * + * Cancelling the verification request will also cancel the parent VerificationRequest. + * + * The method turns into a noop, if the verification flow has already been cancelled. + */ + override suspend fun shortCodeDoesNotMatch() { + cancelHelper(CancelCode.MismatchedSas) + } + + override val method: VerificationMethod + get() = VerificationMethod.QR_CODE_SCAN + + /** Is this verification happening over to-device messages. */ + override fun isToDeviceTransport(): Boolean = inner.roomId() == null + +// /** Does the verification flow support showing emojis as the short auth string */ +// override fun supportsEmoji(): Boolean { +// return inner.supportsEmoji() +// } + + /** Confirm that the short authentication code matches on both sides. + * + * This sends a m.key.verification.mac event out, the verification isn't yet + * done, we still need to receive such an event from the other side if we haven't + * already done so. + * + * This method is a noop if we're not yet in a presentable state, i.e. we didn't receive + * a m.key.verification.key event from the other side or we're cancelled. + */ + override suspend fun userHasVerifiedShortCode() { + confirm() + } + + /** Accept the verification flow, signaling the other side that we do want to verify. + * + * This sends a m.key.verification.accept event out that is a response to a + * m.key.verification.start event from the other side. + * + * This method is a noop if we send the start event out or if the verification has already + * been accepted. + */ + override suspend fun acceptVerification() { + accept() + } + + /** Get the decimal representation of the short auth string. + * + * @return A string of three space delimited numbers that + * represent the short auth string or an empty string if we're not yet + * in a presentable state. + */ + override fun getDecimalCodeRepresentation(): String { + return decimals?.joinToString(" ") ?: "" + } + + /** Get the emoji representation of the short auth string. + * + * @return A list of 7 EmojiRepresentation objects that represent the + * short auth string or an empty list if we're not yet in a presentable + * state. + */ + override fun getEmojiCodeRepresentation(): List { + return emojis?.map { getEmojiForCode(it) } ?: listOf() + } + + internal suspend fun accept() { + val request = inner.accept() ?: return Unit.also { + // TODO should throw here? + } + try { + sender.sendVerificationRequest(request) + } catch (failure: Throwable) { + cancelHelper(CancelCode.UserError) + } + } + + @Throws(CryptoStoreException::class) + private suspend fun confirm() { + val result = withContext(coroutineDispatchers.io) { + inner.confirm() + } ?: return + try { + for (verificationRequest in result.requests) { + sender.sendVerificationRequest(verificationRequest) + verificationListenersHolder.dispatchTxUpdated(this) + } + val signatureRequest = result.signatureRequest + if (signatureRequest != null) { + sender.sendSignatureUpload(signatureRequest) + } + } catch (failure: Throwable) { + cancelHelper(CancelCode.UserError) + } + } + + private suspend fun cancelHelper(code: CancelCode) = withContext(NonCancellable) { + val request = inner.cancel(code.value) ?: return@withContext + tryOrNull("Fail to send cancel request") { + sender.sendVerificationRequest(request, retryCount = Int.MAX_VALUE) + } + verificationListenersHolder.dispatchTxUpdated(this@SasVerification) + } + + override fun onChange(state: SasState) { + innerState = state + verificationListenersHolder.dispatchTxUpdated(this) + } + + override fun toString(): String { + return "SasVerification(" + + "otherUserId='$otherUserId', " + + "otherDeviceId=$otherDeviceId, " + + "isIncoming=$isIncoming, " + + "state=${state()}, " + + "transactionId='$transactionId')" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt index cff359177..cf4932efd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.verification import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.internal.extensions.toUnsignedInt internal fun getEmojiForCode(code: Int): EmojiRepresentation { return when (code % 64) { @@ -86,3 +87,54 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation { /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) } } + +/** + * decimal: generate five bytes by using HKDF. + * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive), + * and add 1000 (resulting in a number between 1000 and 9191 inclusive). + * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers. + * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000, + * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000. + * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic, + * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.) + * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers, + * or with the three numbers on separate lines. + */ +fun ByteArray.getDecimalCodeRepresentation(separator: String = " "): String { + val b0 = this[0].toUnsignedInt() // need unsigned byte + val b1 = this[1].toUnsignedInt() // need unsigned byte + val b2 = this[2].toUnsignedInt() // need unsigned byte + val b3 = this[3].toUnsignedInt() // need unsigned byte + val b4 = this[4].toUnsignedInt() // need unsigned byte + // (B0 << 5 | B1 >> 3) + 1000 + val first = (b0.shl(5) or b1.shr(3)) + 1000 + // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 + val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000 + // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000 + val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000 + return listOf(first, second, third).joinToString(separator) +} + +/** + * emoji: generate six bytes by using HKDF. + * Split the first 42 bits into 7 groups of 6 bits, as one would do when creating a base64 encoding. + * For each group of 6 bits, look up the emoji from Appendix A corresponding + * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) + */ +fun ByteArray.getEmojiCodeRepresentation(): List { + val b0 = this[0].toUnsignedInt() + val b1 = this[1].toUnsignedInt() + val b2 = this[2].toUnsignedInt() + val b3 = this[3].toUnsignedInt() + val b4 = this[4].toUnsignedInt() + val b5 = this[5].toUnsignedInt() + return listOf( + getEmojiForCode((b0 and 0xFC).shr(2)), + getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), + getEmojiForCode((b1 and 0xF).shl(2) or (b2 and 0xC0).shr(6)), + getEmojiForCode((b2 and 0x3F)), + getEmojiForCode((b3 and 0xFC).shr(2)), + getEmojiForCode((b3 and 0x3).shl(4) or (b4 and 0xF0).shr(4)), + getEmojiForCode((b4 and 0xF).shl(2) or (b5 and 0xC0).shr(6)) + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt deleted file mode 100644 index 0615773a7..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject -import org.matrix.android.sdk.api.session.events.model.Content - -internal interface VerificationInfo { - fun toEventContent(): Content? = null - fun toSendToDeviceObject(): SendToDeviceObject? = null - - fun asValidObject(): ValidObjectType? - - /** - * String to identify the transaction. - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - * Alice’s device should record this ID and use it in future messages in this transaction. - */ - val transactionId: String? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt deleted file mode 100644 index 0b9287cb0..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoAccept.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoAccept : VerificationInfo { - /** - * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val keyAgreementProtocol: String? - - /** - * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val hash: String? - - /** - * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device. - */ - val messageAuthenticationCode: String? - - /** - * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device. - */ - val shortAuthenticationStrings: List? - - /** - * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) - * and the canonical JSON representation of the m.key.verification.start message. - */ - var commitment: String? - - override fun asValidObject(): ValidVerificationInfoAccept? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null - val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null - val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null - val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null - val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoAccept( - validTransactionId, - validKeyAgreementProtocol, - validHash, - validMessageAuthenticationCode, - validShortAuthenticationStrings, - validCommitment - ) - } -} - -internal interface VerificationInfoAcceptFactory { - - fun create( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept -} - -internal data class ValidVerificationInfoAccept( - val transactionId: String, - val keyAgreementProtocol: String, - val hash: String, - val messageAuthenticationCode: String, - val shortAuthenticationStrings: List, - var commitment: String? -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt deleted file mode 100644 index 20e2cdcd3..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoCancel.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoCancel : VerificationInfo { - /** - * machine-readable reason for cancelling, see [CancelCode]. - */ - val code: String? - - /** - * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. - */ - val reason: String? - - override fun asValidObject(): ValidVerificationInfoCancel? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validCode = code?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoCancel( - validTransactionId, - validCode, - reason - ) - } -} - -internal data class ValidVerificationInfoCancel( - val transactionId: String, - val code: String, - val reason: String? -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt deleted file mode 100644 index dfbe45a64..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoDone.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone - -internal interface VerificationInfoDone : VerificationInfo { - - override fun asValidObject(): ValidVerificationDone? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - return ValidVerificationDone(validTransactionId) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt deleted file mode 100644 index 2885b81a1..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoKey.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -/** - * Sent by both devices to send their ephemeral Curve25519 public key to the other device. - */ -internal interface VerificationInfoKey : VerificationInfo { - /** - * The device’s ephemeral public key, as an unpadded base64 string. - */ - val key: String? - - override fun asValidObject(): ValidVerificationInfoKey? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validKey = key?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoKey( - validTransactionId, - validKey - ) - } -} - -internal interface VerificationInfoKeyFactory { - fun create(tid: String, pubKey: String): VerificationInfoKey -} - -internal data class ValidVerificationInfoKey( - val transactionId: String, - val key: String -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt deleted file mode 100644 index d6f1d7e4d..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoMac.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -internal interface VerificationInfoMac : VerificationInfo { - /** - * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key. - */ - val mac: Map? - - /** - * The MAC of the comma-separated, sorted list of key IDs given in the mac property, - * as an unpadded base64 string, calculated using the MAC key. - * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will - * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. - */ - val keys: String? - - override fun asValidObject(): ValidVerificationInfoMac? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null - val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoMac( - validTransactionId, - validMac, - validKeys - ) - } -} - -internal interface VerificationInfoMacFactory { - fun create(tid: String, mac: Map, keys: String): VerificationInfoMac -} - -internal data class ValidVerificationInfoMac( - val transactionId: String, - val mac: Map, - val keys: String -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt deleted file mode 100644 index 2e397eee0..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoReady.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady - -/** - * A new event type is added to the key verification framework: m.key.verification.ready, - * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. - * - * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly - * with a m.key.verification.start event instead. - */ - -internal interface VerificationInfoReady : VerificationInfo { - /** - * The ID of the device that sent the m.key.verification.ready message. - */ - val fromDevice: String? - - /** - * An array of verification methods that the device supports. - */ - val methods: List? - - override fun asValidObject(): ValidVerificationInfoReady? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null - val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoReady( - validTransactionId, - validFromDevice, - validMethods - ) - } -} - -internal interface MessageVerificationReadyFactory { - fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt deleted file mode 100644 index 1cf72308b..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoRequest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest - -internal interface VerificationInfoRequest : VerificationInfo { - - /** - * Required. The device ID which is initiating the request. - */ - val fromDevice: String? - - /** - * Required. The verification methods supported by the sender. - */ - val methods: List? - - /** - * The POSIX timestamp in milliseconds for when the request was made. - * If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - * the message should be ignored by the receiver. - */ - val timestamp: Long? - - override fun asValidObject(): ValidVerificationInfoRequest? { - // FIXME No check on Timestamp? - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null - val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null - - return ValidVerificationInfoRequest( - validTransactionId, - validFromDevice, - validMethods, - timestamp - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt deleted file mode 100644 index 66591fe00..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.SasMode -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS - -internal interface VerificationInfoStart : VerificationInfo { - - val method: String? - - /** - * Alice’s device ID. - */ - val fromDevice: String? - - /** - * An array of key agreement protocols that Alice’s client understands. - * Must include “curve25519”. - * Other methods may be defined in the future - */ - val keyAgreementProtocols: List? - - /** - * An array of hashes that Alice’s client understands. - * Must include “sha256”. Other methods may be defined in the future. - */ - val hashes: List? - - /** - * An array of message authentication codes that Alice’s client understands. - * Must include “hkdf-hmac-sha256”. - * Other methods may be defined in the future. - */ - val messageAuthenticationCodes: List? - - /** - * An array of short authentication string methods that Alice’s client (and Alice) understands. - * Must include “decimal”. - * This document also describes the “emoji” method. - * Other methods may be defined in the future - */ - val shortAuthenticationStrings: List? - - /** - * Shared secret, when starting verification with QR code. - */ - val sharedSecret: String? - - fun toCanonicalJson(): String - - override fun asValidObject(): ValidVerificationInfoStart? { - val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null - val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null - - return when (method) { - VERIFICATION_METHOD_SAS -> { - val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null - val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null - val validMessageAuthenticationCodes = messageAuthenticationCodes - ?.takeIf { - it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256) || - it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF) - } - ?: return null - val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null - - ValidVerificationInfoStart.SasVerificationInfoStart( - validTransactionId, - validFromDevice, - validKeyAgreementProtocols, - validHashes, - validMessageAuthenticationCodes, - validShortAuthenticationStrings, - canonicalJson = toCanonicalJson() - ) - } - VERIFICATION_METHOD_RECIPROCATE -> { - val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null - - ValidVerificationInfoStart.ReciprocateVerificationInfoStart( - validTransactionId, - validFromDevice, - validSharedSecret - ) - } - else -> null - } - } -} - -internal sealed class ValidVerificationInfoStart( - open val transactionId: String, - open val fromDevice: String -) { - data class SasVerificationInfoStart( - override val transactionId: String, - override val fromDevice: String, - val keyAgreementProtocols: List, - val hashes: List, - val messageAuthenticationCodes: List, - val shortAuthenticationStrings: List, - val canonicalJson: String - ) : ValidVerificationInfoStart(transactionId, fromDevice) - - data class ReciprocateVerificationInfoStart( - override val transactionId: String, - override val fromDevice: String, - val sharedSecret: String - ) : ValidVerificationInfoStart(transactionId, fromDevice) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt new file mode 100644 index 000000000..3b47d908f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationListenersHolder.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.dbgState +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class VerificationListenersHolder @Inject constructor( + coroutineDispatchers: MatrixCoroutineDispatchers, + @UserId myUserId: String, +) { + + val myUserId = myUserId.take(5) + + val scope = CoroutineScope(SupervisorJob() + coroutineDispatchers.dmVerif) + val eventFlow = MutableSharedFlow(extraBufferCapacity = 20, onBufferOverflow = BufferOverflow.SUSPEND) + + fun dispatchTxAdded(tx: VerificationTransaction) { + scope.launch { + Timber.v("## SAS [$myUserId] dispatchTxAdded txId:${tx.transactionId} | ${tx.dbgState()}") + eventFlow.emit(VerificationEvent.TransactionAdded(tx)) + } + } + + fun dispatchTxUpdated(tx: VerificationTransaction) { + scope.launch { + Timber.v("## SAS [$myUserId] dispatchTxUpdated txId:${tx.transactionId} | ${tx.dbgState()}") + eventFlow.emit(VerificationEvent.TransactionUpdated(tx)) + } + } + + fun dispatchRequestAdded(verificationRequest: VerificationRequest) { + scope.launch { + Timber.v("## SAS [$myUserId] dispatchRequestAdded txId:${verificationRequest.flowId()} state:${verificationRequest.innerState()}") + eventFlow.emit(VerificationEvent.RequestAdded(verificationRequest.toPendingVerificationRequest())) + } + } + + fun dispatchRequestUpdated(verificationRequest: VerificationRequest) { + scope.launch { + Timber.v("## SAS [$myUserId] dispatchRequestUpdated txId:${verificationRequest.flowId()} state:${verificationRequest.innerState()}") + eventFlow.emit(VerificationEvent.RequestUpdated(verificationRequest.toPendingVerificationRequest())) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt deleted file mode 100644 index 8a805a558..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import javax.inject.Inject - -internal class VerificationMessageProcessor @Inject constructor( - private val verificationService: DefaultVerificationService, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, - private val clock: Clock, -) { - - private val transactionsHandledByOtherDevice = ArrayList() - - private val allowedTypes = listOf( - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_READY, - EventType.MESSAGE, - EventType.ENCRYPTED - ) - - fun shouldProcess(eventType: String): Boolean { - return allowedTypes.contains(eventType) - } - - suspend fun process(event: Event) { - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}") - - // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - // the message should be ignored by the receiver. - - if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:${event.ageLocalTs} ms") - } - - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") - - // Relates to is not encrypted - val relatesToEventId = event.content.toModel()?.relatesTo?.eventId - - if (event.senderId == userId) { - // If it's send from me, we need to keep track of Requests or Start - // done from another device of mine - if (EventType.MESSAGE == event.getClearType()) { - val msgType = event.getClearContent().toModel()?.msgType - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is requested from another device - Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") - event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } - } else if (EventType.KEY_VERIFICATION_START == event.getClearType()) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") - relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - verificationService.onRoomRequestHandledByOtherDevice(event) - } - } - } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") - relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - verificationService.onRoomRequestHandledByOtherDevice(event) - } - } - } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) { - relatesToEventId?.let { - transactionsHandledByOtherDevice.remove(it) - verificationService.onRoomRequestHandledByOtherDevice(event) - } - } else if (EventType.ENCRYPTED == event.getClearType()) { - verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) - } - - Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") - return - } - - if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ") - return - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_DONE -> { - verificationService.onRoomEvent(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.msgType) { - verificationService.onRoomRequestReceived(event) - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt new file mode 100644 index 000000000..cded4d596 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf +import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification +import org.matrix.android.sdk.internal.util.time.Clock +import org.matrix.rustcomponents.sdk.crypto.VerificationRequestListener +import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState +import timber.log.Timber +import org.matrix.rustcomponents.sdk.crypto.VerificationRequest as InnerVerificationRequest + +fun InnerVerificationRequest.dbgString(): String { + val that = this + return buildString { + append("(") + append("flowId=${that.flowId()}") + append("state=${that.state()},") + append(")") + } +} + +/** A verification request object + * + * This represents a verification flow that starts with a m.key.verification.request event + * + * Once the VerificationRequest gets to a ready state users can transition into the different + * concrete verification flows. + */ +internal class VerificationRequest @AssistedInject constructor( + @Assisted private var innerVerificationRequest: InnerVerificationRequest, + olmMachine: OlmMachine, + private val requestSender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationListenersHolder: VerificationListenersHolder, + private val sasVerificationFactory: SasVerification.Factory, + private val qrCodeVerificationFactory: QrCodeVerification.Factory, + private val clock: Clock, +) : VerificationRequestListener { + + private val innerOlmMachine = olmMachine.inner() + + @AssistedFactory + interface Factory { + fun create(innerVerificationRequest: InnerVerificationRequest): VerificationRequest + } + + init { + innerVerificationRequest.setChangesListener(this) + } + + fun startQrCode() { + innerVerificationRequest.startQrVerification() + } + +// internal fun dispatchRequestUpdated() { +// val tx = toPendingVerificationRequest() +// verificationListenersHolder.dispatchRequestUpdated(tx) +// } + + /** Get the flow ID of this verification request + * + * This is either the transaction ID if the verification is happening + * over to-device events, or the event ID of the m.key.verification.request + * event that initiated the flow. + */ + internal fun flowId(): String { + return innerVerificationRequest.flowId() + } + + fun innerState() = innerVerificationRequest.state() + + /** The user ID of the other user that is participating in this verification flow. */ + internal fun otherUser(): String { + return innerVerificationRequest.otherUserId() + } + + /** The device ID of the other user's device that is participating in this verification flow + * + * This will we null if we're initiating the request and the other side + * didn't yet accept the verification flow. + * */ + internal fun otherDeviceId(): String? { + return innerVerificationRequest.otherDeviceId() + } + + /** Did we initiate this verification flow. */ + internal fun weStarted(): Boolean { + return innerVerificationRequest.weStarted() + } + + /** Get the id of the room where this verification is happening + * + * Will be null if the verification is not happening inside a room. + */ + internal fun roomId(): String? { + return innerVerificationRequest.roomId() + } + + /** Did the non-initiating side respond with a m.key.verification.read event + * + * Once the verification request is ready, we're able to transition into a + * concrete verification flow, i.e. we can show/scan a QR code or start emoji + * verification. + */ +// internal fun isReady(): Boolean { +// return innerVerificationRequest.isReady() +// } + + /** Did we advertise that we're able to scan QR codes. */ + internal fun canScanQrCodes(): Boolean { + return innerVerificationRequest.ourSupportedMethods()?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) ?: false + } + + /** Accept the verification request advertising the given methods as supported + * + * This will send out a m.key.verification.ready event advertising support for + * the given verification methods to the other side. After this method call, the + * verification request will be considered to be ready and will be able to transition + * into concrete verification flows. + * + * The method turns into a noop, if the verification flow has already been accepted + * and is in the ready state, which can be checked with the isRead() method. + * + * @param methods The list of VerificationMethod that we wish to advertise to the other + * side as supported. + */ + suspend fun acceptWithMethods(methods: List) { + val stringMethods = prepareMethods(methods) + + val request = innerVerificationRequest.accept(stringMethods) + ?: return // should throw here? + try { + requestSender.sendVerificationRequest(request) + } catch (failure: Throwable) { + cancel() + } + } + +// var activeQRCode: QrCode? = null + + /** Transition from a ready verification request into emoji verification + * + * This method will move the verification forward into emoji verification, + * it will send out a m.key.verification.start event with the method set to + * m.sas.v1. + * + * Note: This method will be a noop and return null if the verification request + * isn't considered to be ready, you can check if the request is ready using the + * isReady() method. + * + * @return A freshly created SasVerification object that represents the newly started + * emoji verification, or null if we can't yet transition into emoji verification. + */ + internal suspend fun startSasVerification(): SasVerification? { + return withContext(coroutineDispatchers.io) { + val result = innerVerificationRequest.startSasVerification() + ?: return@withContext null.also { + Timber.w("Failed to start verification") + } + try { + requestSender.sendVerificationRequest(result.request) + sasVerificationFactory.create(result.sas) + } catch (failure: Throwable) { + cancel() + null + } + } + } + + /** Scan a QR code and transition into QR code verification + * + * This method will move the verification forward into QR code verification. + * It will send out a m.key.verification.start event with the method + * set to m.reciprocate.v1. + * + * Note: This method will be a noop and return null if the verification request + * isn't considered to be ready, you can check if the request is ready using the + * isReady() method. + * + * @return A freshly created QrCodeVerification object that represents the newly started + * QR code verification, or null if we can't yet transition into QR code verification. + */ + internal suspend fun scanQrCode(data: String): QrCodeVerification? { + // TODO again, what's the deal with ISO_8859_1? + val byteArray = data.toByteArray(Charsets.ISO_8859_1) + val encodedData = byteArray.toBase64NoPadding() +// val result = innerOlmMachine.scanQrCode(otherUser(), flowId(), encodedData) ?: return null + val result = innerVerificationRequest.scanQrCode(encodedData) ?: return null + try { + requestSender.sendVerificationRequest(result.request) + } catch (failure: Throwable) { + cancel() + return null + } + return qrCodeVerificationFactory.create(result.qr) + } + + /** Cancel the verification flow + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to m.user. + * + * Cancelling the verification request will also cancel any QrcodeVerification and + * SasVerification objects that are related to this verification request. + * + * The method turns into a noop, if the verification flow has already been cancelled. + */ + internal suspend fun cancel() = withContext(NonCancellable) { + val request = innerVerificationRequest.cancel() ?: return@withContext + tryOrNull("Fail to send cancel request") { + requestSender.sendVerificationRequest(request, retryCount = Int.MAX_VALUE) + } + } + + private fun state(): EVerificationState { + Timber.v("Verification state() ${innerVerificationRequest.state()}") + when (innerVerificationRequest.state()) { + VerificationRequestState.Requested -> { + return if (weStarted()) { + EVerificationState.WaitingForReady + } else { + EVerificationState.Requested + } + } + is VerificationRequestState.Ready -> { + val started = innerOlmMachine.getVerification(otherUser(), flowId()) + if (started != null) { + val asSas = started.asSas() + if (asSas != null) { + return if (asSas.weStarted()) { + EVerificationState.WeStarted + } else { + EVerificationState.Started + } + } + val asQR = started.asQr() + if (asQR != null) { + if (asQR.reciprocated() || asQR.hasBeenScanned()) { + return if (weStarted()) { + EVerificationState.WeStarted + } else EVerificationState.Started + } + } + } + return EVerificationState.Ready + } + VerificationRequestState.Done -> { + return EVerificationState.Done + } + is VerificationRequestState.Cancelled -> { + return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) { + EVerificationState.HandledByOtherSession + } else { + EVerificationState.Cancelled + } + } + } +// +// if (innerVerificationRequest.isCancelled()) { +// return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) { +// EVerificationState.HandledByOtherSession +// } else { +// EVerificationState.Cancelled +// } +// } +// if (innerVerificationRequest.isPassive()) { +// return EVerificationState.HandledByOtherSession +// } +// if (innerVerificationRequest.isDone()) { +// return EVerificationState.Done +// } +// +// val started = innerOlmMachine.getVerification(otherUser(), flowId()) +// if (started != null) { +// val asSas = started.asSas() +// if (asSas != null) { +// return if (asSas.weStarted()) { +// EVerificationState.WeStarted +// } else { +// EVerificationState.Started +// } +// } +// val asQR = started.asQr() +// if (asQR != null) { +// if (asQR.reciprocated() || asQR.hasBeenScanned()) { +// return if (weStarted()) { +// EVerificationState.WeStarted +// } else EVerificationState.Started +// } +// } +// } +// if (innerVerificationRequest.isReady()) { +// return EVerificationState.Ready +// } + } + + /** Convert the VerificationRequest into a PendingVerificationRequest + * + * The public interface of the VerificationService dispatches the data class + * PendingVerificationRequest, this method allows us to easily transform this + * request into the data class. It fetches fresh info from the Rust side before + * it does the transform. + * + * @return The PendingVerificationRequest that matches data from this VerificationRequest. + */ + internal fun toPendingVerificationRequest(): PendingVerificationRequest { + val cancelInfo = innerVerificationRequest.cancelInfo() + val cancelCode = + if (cancelInfo != null) { + safeValueOf(cancelInfo.cancelCode) + } else { + null + } + + val ourMethods = innerVerificationRequest.ourSupportedMethods() + val theirMethods = innerVerificationRequest.theirSupportedMethods() + val otherDeviceId = innerVerificationRequest.otherDeviceId() + + return PendingVerificationRequest( + // Creation time + ageLocalTs = clock.epochMillis(), + state = state(), + // Who initiated the request + isIncoming = !innerVerificationRequest.weStarted(), + // Local echo id, what to do here? + otherDeviceId = innerVerificationRequest.otherDeviceId(), + // other user + otherUserId = innerVerificationRequest.otherUserId(), + // room id + roomId = innerVerificationRequest.roomId(), + // transaction id + transactionId = innerVerificationRequest.flowId(), + // cancel code if there is one + cancelConclusion = cancelCode, + isFinished = innerVerificationRequest.isDone() || innerVerificationRequest.isCancelled(), + // did another device answer the request + handledByOtherSession = innerVerificationRequest.isPassive(), + // devices that should receive the events we send out + targetDevices = otherDeviceId?.let { listOf(it) }, + qrCodeText = getQrCode(), + isSasSupported = ourMethods.canSas() && theirMethods.canSas(), + weShouldDisplayQRCode = theirMethods.canScanQR() && ourMethods.canShowQR(), + weShouldShowScanOption = ourMethods.canScanQR() && theirMethods.canShowQR() + ) + } + + private fun getQrCode(): String? { + return innerOlmMachine.getVerification(otherUser(), flowId())?.asQr()?.generateQrCode()?.fromBase64()?.let { + String(it, Charsets.ISO_8859_1) + } + } + + override fun onChange(state: VerificationRequestState) { + verificationListenersHolder.dispatchRequestUpdated(this) + } + + override fun toString(): String { + return super.toString() + "\n${innerVerificationRequest.dbgString()}" + } + + private fun List?.canSas() = orEmpty().contains(VERIFICATION_METHOD_SAS) + private fun List?.canShowQR() = orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE) && orEmpty().contains(VERIFICATION_METHOD_QR_CODE_SHOW) + private fun List?.canScanQR() = orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE) && orEmpty().contains(VERIFICATION_METHOD_QR_CODE_SCAN) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt deleted file mode 100644 index 5314c2387..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState - -/** - * Verification can be performed using toDevice events or via DM. - * This class abstracts the concept of transport for verification - */ -internal interface VerificationTransport { - - /** - * Sends a message. - */ - fun sendToOther( - type: String, - verificationInfo: VerificationInfo, - nextState: VerificationTxState, - onErrorReason: CancelCode, - onDone: (() -> Unit)? - ) - - /** - * @param supportedMethods list of supported method by this client - * @param localId a local Id - * @param otherUserId the user id to send the verification request to - * @param roomId a room Id to use to send verification message - * @param toDevices list of device Ids - * @param callback will be called with eventId and ValidVerificationInfoRequest in case of success - */ - fun sendVerificationRequest( - supportedMethods: List, - localId: String, - otherUserId: String, - roomId: String?, - toDevices: List?, - callback: (String?, ValidVerificationInfoRequest?) -> Unit - ) - - fun cancelTransaction( - transactionId: String, - otherUserId: String, - otherUserDeviceId: String?, - code: CancelCode - ) - - fun cancelTransaction( - transactionId: String, - otherUserId: String, - otherUserDeviceIds: List, - code: CancelCode - ) - - fun done( - transactionId: String, - onDone: (() -> Unit)? - ) - - /** - * Creates an accept message suitable for this transport. - */ - fun createAccept( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept - - fun createKey( - tid: String, - pubKey: String - ): VerificationInfoKey - - /** - * Create start for SAS verification. - */ - fun createStartForSas( - fromDevice: String, - transactionId: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List - ): VerificationInfoStart - - /** - * Create start for QR code verification. - */ - fun createStartForQrCode( - fromDevice: String, - transactionId: String, - sharedSecret: String - ): VerificationInfoStart - - fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac - - fun createReady( - tid: String, - fromDevice: String, - methods: List - ): VerificationInfoReady - - // TODO Refactor - fun sendVerificationReady( - keyReq: VerificationInfoReady, - otherUserId: String, - otherDeviceId: String?, - callback: (() -> Unit)? - ) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt deleted file mode 100644 index f38a60489..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.UnsignedData -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationDoneContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent -import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber -import java.util.concurrent.Executors - -internal class VerificationTransportRoomMessage( - private val sendVerificationMessageTask: SendVerificationMessageTask, - private val userId: String, - private val userDeviceId: String?, - private val roomId: String, - private val localEchoEventFactory: LocalEchoEventFactory, - private val tx: DefaultVerificationTransaction?, - cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) : VerificationTransport { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - override fun sendToOther( - type: String, - verificationInfo: VerificationInfo, - nextState: VerificationTxState, - onErrorReason: CancelCode, - onDone: (() -> Unit)? - ) { - Timber.d("## SAS sending msg type $type") - Timber.v("## SAS sending msg info $verificationInfo") - val event = createEventAndLocalEcho( - type = type, - roomId = roomId, - content = verificationInfo.toEventContent()!! - ) - - verificationSenderScope.launch { - sequencer.post { - try { - val params = SendVerificationMessageTask.Params(event) - sendVerificationMessageTask.executeRetry(params, 5) - // Do I need to update local echo state to sent? - if (onDone != null) { - onDone() - } else { - tx?.state = nextState - } - } catch (failure: Throwable) { - tx?.cancel(onErrorReason) - } - } - } - } - - override fun sendVerificationRequest( - supportedMethods: List, - localId: String, - otherUserId: String, - roomId: String?, - toDevices: List?, - callback: (String?, ValidVerificationInfoRequest?) -> Unit - ) { - Timber.d("## SAS sending verification request with supported methods: $supportedMethods") - // This transport requires a room - requireNotNull(roomId) - - val validInfo = ValidVerificationInfoRequest( - transactionId = "", - fromDevice = userDeviceId ?: "", - methods = supportedMethods, - timestamp = clock.epochMillis() - ) - - val info = MessageVerificationRequestContent( - body = "$userId is requesting to verify your key, but your client does not support in-chat key verification." + - " You will need to use legacy key verification to verify keys.", - fromDevice = validInfo.fromDevice, - toUserId = otherUserId, - timestamp = validInfo.timestamp, - methods = validInfo.methods - ) - val content = info.toContent() - - val event = createEventAndLocalEcho( - localId = localId, - type = EventType.MESSAGE, - roomId = roomId, - content = content - ) - - verificationSenderScope.launch { - val params = SendVerificationMessageTask.Params(event) - sequencer.post { - try { - val eventId = sendVerificationMessageTask.executeRetry(params, 5) - // Do I need to update local echo state to sent? - callback(eventId, validInfo) - } catch (failure: Throwable) { - callback(null, null) - } - } - } - } - - override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - val event = createEventAndLocalEcho( - type = EventType.KEY_VERIFICATION_CANCEL, - roomId = roomId, - content = MessageVerificationCancelContent.create(transactionId, code).toContent() - ) - - verificationSenderScope.launch { - sequencer.post { - try { - val params = SendVerificationMessageTask.Params(event) - sendVerificationMessageTask.executeRetry(params, 5) - } catch (failure: Throwable) { - Timber.w(failure, "Failed to cancel verification transaction") - } - } - } - } - - override fun cancelTransaction( - transactionId: String, - otherUserId: String, - otherUserDeviceIds: List, - code: CancelCode - ) = cancelTransaction(transactionId, otherUserId, null, code) - - override fun done( - transactionId: String, - onDone: (() -> Unit)? - ) { - Timber.d("## SAS sending done for $transactionId") - val event = createEventAndLocalEcho( - type = EventType.KEY_VERIFICATION_DONE, - roomId = roomId, - content = MessageVerificationDoneContent( - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - transactionId - ) - ).toContent() - ) - verificationSenderScope.launch { - sequencer.post { - try { - val params = SendVerificationMessageTask.Params(event) - sendVerificationMessageTask.executeRetry(params, 5) - } catch (failure: Throwable) { - Timber.w(failure, "Failed to complete (done) verification") - // should we call onDone? - } finally { - onDone?.invoke() - } - } - } - } - - override fun createAccept( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept = - MessageVerificationAcceptContent.create( - tid, - keyAgreementProtocol, - hash, - commitment, - messageAuthenticationCode, - shortAuthenticationStrings - ) - - override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) - - override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) - - override fun createStartForSas( - fromDevice: String, - transactionId: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List - ): VerificationInfoStart { - return MessageVerificationStartContent( - fromDevice, - hashes, - keyAgreementProtocols, - messageAuthenticationCodes, - shortAuthenticationStrings, - VERIFICATION_METHOD_SAS, - RelationDefaultContent( - type = RelationType.REFERENCE, - eventId = transactionId - ), - null - ) - } - - override fun createStartForQrCode( - fromDevice: String, - transactionId: String, - sharedSecret: String - ): VerificationInfoStart { - return MessageVerificationStartContent( - fromDevice, - null, - null, - null, - null, - VERIFICATION_METHOD_RECIPROCATE, - RelationDefaultContent( - type = RelationType.REFERENCE, - eventId = transactionId - ), - sharedSecret - ) - } - - override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { - return MessageVerificationReadyContent( - fromDevice = fromDevice, - relatesTo = RelationDefaultContent( - type = RelationType.REFERENCE, - eventId = tid - ), - methods = methods - ) - } - - private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { - return Event( - roomId = roomId, - originServerTs = clock.epochMillis(), - senderId = userId, - eventId = localId, - type = type, - content = content, - unsignedData = UnsignedData(age = null, transactionId = localId) - ).also { - localEchoEventFactory.createLocalEcho(it) - } - } - - override fun sendVerificationReady( - keyReq: VerificationInfoReady, - otherUserId: String, - otherDeviceId: String?, - callback: (() -> Unit)? - ) { - // Not applicable (send event is called directly) - Timber.w("## SAS ignored verification ready with methods: ${keyReq.methods}") - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt deleted file mode 100644 index 345948e60..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class VerificationTransportRoomMessageFactory @Inject constructor( - private val sendVerificationMessageTask: SendVerificationMessageTask, - @UserId - private val userId: String, - @DeviceId - private val deviceId: String?, - private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) { - - fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { - return VerificationTransportRoomMessage( - sendVerificationMessageTask = sendVerificationMessageTask, - userId = userId, - userDeviceId = deviceId, - roomId = roomId, - localEchoEventFactory = localEchoEventFactory, - tx = tx, - cryptoCoroutineScope = cryptoCoroutineScope, - clock = clock, - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt deleted file mode 100644 index 23a75d2bb..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest -import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE -import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber - -// TODO var could be val -internal class VerificationTransportToDevice( - private var tx: DefaultVerificationTransaction?, - private var sendToDeviceTask: SendToDeviceTask, - private val myDeviceId: String?, - private var taskExecutor: TaskExecutor, - private val clock: Clock, -) : VerificationTransport { - - override fun sendVerificationRequest( - supportedMethods: List, - localId: String, - otherUserId: String, - roomId: String?, - toDevices: List?, - callback: (String?, ValidVerificationInfoRequest?) -> Unit - ) { - Timber.d("## SAS sending verification request with supported methods: $supportedMethods") - val contentMap = MXUsersDevicesMap() - val validKeyReq = ValidVerificationInfoRequest( - transactionId = localId, - fromDevice = myDeviceId ?: "", - methods = supportedMethods, - timestamp = clock.epochMillis() - ) - val keyReq = KeyVerificationRequest( - fromDevice = validKeyReq.fromDevice, - methods = validKeyReq.methods, - timestamp = validKeyReq.timestamp, - transactionId = validKeyReq.transactionId - ) - toDevices?.forEach { - contentMap.setObject(otherUserId, it, keyReq) - } - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## verification [${tx?.transactionId}] send toDevice request success") - callback.invoke(localId, validKeyReq) - } - - override fun onFailure(failure: Throwable) { - Timber.e("## verification [${tx?.transactionId}] failed to send toDevice request") - } - } - } - .executeBy(taskExecutor) - } - - override fun sendVerificationReady( - keyReq: VerificationInfoReady, - otherUserId: String, - otherDeviceId: String?, - callback: (() -> Unit)? - ) { - Timber.d("## SAS sending verification ready with methods: ${keyReq.methods}") - val contentMap = MXUsersDevicesMap() - - contentMap.setObject(otherUserId, otherDeviceId, keyReq) - - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_READY, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## verification [${tx?.transactionId}] send toDevice request success") - callback?.invoke() - } - - override fun onFailure(failure: Throwable) { - Timber.e("## verification [${tx?.transactionId}] failed to send toDevice request") - } - } - } - .executeBy(taskExecutor) - } - - override fun sendToOther( - type: String, - verificationInfo: VerificationInfo, - nextState: VerificationTxState, - onErrorReason: CancelCode, - onDone: (() -> Unit)? - ) { - Timber.d("## SAS sending msg type $type") - Timber.v("## SAS sending msg info $verificationInfo") - val stateBeforeCall = tx?.state - val tx = tx ?: return - val contentMap = MXUsersDevicesMap() - val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() - ?: return Unit.also { tx.cancel() } - - contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject) - - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(type, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [${tx.transactionId}] toDevice type '$type' success.") - if (onDone != null) { - onDone() - } else { - // we may have received next state (e.g received accept in sending_start) - // We only put next state if the state was what is was before we started - if (tx.state == stateBeforeCall) { - tx.state = nextState - } - } - } - - override fun onFailure(failure: Throwable) { - Timber.e("## SAS verification [${tx.transactionId}] failed to send toDevice in state : ${tx.state}") - tx.cancel(onErrorReason) - } - } - } - .executeBy(taskExecutor) - } - - override fun done(transactionId: String, onDone: (() -> Unit)?) { - val otherUserId = tx?.otherUserId ?: return - val otherUserDeviceId = tx?.otherDeviceId ?: return - val cancelMessage = KeyVerificationDone(transactionId) - val contentMap = MXUsersDevicesMap() - contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - onDone?.invoke() - Timber.v("## SAS verification [$transactionId] done") - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to done.") - } - } - } - .executeBy(taskExecutor) - } - - override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - val cancelMessage = KeyVerificationCancel.create(transactionId, code) - val contentMap = MXUsersDevicesMap() - contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") - } - } - } - .executeBy(taskExecutor) - } - - override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - val cancelMessage = KeyVerificationCancel.create(transactionId, code) - val contentMap = MXUsersDevicesMap() - val messages = otherUserDeviceIds.associateWith { cancelMessage } - contentMap.setObjects(otherUserId, messages) - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") - } - } - } - .executeBy(taskExecutor) - } - - override fun createAccept( - tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List - ): VerificationInfoAccept = KeyVerificationAccept.create( - tid, - keyAgreementProtocol, - hash, - commitment, - messageAuthenticationCode, - shortAuthenticationStrings - ) - - override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) - - override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) - - override fun createStartForSas( - fromDevice: String, - transactionId: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List - ): VerificationInfoStart { - return KeyVerificationStart( - fromDevice, - VERIFICATION_METHOD_SAS, - transactionId, - keyAgreementProtocols, - hashes, - messageAuthenticationCodes, - shortAuthenticationStrings, - null - ) - } - - override fun createStartForQrCode( - fromDevice: String, - transactionId: String, - sharedSecret: String - ): VerificationInfoStart { - return KeyVerificationStart( - fromDevice, - VERIFICATION_METHOD_RECIPROCATE, - transactionId, - null, - null, - null, - null, - sharedSecret - ) - } - - override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { - return KeyVerificationReady( - transactionId = tid, - fromDevice = fromDevice, - methods = methods - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt deleted file mode 100644 index 312d91182..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDeviceFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification - -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.time.Clock -import javax.inject.Inject - -internal class VerificationTransportToDeviceFactory @Inject constructor( - private val sendToDeviceTask: SendToDeviceTask, - @DeviceId val myDeviceId: String?, - private val taskExecutor: TaskExecutor, - private val clock: Clock, -) { - - fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { - return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, taskExecutor, clock) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt new file mode 100644 index 000000000..35d81dec7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationsProvider.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification + +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeVerification +import javax.inject.Inject +import javax.inject.Provider +import org.matrix.rustcomponents.sdk.crypto.OlmMachine as InnerOlmMachine + +internal class VerificationsProvider @Inject constructor( + private val olmMachine: Provider, + private val verificationRequestFactory: VerificationRequest.Factory, + private val sasVerificationFactory: SasVerification.Factory, + private val qrVerificationFactory: QrCodeVerification.Factory) { + + private val innerMachine: InnerOlmMachine + get() = olmMachine.get().inner() + + fun getVerificationRequests(userId: String): List { + return innerMachine.getVerificationRequests(userId).map(verificationRequestFactory::create) + } + + /** Get a verification request for the given user with the given flow ID. */ + fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { + return innerMachine.getVerificationRequest(userId, flowId)?.let { innerVerificationRequest -> + verificationRequestFactory.create(innerVerificationRequest) + } + } + + /** Get an active verification for the given user and given flow ID. + * + * @return Either a [SasVerification] verification or a [QrCodeVerification] + * verification. + */ + fun getVerification(userId: String, flowId: String): VerificationTransaction? { + val verification = innerMachine.getVerification(userId, flowId) + return if (verification?.asSas() != null) { + sasVerificationFactory.create(verification.asSas()!!) + } else if (verification?.asQr() != null) { + qrVerificationFactory.create(verification.asQr()!!) + } else { + null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt deleted file mode 100644 index 5b1a4752f..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService -import org.matrix.android.sdk.api.session.crypto.verification.CancelCode -import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager -import org.matrix.android.sdk.internal.crypto.SecretShareManager -import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction -import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction -import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart -import timber.log.Timber - -internal class DefaultQrCodeVerificationTransaction( - setDeviceVerificationAction: SetDeviceVerificationAction, - override val transactionId: String, - override val otherUserId: String, - override var otherDeviceId: String?, - private val crossSigningService: CrossSigningService, - outgoingKeyRequestManager: OutgoingKeyRequestManager, - secretShareManager: SecretShareManager, - private val cryptoStore: IMXCryptoStore, - // Not null only if other user is able to scan QR code - private val qrCodeData: QrCodeData?, - val userId: String, - val deviceId: String, - override val isIncoming: Boolean -) : DefaultVerificationTransaction( - setDeviceVerificationAction, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - userId, - transactionId, - otherUserId, - otherDeviceId, - isIncoming -), - QrCodeVerificationTransaction { - - override val qrCodeText: String? - get() = qrCodeData?.toEncodedString() - - override var state: VerificationTxState = VerificationTxState.None - set(newState) { - field = newState - - listeners.forEach { - try { - it.transactionUpdated(this) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - - override fun userHasScannedOtherQrCode(otherQrCodeText: String) { - val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run { - Timber.d("## Verification QR: Invalid QR Code Data") - cancel(CancelCode.QrCodeInvalid) - return - } - - // Perform some checks - if (otherQrCodeData.transactionId != transactionId) { - Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId") - cancel(CancelCode.UnknownTransaction) - return - } - - // check master key - val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey - var canTrustOtherUserMasterKey = false - - // Check the other device view of my MSK - when (otherQrCodeData) { - is QrCodeData.VerifyingAnotherUser -> { - // key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is. - // Let's check that it's correct - // If not -> Cancel - if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) { - Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") - cancel(CancelCode.MismatchedKeys) - return - } else Unit - } - is QrCodeData.SelfVerifyingMasterKeyTrusted -> { - // key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. - // Let's check that I see the same MSK - // If not -> Cancel - if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { - Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancel(CancelCode.MismatchedKeys) - return - } else { - // I can trust the MSK then (i see the same one, and other session tell me it's trusted by him) - canTrustOtherUserMasterKey = true - } - } - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { - // key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK. - // Let's check that it's the good one - // If not -> Cancel - if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) { - Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancel(CancelCode.MismatchedKeys) - return - } else { - // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK - } - } - } - - val toVerifyDeviceIds = mutableListOf() - - // Let's now check the other user/device key material - when (otherQrCodeData) { - is QrCodeData.VerifyingAnotherUser -> { - // key1(aka userMasterCrossSigningPublicKey) is the MSK of the one displaying the QR code (i.e other user) - // Let's check that it matches what I think it should be - if (otherQrCodeData.userMasterCrossSigningPublicKey - != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { - Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") - cancel(CancelCode.MismatchedKeys) - return - } else { - // It does so i should mark it as trusted - canTrustOtherUserMasterKey = true - Unit - } - } - is QrCodeData.SelfVerifyingMasterKeyTrusted -> { - // key2 (aka otherDeviceKey) is my current device key in POV of the one displaying the QR code (i.e other device) - // Let's check that it's correct - if (otherQrCodeData.otherDeviceKey - != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { - Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}") - cancel(CancelCode.MismatchedKeys) - return - } else Unit // Nothing special here, we will send a reciprocate start event, and then the other session will trust my device - // and thus allow me to request SSSS secret - } - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { - // key1 (aka otherDeviceKey) is the device key of the one displaying the QR code (i.e other device) - // Let's check that it matches what I have locally - if (otherQrCodeData.deviceKey - != cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) { - Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}") - cancel(CancelCode.MismatchedKeys) - return - } else { - // Yes it does -> i should trust it and sign then upload the signature - toVerifyDeviceIds.add(otherDeviceId ?: "") - Unit - } - } - } - - if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { - // Nothing to verify - cancel(CancelCode.MismatchedKeys) - return - } - - // All checks are correct - // Send the shared secret so that sender can trust me - // qrCodeData.sharedSecret will be used to send the start request - start(otherQrCodeData.sharedSecret) - - trust( - canTrustOtherUserMasterKey = canTrustOtherUserMasterKey, - toVerifyDeviceIds = toVerifyDeviceIds.distinct(), - eventuallyMarkMyMasterKeyAsTrusted = true, - autoDone = false - ) - } - - private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) { - if (state != VerificationTxState.None) { - Timber.e("## Verification QR: start verification from invalid state") - // should I cancel?? - throw IllegalStateException("Interactive Key verification already started") - } - - state = VerificationTxState.Started - val startMessage = transport.createStartForQrCode( - deviceId, - transactionId, - remoteSecret - ) - - transport.sendToOther( - EventType.KEY_VERIFICATION_START, - startMessage, - VerificationTxState.WaitingOtherReciprocateConfirm, - CancelCode.User, - onDone - ) - } - - override fun cancel() { - cancel(CancelCode.User) - } - - override fun cancel(code: CancelCode) { - state = VerificationTxState.Cancelled(code, true) - transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) - } - - override fun isToDeviceTransport() = false - - // Other user has scanned our QR code. check that the secret matched, so we can trust him - fun onStartReceived(startReq: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) { - if (qrCodeData == null) { - // Should not happen - cancel(CancelCode.UnexpectedMessage) - return - } - - if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) { - // Ok, we can trust the other user - // We can only trust the master key in this case - // But first, ask the user for a confirmation - state = VerificationTxState.QrScannedByOther - } else { - // Display a warning - cancel(CancelCode.MismatchedKeys) - } - } - - fun onDoneReceived() { - if (state != VerificationTxState.WaitingOtherReciprocateConfirm) { - cancel(CancelCode.UnexpectedMessage) - return - } - state = VerificationTxState.Verified - transport.done(transactionId) {} - } - - override fun otherUserScannedMyQrCode() { - when (qrCodeData) { - is QrCodeData.VerifyingAnotherUser -> { - // Alice telling Bob that the code was scanned successfully is sufficient for Bob to trust Alice's key, - trust(true, emptyList(), false) - } - is QrCodeData.SelfVerifyingMasterKeyTrusted -> { - // I now know that I have the correct device key for other session, - // and can sign it with the self-signing key and upload the signature - trust(false, listOf(otherDeviceId ?: ""), false) - } - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { - // I now know that i can trust my MSK - trust(true, emptyList(), true) - } - null -> Unit - } - } - - override fun otherUserDidNotScannedMyQrCode() { - // What can I do then? - // At least remove the transaction... - cancel(CancelCode.MismatchedKeys) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt deleted file mode 100644 index a0202485d..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.extensions.toUnsignedInt - -// MATRIX -private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1) - -internal fun QrCodeData.toEncodedString(): String { - var result = ByteArray(0) - - // MATRIX - for (i in prefix.indices) { - result += prefix[i] - } - - // Version - result += 2 - - // Mode - result += when (this) { - is QrCodeData.VerifyingAnotherUser -> 0 - is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1 - is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2 - }.toByte() - - // TransactionId length - val length = transactionId.length - result += ((length and 0xFF00) shr 8).toByte() - result += length.toByte() - - // TransactionId - transactionId.forEach { - result += it.code.toByte() - } - - // Keys - firstKey.fromBase64().forEach { - result += it - } - secondKey.fromBase64().forEach { - result += it - } - - // Secret - sharedSecret.fromBase64().forEach { - result += it - } - - return result.toString(Charsets.ISO_8859_1) -} - -internal fun String.toQrCodeData(): QrCodeData? { - val byteArray = toByteArray(Charsets.ISO_8859_1) - - // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength - - // Check header - // MATRIX - if (byteArray.size < 10) return null - - for (i in prefix.indices) { - if (byteArray[i] != prefix[i]) { - return null - } - } - - var cursor = prefix.size // 6 - - // Version - if (byteArray[cursor] != 2.toByte()) { - return null - } - cursor++ - - // Get mode - val mode = byteArray[cursor].toInt() - cursor++ - - // Get transaction length, Big Endian format - val msb = byteArray[cursor].toUnsignedInt() - val lsb = byteArray[cursor + 1].toUnsignedInt() - - val transactionLength = msb.shl(8) + lsb - - cursor++ - cursor++ - - val secretLength = byteArray.size - 74 - transactionLength - - // ensure the secret length is 8 bytes min - if (secretLength < 8) { - return null - } - - val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1) - cursor += transactionLength - val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() - cursor += 32 - val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding() - cursor += 32 - val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding() - - return when (mode) { - 0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret) - 1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret) - 2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret) - else -> null - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt deleted file mode 100644 index f308807e0..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.verification.qrcode - -/** - * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format - */ -internal sealed class QrCodeData( - /** - * the event ID or transaction_id of the associated verification. - */ - open val transactionId: String, - /** - * First key (32 bytes, in base64 no padding). - */ - val firstKey: String, - /** - * Second key (32 bytes, in base64 no padding). - */ - val secondKey: String, - /** - * a random shared secret (in base64 no padding). - */ - open val sharedSecret: String -) { - /** - * Verifying another user with cross-signing - * QR code verification mode: 0x00. - */ - data class VerifyingAnotherUser( - override val transactionId: String, - /** - * the user's own master cross-signing public key. - */ - val userMasterCrossSigningPublicKey: String, - /** - * what the device thinks the other user's master cross-signing key is. - */ - val otherUserMasterCrossSigningPublicKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - userMasterCrossSigningPublicKey, - otherUserMasterCrossSigningPublicKey, - sharedSecret - ) - - /** - * self-verifying in which the current device does trust the master key - * QR code verification mode: 0x01. - */ - data class SelfVerifyingMasterKeyTrusted( - override val transactionId: String, - /** - * the user's own master cross-signing public key. - */ - val userMasterCrossSigningPublicKey: String, - /** - * what the device thinks the other device's device key is. - */ - val otherDeviceKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - userMasterCrossSigningPublicKey, - otherDeviceKey, - sharedSecret - ) - - /** - * self-verifying in which the current device does not yet trust the master key - * QR code verification mode: 0x02. - */ - data class SelfVerifyingMasterKeyNotTrusted( - override val transactionId: String, - /** - * the current device's device key. - */ - val deviceKey: String, - /** - * what the device thinks the user's master cross-signing key is. - */ - val userMasterCrossSigningPublicKey: String, - override val sharedSecret: String - ) : QrCodeData( - transactionId, - deviceKey, - userMasterCrossSigningPublicKey, - sharedSecret - ) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt new file mode 100644 index 000000000..03df26610 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeVerification.kt @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.verification.qrcode + +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.network.RequestSender +import org.matrix.android.sdk.internal.crypto.verification.VerificationListenersHolder +import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException +import org.matrix.rustcomponents.sdk.crypto.QrCode +import org.matrix.rustcomponents.sdk.crypto.QrCodeState +import timber.log.Timber + +/** Class representing a QR code based verification flow. */ +internal class QrCodeVerification @AssistedInject constructor( + @Assisted private var inner: QrCode, + private val olmMachine: OlmMachine, + private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val verificationListenersHolder: VerificationListenersHolder, +) : QrCodeVerificationTransaction { + + @AssistedFactory + interface Factory { + fun create(inner: QrCode): QrCodeVerification + } + + override val method: VerificationMethod + get() = VerificationMethod.QR_CODE_SCAN + + private val innerMachine = olmMachine.inner() + + private fun dispatchTxUpdated() { + refreshData() + verificationListenersHolder.dispatchTxUpdated(this) + } + + /** Generate, if possible, data that should be encoded as a QR code for QR code verification. + * + * QR code verification can't verify devices between two users, so in the case that + * we're verifying another user and we don't have or trust our cross signing identity + * no QR code will be generated. + * + * @return A ISO_8859_1 encoded string containing data that should be encoded as a QR code. + * The string contains data as specified in the [QR code format] part of the Matrix spec. + * The list of bytes as defined in the spec are then encoded using ISO_8859_1 to get a string. + * + * [QR code format]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format + */ + override val qrCodeText: String? + get() { + val data = inner.generateQrCode() + + // TODO Why are we encoding this to ISO_8859_1? If we're going to encode, why not base64? + return data?.fromBase64()?.toString(Charsets.ISO_8859_1) + } + + /** Pass the data from a scanned QR code into the QR code verification object */ +// override suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) { +// request.scanQrCode(otherQrCodeText) +// dispatchTxUpdated() +// } + + /** Confirm that the other side has indeed scanned the QR code we presented. */ + override suspend fun otherUserScannedMyQrCode() { + confirm() + } + + /** Cancel the QR code verification, denying that the other side has scanned the QR code. */ + override suspend fun otherUserDidNotScannedMyQrCode() { + // TODO Is this code correct here? The old code seems to do this + cancelHelper(CancelCode.MismatchedKeys) + } + + override fun state(): QRCodeVerificationState { + Timber.v("SAS QR state${inner.state()}") + return when (inner.state()) { + // / The QR verification has been started. + QrCodeState.Started -> QRCodeVerificationState.Reciprocated + // / The QR verification has been scanned by the other side. + QrCodeState.Scanned -> QRCodeVerificationState.WaitingForScanConfirmation + // / The scanning of the QR code has been confirmed by us. + QrCodeState.Confirmed -> QRCodeVerificationState.WaitingForOtherDone + // / We have successfully scanned the QR code and are able to send a + // / reciprocation event. + QrCodeState.Reciprocated -> QRCodeVerificationState.WaitingForOtherDone + // / The verification process has been successfully concluded. + QrCodeState.Done -> QRCodeVerificationState.Done + is QrCodeState.Cancelled -> QRCodeVerificationState.Cancelled + } + } + + /** Get the unique id of this verification. */ + override val transactionId: String + get() = inner.flowId() + + /** Get the user id of the other user participating in this verification flow. */ + override val otherUserId: String + get() = inner.otherUserId() + + /** Get the device id of the other user's device participating in this verification flow. */ + override var otherDeviceId: String? + get() = inner.otherDeviceId() + @Suppress("UNUSED_PARAMETER") + set(value) { + } + + /** Did the other side initiate this verification flow. */ + override val isIncoming: Boolean + get() = !inner.weStarted() + + /** Cancel the verification flow. + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to m.user. + * + * Cancelling the verification request will also cancel the parent VerificationRequest. + * + * The method turns into a noop, if the verification flow has already been cancelled. + * */ + override suspend fun cancel() { + cancelHelper(CancelCode.User) + } + + /** Cancel the verification flow. + * + * This will send out a m.key.verification.cancel event with the cancel + * code set to the given CancelCode. + * + * Cancelling the verification request will also cancel the parent VerificationRequest. + * + * The method turns into a noop, if the verification flow has already been cancelled. + * + * @param code The cancel code that should be given as the reason for the cancellation. + * */ + override suspend fun cancel(code: CancelCode) { + cancelHelper(code) + } + + /** Is this verification happening over to-device messages. */ + override fun isToDeviceTransport(): Boolean { + return inner.roomId() == null + } + + /** Confirm the QR code verification. + * + * This confirms that the other side has scanned our QR code and sends + * out a m.key.verification.done event to the other side. + * + * The method turns into a noop if we're not yet ready to confirm the scanning, + * i.e. we didn't yet receive a m.key.verification.start event from the other side. + */ + @Throws(CryptoStoreException::class) + private suspend fun confirm() { + val result = withContext(coroutineDispatchers.io) { + inner.confirm() + } ?: return + dispatchTxUpdated() + try { + for (verificationRequest in result.requests) { + sender.sendVerificationRequest(verificationRequest) + } + val signatureRequest = result.signatureRequest + if (signatureRequest != null) { + sender.sendSignatureUpload(signatureRequest) + } + } catch (failure: Throwable) { + cancelHelper(CancelCode.UserError) + } + } + + private suspend fun cancelHelper(code: CancelCode) = withContext(NonCancellable) { + val request = inner.cancel(code.value) ?: return@withContext + dispatchTxUpdated() + tryOrNull("Fail to send cancel verification request") { + sender.sendVerificationRequest(request, retryCount = Int.MAX_VALUE) + } + } + + /** Fetch fresh data from the Rust side for our verification flow. */ + private fun refreshData() { + innerMachine.getVerification(inner.otherUserId(), inner.flowId()) + ?.asQr()?.let { + inner = it + } +// when (val verification = innerMachine.getVerification(request.otherUser(), request.flowId())) { +// is Verification.QrCodeV1 -> { +// inner = verification.qrcode +// } +// else -> { +// } +// } + + return + } + + override fun toString(): String { + return "QrCodeVerification(" + + "qrCodeText=$qrCodeText, " + + "state=${state()}, " + + "transactionId='$transactionId', " + + "otherUserId='$otherUserId', " + + "otherDeviceId=$otherDeviceId, " + + "isIncoming=$isIncoming)" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index c5ececcdd..4a7064ebf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -68,6 +68,9 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo054 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -76,7 +79,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 51L, + schemaVersion = 54L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -137,5 +140,8 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 49) MigrateSessionTo049(realm).perform() if (oldVersion < 50) MigrateSessionTo050(realm).perform() if (oldVersion < 51) MigrateSessionTo051(realm).perform() + if (oldVersion < 52) MigrateSessionTo052(realm).perform() + if (oldVersion < 53) MigrateSessionTo053(realm).perform() + if (oldVersion < 54) MigrateSessionTo054(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 43f84e771..b48e71464 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.createObject import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.ChunkEntity @@ -76,7 +77,7 @@ internal fun ChunkEntity.addTimelineEvent( val senderId = eventEntity.sender ?: "" // Update RR for the sender of a new message with a dummy one - val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null + val readReceiptsSummaryEntity = handleReadReceiptsOfSender(realm, roomId, eventEntity, senderId) val timelineEventEntity = realm.createObject().apply { this.localId = localId this.root = eventEntity @@ -124,7 +125,7 @@ internal fun computeIsUnique( } } -private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { +private fun handleReadReceiptsOfSender(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() ?: realm.createObject(eventEntity.eventId).apply { this.roomId = roomId @@ -132,7 +133,12 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE val originServerTs = eventEntity.originServerTs if (originServerTs != null) { val timestampOfEvent = originServerTs.toDouble() - val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId) + val readReceiptOfSender = ReadReceiptEntity.getOrCreate( + realm = realm, + roomId = roomId, + userId = senderId, + threadId = eventEntity.rootThreadEventId ?: ReadService.THREAD_ID_MAIN + ) // If the synced RR is older, update if (timestampOfEvent > readReceiptOfSender.originServerTs) { val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 908c710df..211425051 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -210,7 +210,7 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe + verificationState = result.messageVerificationState ) // Save decryption result, to not decrypt every time we enter the thread list eventEntity.setDecryptionResult(result) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 0f0a847c7..2f243dd85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -54,6 +54,7 @@ internal object EventMapper { eventEntity.decryptionResultJson = event.mxDecryptionResult?.let { MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it) } + eventEntity.isVerificationStateDirty = event.verificationStateIsDirty eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorCode = event.mCryptoError?.name eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false @@ -93,6 +94,7 @@ internal object EventMapper { eventEntity.decryptionResultJson?.let { json -> try { it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) + it.verificationStateIsDirty = eventEntity.isVerificationStateDirty } catch (t: JsonDataException) { Timber.e(t, "Failed to parse decryption result") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 1c7a0591a..25af5be66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -47,8 +47,10 @@ internal object HomeServerCapabilitiesMapper { canLoginWithQrCode = entity.canLoginWithQrCode, canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, - canRedactEventWithRelations = entity.canRedactEventWithRelations, + canRedactRelatedEvents = entity.canRedactEventWithRelations, externalAccountManagementUrl = entity.externalAccountManagementUrl, + authenticationIssuer = entity.authenticationIssuer, + disableNetworkConstraint = entity.disableNetworkConstraint, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt index 3bed97073..3ca984602 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo051.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabi import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateSessionTo051(realm: DynamicRealm) : RealmMigrator(realm, 51) { - override fun doMigrate(realm: DynamicRealm) { realm.schema.get("HomeServerCapabilitiesEntity") ?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt new file mode 100644 index 000000000..42a25b940 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo052.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo052(realm: DynamicRealm) : RealmMigrator(realm, 52) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("EventEntity") + ?.addField(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, Boolean::class.java) + ?.setNullable(EventEntityFields.IS_VERIFICATION_STATE_DIRTY, true) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo053.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo053.kt new file mode 100644 index 000000000..32fac1ad4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo053.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo053(realm: DynamicRealm) : RealmMigrator(realm, 53) { + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.AUTHENTICATION_ISSUER, String::class.java) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt new file mode 100644 index 000000000..19f65153c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo054.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo054(realm: DynamicRealm) : RealmMigrator(realm, 54) { + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.DISABLE_NETWORK_CONSTRAINT, Boolean::class.java) + ?.setNullable(HomeServerCapabilitiesEntityFields.DISABLE_NETWORK_CONSTRAINT, true) + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index ee5c3d90c..0583ae5b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -47,7 +47,8 @@ internal open class EventEntity( @Index var rootThreadEventId: String? = null, // Number messages within the thread var numberOfThreads: Int = 0, - var threadSummaryLatestMessage: TimelineEventEntity? = null + var threadSummaryLatestMessage: TimelineEventEntity? = null, + var isVerificationStateDirty: Boolean? = null, ) : RealmObject() { private var sendStateStr: String = SendState.UNKNOWN.name @@ -88,12 +89,13 @@ internal open class EventEntity( senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe + verificationState = result.messageVerificationState ) val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) decryptionErrorCode = null decryptionErrorReason = null + isVerificationStateDirty = false // If we have an EventInsertEntity for the eventId we make sures it can be processed now. realm.where(EventInsertEntity::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 35a5c654d..389194841 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -36,6 +36,8 @@ internal open class HomeServerCapabilitiesEntity( var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, var canRedactEventWithRelations: Boolean = false, var externalAccountManagementUrl: String? = null, + var authenticationIssuer: String? = null, + var disableNetworkConstraint: Boolean? = null, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt index 74dbd647a..6715a6c09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/FileQualifiers.kt @@ -33,3 +33,7 @@ internal annotation class CacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class ExternalFilesDirectory + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SessionRustFilesDirectory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt index d8cdd162f..fe021e76d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt @@ -30,7 +30,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.worker.MatrixWorkerFactory +import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -102,12 +104,20 @@ internal class WorkManagerProvider @Inject constructor( companion object { private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" - /** - * Default constraints: connected network. - */ - val workConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() + fun getWorkConstraints( + workManagerConfig: WorkManagerConfig, + ): Constraints { + val withNetworkConstraint = workManagerConfig.withNetworkConstraint() + return Constraints.Builder() + .apply { + if (withNetworkConstraint) { + setRequiredNetworkType(NetworkType.CONNECTED) + } else { + Timber.w("Network constraint is disabled") + } + } + .build() + } // Use min value, smaller value will be ignored const val BACKOFF_DELAY_MILLIS = WorkRequest.MIN_BACKOFF_MILLIS diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt deleted file mode 100644 index 7d52d9b2b..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.legacy - -import android.content.Context -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.runBlocking -import org.matrix.android.sdk.api.auth.LoginType -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.auth.data.DiscoveryInformation -import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig -import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig -import org.matrix.android.sdk.api.legacy.LegacySessionImporter -import org.matrix.android.sdk.api.network.ssl.Fingerprint -import org.matrix.android.sdk.api.util.md5 -import org.matrix.android.sdk.internal.auth.SessionParamsStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule -import org.matrix.android.sdk.internal.database.RealmKeysUtils -import org.matrix.android.sdk.internal.legacy.riot.LoginStorage -import timber.log.Timber -import java.io.File -import javax.inject.Inject -import org.matrix.android.sdk.internal.legacy.riot.Fingerprint as LegacyFingerprint -import org.matrix.android.sdk.internal.legacy.riot.HomeServerConnectionConfig as LegacyHomeServerConnectionConfig - -internal class DefaultLegacySessionImporter @Inject constructor( - private val context: Context, - private val sessionParamsStore: SessionParamsStore, - private val realmKeysUtils: RealmKeysUtils, - private val realmCryptoStoreMigration: RealmCryptoStoreMigration -) : 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(): Boolean { - Timber.d("Migration: Importing legacy session") - - val list = loginStorage.credentialsList - - Timber.d("Migration: found ${list.size} session(s).") - - val legacyConfig = list.firstOrNull() ?: return false - - 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") - } - } - - // A session has been imported - return true - } - - 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, - loginType = LoginType.UNKNOWN, - ) - - 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 Matrix SDK2, 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.schemaVersion) - .migration(realmCryptoStoreMigration) - .build() - - Timber.d("Migration: copy DB to encrypted DB") - Realm.getInstance(realmConfiguration).use { - // Move the DB to the new location, handled by Matrix SDK2 - it.writeEncryptedCopyTo(File(newLocation, realmConfiguration.realmFileName), realmKeysUtils.getRealmEncryptionKey(keyAlias)) - } - } - - // Delete all the files created by Riot Android which will not be used anymore by Element - 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/org/matrix/android/sdk/internal/legacy/riot/Credentials.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java deleted file mode 100644 index bbed159e3..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.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 homeserver 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/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java deleted file mode 100644 index 82541d38f..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.legacy.riot; - -import android.util.Base64; - -import org.json.JSONException; -import org.json.JSONObject; - -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; - - public Fingerprint(HashType hashType, byte[] bytes) { - mHashType = hashType; - mBytes = bytes; - } - - public HashType getType() { - return mHashType; - } - - public byte[] getBytes() { - return mBytes; - } - - 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); - } - - @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/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java deleted file mode 100644 index b2bb852cd..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.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 homeserver 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 homeserver URI. - * - * @param uri the new HS uri - */ - public void setHomeserverUri(Uri uri) { - mHomeServerUri = uri; - } - - /** - * @return the homeserver 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 homeserver 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 homeserver. - */ - @Nullable - public List getAcceptedTlsVersions() { - return mTlsVersions; - } - - /** - * TLS cipher suites accepted for TLS connections with the homeserver. - */ - @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 homeserver 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 homeserver 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 homeserver. - * - * @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 homeserver. - * - * @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("Homeserver URI not set"); - } - - return mHomeServerConnectionConfig; - } - - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java deleted file mode 100755 index 924bd461e..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.legacy.riot; - -import android.annotation.SuppressLint; -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 + homeserver 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 homeserver 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 homeserver 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 - */ - @SuppressLint("ApplySharedPref") - 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(); - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt deleted file mode 100644 index a754a0da9..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.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/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt deleted file mode 100644 index 2a4ae295f..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.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/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt deleted file mode 100644 index beb95a1d6..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.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/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index fefb7fb5e..0f6cdba92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -40,6 +40,8 @@ import java.io.IOException * @param maxRetriesCount the max number of retries * @param requestBlock a suspend lambda to perform the network request */ + +const val DEFAULT_REQUEST_RETRY_COUNT = 3 internal suspend inline fun executeRequest( globalErrorReceiver: GlobalErrorReceiver?, canRetry: Boolean = false, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 6c28b9fcc..7eeae57ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -23,6 +23,8 @@ import com.squareup.moshi.Moshi import java.io.IOException import java.lang.reflect.Type import java.math.BigDecimal +import kotlin.math.ceil +import kotlin.math.floor /** * This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row. @@ -53,7 +55,16 @@ internal interface CheckNumberType { } override fun toJson(writer: JsonWriter, value: Any?) { - delegate.toJson(writer, value) + if (value is Number) { + val double = value.toDouble() + if (ceil(double) == floor(double)) { + writer.value(value.toLong()) + } else { + writer.value(value.toDouble()) + } + } else { + delegate.toJson(writer, value) + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 1af904bbc..992ea650c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -66,7 +66,6 @@ import org.matrix.android.sdk.api.session.widgets.WidgetService import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH import org.matrix.android.sdk.internal.auth.SessionParamsStore -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.di.ContentScannerDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase @@ -103,7 +102,7 @@ internal class DefaultSession @Inject constructor( private val pushersService: Lazy, private val termsService: Lazy, private val searchService: Lazy, - private val cryptoService: Lazy, + private val cryptoService: Lazy, private val defaultFileService: Lazy, private val permalinkService: Lazy, private val profileService: Lazy, @@ -145,7 +144,7 @@ internal class DefaultSession @Inject constructor( override fun open() { sessionState.setIsOpen(true) globalErrorHandler.listener = this - cryptoService.get().ensureDevice() + cryptoService.get().start() uiHandler.post { lifecycleObservers.forEach { it.onSessionStarted(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt index 609acdd89..029e803d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt @@ -19,16 +19,11 @@ package org.matrix.android.sdk.internal.session import org.matrix.android.sdk.api.session.ToDeviceService import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import javax.inject.Inject internal class DefaultToDeviceService @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter, - private val cryptoStore: IMXCryptoStore ) : ToDeviceService { override suspend fun sendToDevice(eventType: String, targets: Map>, content: Content, txnId: String?) { @@ -42,17 +37,18 @@ internal class DefaultToDeviceService @Inject constructor( } override suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap, txnId: String?) { - sendToDeviceTask.executeRetry( + sendToDeviceTask.execute( SendToDeviceTask.Params( eventType = eventType, contentMap = contentMap, transactionId = txnId - ), - 3 + ) ) } override suspend fun sendEncryptedToDevice(eventType: String, targets: Map>, content: Content, txnId: String?) { + // TODO add to rust-ffi + /* val payloadJson = mapOf( "type" to eventType, "content" to content @@ -63,11 +59,13 @@ internal class DefaultToDeviceService @Inject constructor( targets.forEach { (userId, deviceIdList) -> deviceIdList.forEach { deviceId -> cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo -> - sendToDeviceMap.setObject(userId, deviceId, messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))) + sendToDeviceMap.setObject(userId, deviceId, encryptEventContent(payloadJson, listOf(deviceInfo))) } } } sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId) + + */ } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt new file mode 100644 index 000000000..b4944edbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/MigrateEAtoEROperation.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session + +import io.realm.DynamicRealm +import io.realm.Realm +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.crypto.store.db.migration.rust.ExtractMigrationDataUseCase +import org.matrix.android.sdk.internal.crypto.store.db.migration.rust.RealmToMigrate +import org.matrix.rustcomponents.sdk.crypto.ProgressListener +import timber.log.Timber +import java.io.File + +class MigrateEAtoEROperation(private val migrateGroupSessions: Boolean = false) { + + fun execute(cryptoRealm: RealmConfiguration, rustFilesDir: File, passphrase: String?): File { + // Temporary code for migration + if (!rustFilesDir.exists()) { + rustFilesDir.mkdir() + // perform a migration? + val extractMigrationData = ExtractMigrationDataUseCase(migrateGroupSessions) + val hasExitingData = extractMigrationData.hasExistingData(cryptoRealm) + if (!hasExitingData) return rustFilesDir + + try { + val progressListener = object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + Timber.v("OnProgress: $progress/$total") + } + } + Realm.getInstance(cryptoRealm).use { realm -> + extractMigrationData.extractData(RealmToMigrate.ClassicRealm(realm)) { + org.matrix.rustcomponents.sdk.crypto.migrate(it, rustFilesDir.path, passphrase, progressListener) + } + } + } catch (failure: Throwable) { + Timber.e(failure, "Failure while calling rust migration method") + throw failure + } + } + return rustFilesDir + } + + fun dynamicExecute(dynamicRealm: DynamicRealm, rustFilesDir: File, passphrase: String?) { + if (!rustFilesDir.exists()) { + rustFilesDir.mkdir() + } + val extractMigrationData = ExtractMigrationDataUseCase(migrateGroupSessions) + + try { + val progressListener = object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + Timber.v("OnProgress: $progress/$total") + } + } + extractMigrationData.extractData(RealmToMigrate.DynamicRealm(dynamicRealm)) { + org.matrix.rustcomponents.sdk.crypto.migrate(it, rustFilesDir.path, passphrase, progressListener) + } + } catch (failure: Throwable) { + Timber.e(failure, "Failure while calling rust migration method") + throw failure + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index a7572035d..0b5013140 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -1,11 +1,11 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.securestorage.SecureStorageModule import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.CryptoModule +import org.matrix.android.sdk.internal.crypto.OlmMachine import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.federation.FederationModule @@ -113,6 +114,8 @@ internal interface SessionComponent { fun networkConnectivityChecker(): NetworkConnectivityChecker + fun olmMachine(): OlmMachine + fun taskExecutor(): TaskExecutor fun inject(worker: SendEventWorker) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index b9f56cbc9..54834f426 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.SessionRustFilesDirectory import org.matrix.android.sdk.internal.di.Unauthenticated import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress @@ -96,6 +97,8 @@ import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEvent import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter +import org.matrix.android.sdk.internal.session.workmanager.DefaultWorkManagerConfig +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import retrofit2.Retrofit import java.io.File import javax.inject.Provider @@ -140,7 +143,7 @@ internal abstract class SessionModule { @JvmStatic @DeviceId @Provides - fun providesDeviceId(credentials: Credentials): String? { + fun providesDeviceId(credentials: Credentials): String { return credentials.deviceId } @@ -178,6 +181,16 @@ internal abstract class SessionModule { return File(context.filesDir, sessionId) } + @JvmStatic + @Provides + @SessionRustFilesDirectory + @SessionScope + fun providesRustCryptoFilesDir( + @SessionFilesDirectory parent: File, + ): File { + return File(parent, "rustFlavor") + } + @JvmStatic @Provides @SessionDownloadsDirectory @@ -279,8 +292,14 @@ internal abstract class SessionModule { sessionParams: SessionParams, retrofitFactory: RetrofitFactory ): Retrofit { + var uri = sessionParams.homeServerConnectionConfig.homeServerUriBase.toString() + if (uri == "http://localhost:8080") { + uri = "http://10.0.2.2:8080" + } else if (uri == "http://localhost:8081") { + uri = "http://10.0.2.2:8081" + } return retrofitFactory - .create(okHttpClient, sessionParams.homeServerConnectionConfig.homeServerUriBase.toString()) + .create(okHttpClient, uri) } @JvmStatic @@ -405,4 +424,7 @@ internal abstract class SessionModule { @Binds abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor + + @Binds + abstract fun bindWorkManaerConfig(config: DefaultWorkManagerConfig): WorkManagerConfig } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index 5b4100f27..ebf91112a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -32,7 +32,7 @@ import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class MxCallFactory @Inject constructor( - @DeviceId private val deviceId: String?, + @DeviceId private val deviceId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, private val matrixConfiguration: MatrixConfiguration, @@ -48,7 +48,7 @@ internal class MxCallFactory @Inject constructor( isOutgoing = false, roomId = roomId, userId = userId, - ourPartyId = deviceId ?: "", + ourPartyId = deviceId, isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, @@ -66,7 +66,7 @@ internal class MxCallFactory @Inject constructor( isOutgoing = true, roomId = roomId, userId = userId, - ourPartyId = deviceId ?: "", + ourPartyId = deviceId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index 95ff44807..7c60eab08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -71,7 +71,14 @@ internal data class Capabilities( * True if the user can use m.thread relation, false otherwise. */ @Json(name = "m.thread") - val threads: BooleanCapability? = null + val threads: BooleanCapability? = null, + + /** + * Capability to indicate if the server supports login token issuance for signing in another device. + * True if the user can use /login/get_token, false otherwise. + */ + @Json(name = "m.get_login_token") + val getLoginToken: BooleanCapability? = null ) @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index ec12695ec..f007f2236 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin -import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactEventWithRelations +import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads @@ -151,12 +151,10 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getVersionResult.doesServerSupportThreads() homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = getVersionResult.doesServerSupportThreadUnreadNotifications() - homeServerCapabilitiesEntity.canLoginWithQrCode = - getVersionResult.doesServerSupportQrCodeLogin() homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() homeServerCapabilitiesEntity.canRedactEventWithRelations = - getVersionResult.doesServerSupportRedactEventWithRelations() + getVersionResult.doesServerSupportRedactionOfRelatedEvents() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { @@ -167,12 +165,29 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( Timber.v("Extracted integration config : $config") realm.insertOrUpdate(config) } + homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl + homeServerCapabilitiesEntity.disableNetworkConstraint = getWellknownResult.wellKnown.disableNetworkConstraint } + + homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult) + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } } + private fun canLoginWithQrCode(getCapabilitiesResult: GetCapabilitiesResult?, getVersionResult: Versions?): Boolean { + // in r0 of MSC3882 an unstable feature was exposed. In stable it is done via /capabilities and /login + + // in stable 1.7 a capability is exposed for the authenticated user + if (getCapabilitiesResult?.capabilities?.getLoginToken != null) { + return getCapabilitiesResult.capabilities.getLoginToken.enabled == true + } + + @Suppress("DEPRECATION") + return getVersionResult?.doesServerSupportQrCodeLogin() == true + } + companion object { // 8 hours like on Element Web private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt index 196a8c122..270676ff6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.permalinks +import androidx.core.net.toUri import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.permalinks.PermalinkService import javax.inject.Inject @@ -47,4 +48,9 @@ internal class DefaultPermalinkService @Inject constructor( override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String { return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo) } + + override fun isPermalinkSupported(supportedHosts: Array, url: String): Boolean { + return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE) || + supportedHosts.any { url.toUri().host == it } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index e89cfa508..690a6dd71 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -44,7 +45,8 @@ internal class DefaultPushersService @Inject constructor( private val addPusherTask: AddPusherTask, private val togglePusherTask: TogglePusherTask, private val removePusherTask: RemovePusherTask, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val workManagerConfig: WorkManagerConfig, ) : PushersService { override suspend fun testPush( @@ -130,7 +132,7 @@ internal class DefaultPushersService @Inject constructor( private fun enqueueAddPusher(pusher: JsonPusher): UUID { val params = AddPusherWorker.Params(sessionId, pusher) val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 6d72b8ef2..1c8b994b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -154,6 +154,12 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } + override fun roomSummariesChangesLive( + queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder): LiveData> { + return roomSummaryDataSource.getRoomSummariesChangesLive(queryParams, sortOrder) + } + override fun getFilteredPagedRoomSummariesLive( queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index 5a66e7e62..fbf1dc532 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -26,11 +26,10 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import javax.inject.Inject -internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { +internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCommonCryptoStore) { sealed class EditValidity { object Valid : EditValidity() @@ -53,7 +52,6 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto * If the original event was encrypted, the replacement should be too. */ fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { - Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent") // we might not know the original event at that time. In this case we can't perform the validation // Edits should be revalidated when the original event is received if (originalEvent == null) { @@ -80,25 +78,21 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto val replaceDecrypted = replaceEvent.toValidDecryptedEvent() ?: return EditValidity.Unknown // UTD can't decide - val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId - val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId + if (originalEvent.senderId != replaceEvent.senderId) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + + val originalSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, originalDecrypted.cryptoSenderKey) } + val editSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, replaceDecrypted.cryptoSenderKey) } if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } - if (originalCryptoSenderId == null || editCryptoSenderId == null) { + if (originalSendingDevice == null || editSendingDevice == null) { // mm what can we do? we don't know if it's cryptographically from same user? - // let valid and UI should display send by deleted device warning? - val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId - val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId - if (bestEffortOriginal != bestEffortEdit) { - return EditValidity.Invalid("original event and replacement event must have the same sender") - } - } else { - if (originalCryptoSenderId != editCryptoSenderId) { - return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender") - } + // maybe it's a deleted device or a not yet downloaded one? + return EditValidity.Unknown } if (originalDecrypted.type != replaceDecrypted.type) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index c3d55b267..742e4b8ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -31,7 +32,12 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject -internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { +internal class RoomAvatarResolver @Inject constructor( + matrixConfiguration: MatrixConfiguration, + @UserId private val userId: String +) { + + private val roomDisplayNameFallbackProvider = matrixConfiguration.roomDisplayNameFallbackProvider /** * Compute the room avatar url. @@ -40,21 +46,26 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId * @return the room avatar url, can be a fallback to a room member avatar or null */ fun resolve(realm: Realm, roomId: String): String? { - val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") + val roomAvatarUrl = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") ?.root ?.asDomain() ?.content ?.toModel() ?.avatarUrl - if (!roomName.isNullOrEmpty()) { - return roomName + if (!roomAvatarUrl.isNullOrEmpty()) { + return roomAvatarUrl } - val roomMembers = RoomMemberHelper(realm, roomId) - val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse() if (isDirectRoom) { + val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId) + val roomMembers = RoomMemberHelper(realm, roomId) + val members = roomMembers + .queryActiveRoomMembersEvent() + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) + .findAll() + if (members.size == 1) { // Use avatar of a left user val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt index e3f4732cc..f45f2b848 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -51,7 +52,12 @@ internal class DefaultRoomGetter @Inject constructor( .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .findAll() - .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first(null) == otherUserId } + .firstOrNull { dm -> + // deferred DM could create local echo of summaries + !RoomLocalEcho.isLocalEchoId(dm.roomId) && + dm.otherMemberIds.size == 1 && + dm.otherMemberIds.first(null) == otherUserId + } ?.roomId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 653069b3c..6e6fcb718 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -21,6 +21,8 @@ import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure @@ -30,7 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -65,7 +66,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( private val roomSummaryUpdater: RoomSummaryUpdater, @SessionDatabase private val realmConfiguration: RealmConfiguration, private val createRoomBodyBuilder: CreateRoomBodyBuilder, - private val cryptoService: DefaultCryptoService, + private val cryptoService: CryptoService, private val clock: Clock, private val createLocalRoomStateEventsTask: CreateLocalRoomStateEventsTask, ) : CreateLocalRoomTask { @@ -176,7 +177,9 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( } // Give info to crypto module - cryptoService.onStateEvent(roomId, event, null) + runBlocking { + cryptoService.onStateEvent(roomId, event, null) + } } roomMemberContentsByUser.getOrPut(event.senderId) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 92081885a..5bef61cae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.create import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent @@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.MimeTypes -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.token.AccessTokenProvider @@ -44,7 +44,7 @@ import javax.inject.Inject internal class CreateRoomBodyBuilder @Inject constructor( private val ensureIdentityTokenTask: EnsureIdentityTokenTask, - private val deviceListManager: DeviceListManager, + private val cryptoService: CryptoService, private val identityStore: IdentityStore, private val fileUploader: FileUploader, @UserId @@ -193,8 +193,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( // for now remove checks on cross signing // && crossSigningService.isCrossSigningVerified() params.invitedUserIds.let { userIds -> - val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) - + val keys = cryptoService.downloadKeysIfNeeded(userIds, forceDownload = false) userIds.all { userId -> keys.map[userId].let { deviceMap -> if (deviceMap.isNullOrEmpty()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/crypto/DefaultRoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/crypto/DefaultRoomCryptoService.kt index 4f0228e6a..92cd30c7d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/crypto/DefaultRoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/crypto/DefaultRoomCryptoService.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService -import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.internal.session.room.state.SendStateTask import java.security.InvalidParameterException @@ -51,9 +50,7 @@ internal class DefaultRoomCryptoService @AssistedInject constructor( } override suspend fun prepareToEncrypt() { - awaitCallback { - cryptoService.prepareToEncrypt(roomId, it) - } + cryptoService.prepareToEncrypt(roomId) } override suspend fun enableEncryption(algorithm: String, force: Boolean) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index c02049f40..e82f55628 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -17,12 +17,13 @@ package org.matrix.android.sdk.internal.session.room.membership import com.zhuinden.monarchy.Monarchy +import dagger.Lazy import io.realm.kotlin.createObject import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity @@ -63,7 +64,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( private val roomSummaryUpdater: RoomSummaryUpdater, private val roomMemberEventHandler: RoomMemberEventHandler, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - private val deviceListManager: DeviceListManager, + private val cryptoService: Lazy, private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, ) : LoadRoomMembersTask { @@ -139,7 +140,10 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( roomSummaryUpdater.update(realm, roomId, updateMembers = true) } if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) { - deviceListManager.onRoomMembersLoadedFor(roomId) + cryptoService.get().onE2ERoomMemberLoadedFromServer(roomId) +// val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true) +// olmMachineProvider.olmMachine.updateTrackedUsers(userIds) +// deviceListManager.onRoomMembersLoadedFor(roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 7497ecf21..7a5b91a0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -92,18 +92,20 @@ internal class RoomDisplayNameResolver @Inject constructor( } ?: roomDisplayNameFallbackProvider.getNameForRoomInvite() } else if (roomEntity?.membership == Membership.JOIN) { + val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId) val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val invitedCount = roomSummary?.invitedMembersCount ?: 0 val joinedCount = roomSummary?.joinedMembersCount ?: 0 val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { - it.membership == Membership.INVITE || it.membership == Membership.JOIN + (it.membership == Membership.INVITE || it.membership == Membership.JOIN) && !excludedUserIds.contains(it.userId) } } } else { activeMembers.where() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) .limit(5) .findAll() .createSnapshot() @@ -113,6 +115,7 @@ internal class RoomDisplayNameResolver @Inject constructor( 0 -> { // Get left members if any val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) .findAll() .map { displayNameResolver.getBestName(it.toMatrixItem()) } val directUserId = roomSummary?.directUserId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt index 42b069f8f..8707c2438 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt @@ -63,7 +63,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? pattern = roomId ) val rule = PushRule( - actions = listOf(Action.DoNotNotify).toJson(), + actions = emptyList().toJson(), enabled = true, ruleId = roomId, conditions = listOf(condition) @@ -81,7 +81,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? internal fun RoomPushRule.toRoomNotificationState(): RoomNotificationState { return if (rule.enabled) { val actions = rule.getActions() - if (actions.contains(Action.DoNotNotify)) { + if (actions.isEmpty()) { if (kind == RuleSetKey.OVERRIDE) { RoomNotificationState.MUTE } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 36ec5e8da..73b7ae05f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -22,6 +22,8 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.util.Optional @@ -43,7 +45,8 @@ internal class DefaultReadService @AssistedInject constructor( private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, @UserId private val userId: String, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, ) : ReadService { @AssistedFactory @@ -66,7 +69,7 @@ internal class DefaultReadService @AssistedInject constructor( setReadMarkersTask.execute(taskParams) } - override suspend fun setReadReceipt(eventId: String, threadId: String) { + override suspend fun setReadReceipt(eventId: String, threadId: String) = withContext(matrixCoroutineDispatchers.io) { val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) { threadId } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index 8e7592a8b..5c4493100 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.read import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity @@ -64,9 +66,10 @@ internal class DefaultSetReadMarkersTask @Inject constructor( private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val coroutineDispatchers: MatrixCoroutineDispatchers, ) : SetReadMarkersTask { - override suspend fun execute(params: SetReadMarkersTask.Params) { + override suspend fun execute(params: SetReadMarkersTask.Params) = withContext(coroutineDispatchers.io) { val markers = mutableMapOf() Timber.v("Execute set read marker with params: $params") val latestSyncedEventId = latestSyncedEventId(params.roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index ddf3e41df..190dcf747 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor( override fun editReply( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, - newBodyText: String, + newBodyText: CharSequence, newFormattedBodyText: String?, compatibilityBodyText: String ): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index c83539c8f..67433033d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -106,7 +106,7 @@ internal class EventEditor @Inject constructor( fun editReply( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, - newBodyText: String, + newBodyText: CharSequence, newBodyFormattedText: String?, compatibilityBodyText: String ): Cancelable { @@ -131,6 +131,7 @@ internal class EventEditor @Inject constructor( replyToEdit, originalTimelineEvent, newBodyText, + newBodyFormattedText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index 848b9698e..f1756af3f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -17,11 +17,11 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy import io.realm.RealmList +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult import org.matrix.android.sdk.api.session.room.threads.ThreadFilter import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity @@ -55,7 +55,7 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, @SessionDatabase private val monarchy: Monarchy, - private val cryptoService: DefaultCryptoService, + private val cryptoService: CryptoService, @UserId private val userId: String, private val clock: Clock, ) : FetchThreadSummariesTask { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 1e9a785c8..b3322d3fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy import io.realm.Realm import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event @@ -25,7 +26,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.mapper.asDomain @@ -89,7 +89,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, @SessionDatabase private val monarchy: Monarchy, - private val cryptoService: DefaultCryptoService, + private val cryptoService: CryptoService, private val clock: Clock, private val realmSessionProvider: RealmSessionProvider, private val getEventTask: GetEventTask, @@ -135,7 +135,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( if (!isRootThreadTimelineEventEntityKnown) { // Fetch the root event from the server threadRootEvent = tryOrNull { - getEventTask.execute(GetEventTask.Params(roomId = params.roomId, eventId = params.rootThreadEventId)) + getEventTask.execute(GetEventTask.Params(roomId = params.roomId, eventId = params.rootThreadEventId)) } } } @@ -248,7 +248,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe + verificationState = result.messageVerificationState ) } catch (e: MXCryptoError) { if (e is MXCryptoError.Base) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index d29e7d8f3..0c15573aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -49,11 +49,12 @@ import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.TextContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -69,11 +70,12 @@ internal class DefaultSendService @AssistedInject constructor( private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, - private val cancelSendTracker: CancelSendTracker + private val cancelSendTracker: CancelSendTracker, + private val workManagerConfig: WorkManagerConfig, ) : SendService { @AssistedFactory @@ -140,11 +142,11 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun redactEvent(event: Event, reason: String?, withRelations: List?, additionalContent: Content?): Cancelable { + override fun redactEvent(event: Event, reason: String?, withRelTypes: List?, additionalContent: Content?): Cancelable { // TODO manage media/attachements? - val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelations, additionalContent) + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, withRelTypes, additionalContent) .also { createLocalEcho(it) } - return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelations) + return eventSenderProcessor.postRedaction(redactionEcho, reason, withRelTypes) } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { @@ -373,7 +375,7 @@ internal class DefaultSendService @AssistedInject constructor( val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .startChain(true) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c2bdec359..d4d20dfdd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -312,7 +312,8 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, eventReplaced: TimelineEvent, originalEvent: TimelineEvent, - newBodyText: String, + replyText: CharSequence, + replyTextFormatted: String?, autoMarkdown: Boolean, msgType: String, compatibilityText: String, @@ -321,22 +322,23 @@ internal class LocalEchoEventFactory @Inject constructor( val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" - val body = bodyForReply(timelineEvent = originalEvent) + val bodyOfRepliedEvent = bodyForReply(timelineEvent = originalEvent) // As we always supply formatted body for replies we should force the MarkdownParser to produce html. - val newBodyFormatted = markdownParser.parse(newBodyText, force = true, advanced = autoMarkdown).takeFormatted() + val newBodyFormatted = replyTextFormatted ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() // Body of the original message may not have formatted version, so may also have to convert to html. - val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted() + val formattedBodyOfRepliedEvent = + bodyOfRepliedEvent.formattedText ?: markdownParser.parse(text = bodyOfRepliedEvent.text, force = true, advanced = autoMarkdown).takeFormatted() val replyFormatted = buildFormattedReply( permalink, userLink, originalEvent.senderInfo.disambiguatedDisplayName, - bodyFormatted, + formattedBodyOfRepliedEvent, newBodyFormatted ) // // > <@alice:example.org> This is the original body // - val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText) + val replyFallback = buildReplyFallback(bodyOfRepliedEvent, originalEvent.root.senderId ?: "", replyText.toString()) return createMessageEvent( roomId, @@ -812,12 +814,12 @@ internal class LocalEchoEventFactory @Inject constructor( } } */ - fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelations: List? = null, additionalContent: Content? = null): Event { + fun createRedactEvent(roomId: String, eventId: String, reason: String?, withRelTypes: List? = null, additionalContent: Content? = null): Event { val localId = LocalEcho.createLocalEchoId() - val content = if (reason != null || withRelations != null) { + val content = if (reason != null || withRelTypes != null) { EventRedactBody( reason = reason, - withRelations = withRelations, + unstableWithRelTypes = withRelTypes, ).toContent().plus(additionalContent.orEmpty()) } else { additionalContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index 576f31c64..270d3a228 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -43,7 +43,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses val roomId: String, val eventId: String, val reason: String?, - val withRelations: List? = null, + val withRelTypes: List? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -63,7 +63,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters, ses roomId = params.roomId, eventId = params.eventId, reason = params.reason, - withRelations = params.withRelations, + withRelTypes = params.withRelTypes, ) ) }.fold( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt index cf2bc0dc4..2ed5c9f36 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/model/EventRedactBody.kt @@ -25,5 +25,10 @@ internal data class EventRedactBody( val reason: String? = null, @Json(name = "org.matrix.msc3912.with_relations") - val withRelations: List? = null, -) + val unstableWithRelTypes: List? = null, + + @Json(name = "with_rel_types") + val withRelTypes: List? = null, +) { + fun getBestWithRelTypes() = withRelTypes ?: unstableWithRelTypes +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index b285e90c9..90d78a51e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -26,9 +26,9 @@ internal interface EventSenderProcessor : SessionLifecycleObserver { fun postEvent(event: Event, encrypt: Boolean): Cancelable - fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List? = null): Cancelable - fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelations: List? = null): Cancelable + fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?, withRelTypes: List? = null): Cancelable fun postTask(task: QueuedTask): Cancelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 929fe7b9a..a4e3773eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.CoroutineSequencer import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer @@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3 */ @SessionScope internal class EventSenderProcessorCoroutine @Inject constructor( - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, private val sessionParams: SessionParams, private val queuedTaskFactory: QueuedTaskFactory, private val taskExecutor: TaskExecutor, @@ -101,8 +101,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor( return postTask(task) } - override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelations: List?): Cancelable { - return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelations) + override fun postRedaction(redactionLocalEcho: Event, reason: String?, withRelTypes: List?): Cancelable { + return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason, withRelTypes) } override fun postRedaction( @@ -110,9 +110,9 @@ internal class EventSenderProcessorCoroutine @Inject constructor( eventToRedactId: String, roomId: String, reason: String?, - withRelations: List? + withRelTypes: List? ): Cancelable { - val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelations) + val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason, withRelTypes) return postTask(task) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index a900e4ae5..85238ae94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -118,7 +118,7 @@ internal class QueueMemento @Inject constructor( eventId = it.redacts, roomId = it.roomId, reason = body?.reason, - withRelations = body?.withRelations, + withRelTypes = body?.getBestWithRelTypes(), ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt index 46df7e29f..e79808ee3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt @@ -43,13 +43,13 @@ internal class QueuedTaskFactory @Inject constructor( ) } - fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelations: List? = null): QueuedTask { + fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?, withRelTypes: List? = null): QueuedTask { return RedactQueuedTask( redactionLocalEchoId = redactionLocalEcho, toRedactEventId = eventId, roomId = roomId, reason = reason, - withRelations = withRelations, + withRelTypes = withRelTypes, redactEventTask = redactEventTask, localEchoRepository = localEchoRepository, cancelSendTracker = cancelSendTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index f484c24aa..b51a04f86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -26,14 +26,14 @@ internal class RedactQueuedTask( val redactionLocalEchoId: String, private val roomId: String, private val reason: String?, - private val withRelations: List?, + private val withRelTypes: List?, private val redactEventTask: RedactEventTask, private val localEchoRepository: LocalEchoRepository, private val cancelSendTracker: CancelSendTracker ) : QueuedTask(queueIdentifier = roomId, taskIdentifier = redactionLocalEchoId) { override suspend fun doExecute() { - redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelations)) + redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason, withRelTypes)) } override fun onTaskFailed() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 5c4ed8012..d27fe7270 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -130,6 +130,18 @@ internal class RoomSummaryDataSource @Inject constructor( ) } + fun getRoomSummariesChangesLive( + queryParams: RoomSummaryQueryParams, + sortOrder: RoomSortOrder = RoomSortOrder.NONE + ): LiveData> { + return monarchy.findAllMappedWithChanges( + { + roomSummariesQuery(it, queryParams).process(sortOrder) + }, + { emptyList() } + ) + } + fun getSpaceSummariesLive( queryParams: SpaceSummaryQueryParams, sortOrder: RoomSortOrder = RoomSortOrder.NONE @@ -253,6 +265,7 @@ internal class RoomSummaryDataSource @Inject constructor( ) return object : UpdatableLivePageResult { + override val livePagedList: LiveData> = mapped override val liveBoundaries: LiveData @@ -262,7 +275,14 @@ internal class RoomSummaryDataSource @Inject constructor( set(value) { field = value realmDataSourceFactory.updateQuery { - roomSummariesQuery(it, value).process(sortOrder) + roomSummariesQuery(it, value).process(this.sortOrder) + } + } + override var sortOrder: RoomSortOrder = sortOrder + set(value) { + field = value + realmDataSourceFactory.updateQuery { + roomSummariesQuery(it, this.queryParams).process(value) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 8adfdc5db..cbb75398c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -18,9 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm import io.realm.kotlin.createObject -import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -41,8 +39,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity @@ -65,6 +61,7 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo +import org.matrix.android.sdk.internal.session.room.timeline.RoomSummaryEventDecryptor import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import timber.log.Timber import javax.inject.Inject @@ -74,10 +71,9 @@ internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, - private val eventDecryptor: EventDecryptor, - private val crossSigningService: DefaultCrossSigningService, private val roomAccountDataDataSource: RoomAccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val roomSummaryEventDecryptor: RoomSummaryEventDecryptor, private val roomSummaryEventsHelper: RoomSummaryEventsHelper, ) { @@ -172,6 +168,9 @@ internal class RoomSummaryUpdater @Inject constructor( val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases .orEmpty() roomSummaryEntity.updateAliases(roomAliases) + + val wasEncrypted = roomSummaryEntity.isEncrypted + roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content) @@ -201,15 +200,13 @@ internal class RoomSummaryUpdater @Inject constructor( // better to use what we know roomSummaryEntity.joinedMembersCount = otherRoomMembers.size + 1 } - if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) { - if (aggregator == null) { - // Do it now - // mmm maybe we could only refresh shield instead of checking trust also? - crossSigningService.checkTrustAndAffectedRoomShields(otherRoomMembers) - } else { - // Schedule it - aggregator.userIdsForCheckingTrustAndAffectedRoomShields.addAll(otherRoomMembers) - } + } + + if (roomSummaryEntity.isEncrypted) { + if (!wasEncrypted || updateMembers || roomSummaryEntity.roomEncryptionTrustLevel == null) { + // trigger a shield update + // if users add devices/keys or signatures the device list manager will trigger a refresh + aggregator?.roomsWithMembershipChangesForShieldUpdate?.add(roomId) } } } @@ -220,12 +217,7 @@ internal class RoomSummaryUpdater @Inject constructor( Timber.v("Decryption skipped due to missing root event $eventId") } else -> { - if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { - Timber.v("Should decrypt $eventId") - tryOrNull { - runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") } - }?.let { root.setDecryptionResult(it) } - } + roomSummaryEventDecryptor.requestDecryption(root.asDomain()) } } } @@ -417,7 +409,7 @@ internal class RoomSummaryUpdater @Inject constructor( val relatedSpaces = lookupMap.keys .filter { it.roomType == RoomType.SPACE } .filter { - dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toList()).isNotEmpty() + dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toSet()).isNotEmpty() } .map { it.roomId } .distinct() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index 3707205ae..85fd39e9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.crypto.DecryptRoomEventUseCase import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI @@ -35,7 +35,7 @@ internal interface GetEventTask : Task { internal class DefaultGetEventTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, - private val eventDecryptor: EventDecryptor, + private val decryptEvent: DecryptRoomEventUseCase, private val clock: Clock, ) : GetEventTask { @@ -46,7 +46,7 @@ internal class DefaultGetEventTask @Inject constructor( // Try to decrypt the Event if (event.isEncrypted()) { - eventDecryptor.decryptEventAndSaveResult(event, timeline = "") + decryptEvent.decryptAndSaveResult(event) } event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt new file mode 100644 index 000000000..dfda82cc9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class RoomSummaryEventDecryptor @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + cryptoCoroutineScope: CoroutineScope, + private val cryptoService: dagger.Lazy +) { + + internal sealed class Message { + data class DecryptEvent(val event: Event) : Message() + data class NewSessionImported(val sessionId: String) : Message() + } + + private val scope: CoroutineScope = CoroutineScope( + cryptoCoroutineScope.coroutineContext + + SupervisorJob() + + CoroutineName("RoomSummaryDecryptor") + ) + + private val channel = Channel(capacity = 300) + + private val newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, sessionId: String) { + scope.launch(coroutineDispatchers.computation) { + channel.send(Message.NewSessionImported(sessionId)) + } + } + } + + private val unknownSessionsFailure = mutableMapOf>() + + init { + scope.launch { + cryptoService.get().addNewSessionListener(newSessionListener) + for (request in channel) { + when (request) { + is Message.DecryptEvent -> handleDecryptEvent(request.event) + is Message.NewSessionImported -> handleNewSessionImported(request.sessionId) + } + } + } + } + + private fun handleNewSessionImported(sessionId: String) { + unknownSessionsFailure[sessionId] + ?.toList() + .orEmpty() + .also { + unknownSessionsFailure[sessionId]?.clear() + }.forEach { + // post a retry! + requestDecryption(it) + } + } + + private suspend fun handleDecryptEvent(event: Event) { + if (event.getClearType() != EventType.ENCRYPTED) return + val algorithm = event.content?.get("algorithm") as? String + if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return + + try { + val result = cryptoService.get().decryptEvent(event, "") + // now let's persist the result in database + monarchy.writeAsync { realm -> + val eventEntity = EventEntity.where(realm, event.eventId.orEmpty()).findFirst() + eventEntity?.setDecryptionResult(result) + } + } catch (failure: Throwable) { + Timber.v(failure, "Failed to decrypt event ${event.eventId}") + // We don't need to get more details, just mark this session in failures + if (failure is MXCryptoError.Base) { + monarchy.writeAsync { realm -> + EventEntity.where(realm, eventId = event.eventId.orEmpty()) + .findFirst() + ?.let { + it.decryptionErrorCode = failure.errorType.name + it.decryptionErrorReason = failure.technicalMessage.takeIf { it.isNotEmpty() } ?: failure.detailedErrorDescription + } + } + + if (failure.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID || + failure.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX) { + (event.content["session_id"] as? String)?.let { sessionId -> + unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } + .add(event) + } + } + } + } + } + + fun requestDecryption(event: Event) { + channel.trySend(Message.DecryptEvent(event)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index d04b98ef7..917b01919 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -25,6 +25,7 @@ import io.realm.Sort import kotlinx.coroutines.CompletableDeferred import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -420,13 +421,22 @@ internal class TimelineChunk( } fun decryptIfNeeded(timelineEvent: TimelineEvent) { - if (timelineEvent.isEncrypted() && - timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } - } - if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { - // Thread aware for not encrypted events + if (!timelineEvent.isEncrypted()) return + val mxDecryptionResult = timelineEvent.root.mxDecryptionResult + if (mxDecryptionResult == null) { timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } else if (timelineEvent.root.verificationStateIsDirty.orFalse() && + mxDecryptionResult.verificationState == MessageVerificationState.UNKNOWN_DEVICE + ) { + // The goal is to catch late download of devices + timelineEvent.root.eventId?.also { + eventDecryptor.requestDecryption( + TimelineEventDecryptor.DecryptionRequest( + timelineEvent.root, + timelineId + ) + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index de79661de..c5d7598a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -42,7 +42,7 @@ internal class TimelineEventDecryptor @Inject constructor( ) { private val newSessionListener = object : NewSessionListener { - override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { + override fun onNewSession(roomId: String?, sessionId: String) { synchronized(unknownSessionsFailure) { unknownSessionsFailure[sessionId] ?.toList() @@ -130,8 +130,9 @@ internal class TimelineEventDecryptor @Inject constructor( return } try { - // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching - val result = runBlocking { cryptoService.decryptEvent(request.event, timelineId) } + val result = runBlocking { + cryptoService.decryptEvent(request.event, timelineId) + } Timber.v("Successfully decrypted event ${event.eventId}") realm.executeTransaction { val eventId = event.eventId ?: return@executeTransaction diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index 21b508d35..02c541c83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -22,6 +22,7 @@ import androidx.work.ListenableWorker import androidx.work.OneTimeWorkRequest import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.startChain import java.util.concurrent.TimeUnit @@ -34,7 +35,8 @@ import javax.inject.Inject * if not the chain will be doomed in failed state. */ internal class TimelineSendEventWorkCommon @Inject constructor( - private val workManagerProvider: WorkManagerProvider + private val workManagerProvider: WorkManagerProvider, + private val workManagerConfig: WorkManagerConfig, ) { fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable { @@ -47,7 +49,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor( inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .startChain(startChain) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt index 1bb86ecb4..2c34f1e2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt @@ -35,7 +35,12 @@ internal class DefaultSignOutService @Inject constructor( sessionParamsStore.updateCredentials(credentials) } - override suspend fun signOut(signOutFromHomeserver: Boolean) { - return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver)) + override suspend fun signOut(signOutFromHomeserver: Boolean, ignoreServerRequestError: Boolean) { + return signOutTask.execute( + SignOutTask.Params( + signOutFromHomeserver = signOutFromHomeserver, + ignoreServerRequestError = ignoreServerRequestError + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt index e5213c469..f8ec23b24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -30,7 +30,8 @@ import javax.inject.Inject internal interface SignOutTask : Task { data class Params( - val signOutFromHomeserver: Boolean + val signOutFromHomeserver: Boolean, + val ignoreServerRequestError: Boolean, ) } @@ -59,7 +60,9 @@ internal class DefaultSignOutTask @Inject constructor( // Ignore Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") } else { - throw throwable + if (!params.ignoreServerRequestError) { + throw throwable + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt index 76c3c38ab..bca3f55e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionState import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -33,15 +34,26 @@ internal class DefaultSyncService @Inject constructor( private val syncTokenStore: SyncTokenStore, private val syncRequestStateTracker: SyncRequestStateTracker, private val sessionState: SessionState, + private val workManagerConfig: WorkManagerConfig, ) : SyncService { private var syncThread: SyncThread? = null override fun requireBackgroundSync() { - SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) + SyncWorker.requireBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = sessionId, + workManagerConfig = workManagerConfig, + ) } override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) { - SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds) + SyncWorker.automaticallyBackgroundSync( + workManagerProvider = workManagerProvider, + sessionId = sessionId, + workManagerConfig = workManagerConfig, + serverTimeoutInSeconds = timeOutInSeconds, + delayInSeconds = repeatDelayInSeconds, + ) } override fun stopAnyBackgroundSync() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index a9de4d3a3..833178607 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -23,25 +23,28 @@ import org.matrix.android.sdk.api.extensions.measureSpan import org.matrix.android.sdk.api.extensions.measureSpannableMetric import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.pushrules.RuleScope import org.matrix.android.sdk.api.session.sync.InitialSyncStep import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionListeners import org.matrix.android.sdk.internal.session.dispatchTo import org.matrix.android.sdk.internal.session.pushrules.ProcessEventForPushTask -import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -53,13 +56,13 @@ internal class SyncResponseHandler @Inject constructor( private val sessionListeners: SessionListeners, private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, - private val cryptoSyncHandler: CryptoSyncHandler, private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler, - private val cryptoService: DefaultCryptoService, + private val cryptoService: CryptoService, private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, private val presenceSyncHandler: PresenceSyncHandler, + private val clock: Clock, matrixConfiguration: MatrixConfiguration, ) { @@ -72,16 +75,47 @@ internal class SyncResponseHandler @Inject constructor( reporter: ProgressReporter? ) { val isInitialSync = fromToken == null - Timber.v("Start handling sync, is InitialSync: $isInitialSync") + + val aggregator = SyncResponsePostTreatmentAggregator() relevantPlugins.filter { it.shouldReport(isInitialSync, afterPause) }.measureSpannableMetric { startCryptoService(isInitialSync) // Handle the to device events before the room ones // to ensure to decrypt them properly - handleToDevice(syncResponse, reporter) + handleToDevice(syncResponse) + + val syncLocalTimestampMillis = clock.epochMillis() + + // pass live state/crypto related event to crypto + + measureSpan("task", "crypto_session_event_handling") { + syncResponse.rooms?.invite?.entries?.map { (roomId, roomSync) -> + roomSync.inviteState + ?.events + ?.filter { it.isStateEvent() } + ?.forEach { + cryptoService.onStateEvent(roomId, it, aggregator.cryptoStoreAggregator) + } + } - val aggregator = SyncResponsePostTreatmentAggregator() + syncResponse.rooms?.join?.entries?.map { (roomId, roomSync) -> + roomSync.state + ?.events + ?.filter { it.isStateEvent() } + ?.forEach { + cryptoService.onStateEvent(roomId, it, aggregator.cryptoStoreAggregator) + } + + roomSync.timeline?.events?.forEach { + if (it.isEncrypted() && !isInitialSync) { + decryptIfNeeded(it, roomId) + } + it.ageLocalTs = syncLocalTimestampMillis - (it.unsignedData?.age ?: 0) + cryptoService.onLiveEvent(roomId, it, isInitialSync, aggregator.cryptoStoreAggregator) + } + } + } // Prerequisite for thread events handling in RoomSyncHandler // Disabled due to the new fallback @@ -103,7 +137,32 @@ internal class SyncResponseHandler @Inject constructor( } } - private fun List.startCryptoService(isInitialSync: Boolean) { + private suspend fun decryptIfNeeded(event: Event, roomId: String) { + try { + val timelineId = generateTimelineId(roomId) + // Event from sync does not have roomId, so add it to the event first + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + verificationState = result.messageVerificationState + ) + } catch (e: MXCryptoError) { + Timber.v(e, "Failed to decrypt $roomId") + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } + + private fun generateTimelineId(roomId: String): String { + return "RoomSyncHandler$roomId" + } + + private suspend fun List.startCryptoService(isInitialSync: Boolean) { measureSpan("task", "start_crypto_service") { measureTimeMillis { if (!cryptoService.isStarted()) { @@ -117,15 +176,17 @@ internal class SyncResponseHandler @Inject constructor( } } - private suspend fun List.handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) { + private suspend fun List.handleToDevice(syncResponse: SyncResponse) { measureSpan("task", "handle_to_device") { measureTimeMillis { Timber.v("Handle toDevice") - reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) { - if (syncResponse.toDevice != null) { - cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter) - } - } + cryptoService.receiveSyncChanges( + syncResponse.toDevice, + syncResponse.deviceLists, + syncResponse.deviceOneTimeKeysCount, + syncResponse.deviceUnusedFallbackKeyTypes, + syncResponse.nextBatch + ) }.also { Timber.v("Finish handling toDevice in $it ms") } @@ -221,10 +282,10 @@ internal class SyncResponseHandler @Inject constructor( } } - private fun List.markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + private suspend fun List.markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { measureSpan("task", "crypto_sync_handler_onSyncCompleted") { measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator) + cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator) }.also { Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt index af05e08da..4532a8d41 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt @@ -29,7 +29,8 @@ internal class SyncResponsePostTreatmentAggregator { val userIdsToFetch = mutableSetOf() // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync - val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf() + + val roomsWithMembershipChangesForShieldUpdate = mutableSetOf() // For the crypto store val cryptoStoreAggregator = CryptoStoreAggregator() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt deleted file mode 100644 index 7224b0c29..000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.session.sync.handler - -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.sync.model.SyncResponse -import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService -import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator -import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.session.sync.ProgressReporter -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) - -internal class CryptoSyncHandler @Inject constructor( - private val cryptoService: DefaultCryptoService, - private val verificationService: DefaultVerificationService -) { - - suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { - val total = toDevice.events?.size ?: 0 - toDevice.events - ?.filter { isSupportedToDevice(it) } - ?.forEachIndexed { index, event -> - progressReporter?.reportProgress(index * 100F / total) - // Decrypt event if necessary - Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}") - decryptToDeviceEvent(event, null) - - if (event.getClearType() == EventType.MESSAGE && - event.getClearContent()?.toModel()?.msgType == "m.bad.encrypted") { - Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") - } else { - Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}") - verificationService.onToDeviceEvent(event) - cryptoService.onToDeviceEvent(event) - } - } - } - - private val unsupportedPlainToDeviceEventTypes = listOf( - EventType.ROOM_KEY, - EventType.FORWARDED_ROOM_KEY, - EventType.SEND_SECRET - ) - - private fun isSupportedToDevice(event: Event): Boolean { - val algorithm = event.content?.get("algorithm") as? String - val type = event.type.orEmpty() - return if (event.isEncrypted()) { - algorithm == MXCRYPTO_ALGORITHM_OLM - } else { - // some clear events are not allowed - type !in unsupportedPlainToDeviceEventTypes - }.also { - if (!it) { - Timber.tag(loggerTag.value) - .w("Ignoring unsupported to device event ${event.type} alg:${algorithm}") - } - } - } - - fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { - cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator) - } - - /** - * Decrypt an encrypted event. - * - * @param event the event to decrypt - * @param timelineId the timeline identifier - * @return true if the event has been decrypted - */ - private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { - Timber.v("## CRYPTO | decryptToDeviceEvent") - if (event.getClearType() == EventType.ENCRYPTED) { - var result: MXEventDecryptionResult? = null - try { - result = cryptoService.decryptEvent(event, timelineId ?: "") - } catch (exception: MXCryptoError) { - event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) - val senderKey = event.content.toModel()?.senderKey ?: "" - // try to find device id to ease log reading - val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull { - it.identityKey() == senderKey - }?.deviceId ?: senderKey - Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") - } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}") - } - - if (null != result) { - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - return true - } else { - // Could happen for to device events - // None of the known session could decrypt the message - // In this case unwedging process might have been started (rate limited) - Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}") - } - } - - return false - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt new file mode 100644 index 000000000..9f77d7003 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/ShieldSummaryUpdater.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.handler + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class ShieldSummaryUpdater @Inject constructor( + private val olmMachine: dagger.Lazy, + private val coroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val computeShieldForGroup: ComputeShieldForGroupUseCase, +) { + + fun refreshShieldsForRoomsWithMembers(userIds: List) { + coroutineScope.launch(coroutineDispatchers.computation) { + cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userIds).forEach { roomId -> + if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) { + val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId) + val shield = computeShieldForGroup(olmMachine.get(), userGroup) + cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield) + } else { + cryptoSessionInfoProvider.updateShieldForRoom(roomId, null) + } + } + } + } + + fun refreshShieldsForRoomIds(roomIds: Set) { + coroutineScope.launch(coroutineDispatchers.computation) { + roomIds.forEach { roomId -> + val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId) + val shield = computeShieldForGroup(olmMachine.get(), userGroup) + cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt index 85bc8b0f9..3c205d501 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import org.matrix.android.sdk.api.MatrixPatterns -import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository import org.matrix.android.sdk.internal.di.SessionId @@ -39,16 +39,16 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor( private val directChatsHelper: DirectChatsHelper, private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val crossSigningService: DefaultCrossSigningService, private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository, private val workManagerProvider: WorkManagerProvider, + private val roomShieldSummaryUpdater: ShieldSummaryUpdater, @SessionId private val sessionId: String, ) { suspend fun handle(aggregator: SyncResponsePostTreatmentAggregator) { cleanupEphemeralFiles(aggregator.ephemeralFilesToDelete) updateDirectUserIds(aggregator.directChatsToCheck) fetchAndUpdateUsers(aggregator.userIdsToFetch) - handleUserIdsForCheckingTrustAndAffectedRoomShields(aggregator.userIdsForCheckingTrustAndAffectedRoomShields) + handleRefreshRoomShieldsForRooms(aggregator.roomsWithMembershipChangesForShieldUpdate) } private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List) { @@ -82,7 +82,9 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor( } } if (hasUpdate) { - updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + tryOrNull("Unable to update user account data") { + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) + } } } @@ -105,8 +107,8 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor( .enqueue() } - private fun handleUserIdsForCheckingTrustAndAffectedRoomShields(userIdsWithDeviceUpdate: Collection) { - if (userIdsWithDeviceUpdate.isEmpty()) return - crossSigningService.checkTrustAndAffectedRoomShields(userIdsWithDeviceUpdate.toList()) + private fun handleRefreshRoomShieldsForRooms(roomIds: Set) { + if (roomIds.isEmpty()) return + roomShieldSummaryUpdater.refreshShieldsForRoomIds(roomIds) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt index 92ebb41ad..bc4968256 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt @@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmList import io.realm.kotlin.where +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.InitialSyncRequestReason import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent @@ -122,7 +123,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( directMessages = directChats ) - updateUserAccountDataTask.execute(updateUserAccountParams) + tryOrNull("Unable to update user account data") { updateUserAccountDataTask.execute(updateUserAccountParams) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index f37e384b5..2e3707b7a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -19,9 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import dagger.Lazy import io.realm.Realm import io.realm.kotlin.createObject -import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -41,7 +39,6 @@ import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.RoomSync import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.settings.LightweightSettingsStorage -import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addTimelineEvent @@ -92,7 +89,6 @@ internal class RoomSyncHandler @Inject constructor( private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, private val roomAccountDataHandler: RoomSyncAccountDataHandler, - private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler, private val threadsAwarenessHandler: ThreadsAwarenessHandler, @@ -199,7 +195,7 @@ internal class RoomSyncHandler @Inject constructor( roomSync = handlingStrategy.data[it] ?: error("Should not happen"), insertType = EventInsertType.INITIAL_SYNC, syncLocalTimestampMillis = syncLocalTimeStampMillis, - aggregator + aggregator = aggregator, ) } realm.insertOrUpdate(roomEntities) @@ -257,8 +253,6 @@ internal class RoomSyncHandler @Inject constructor( eventId = event.eventId root = eventEntity } - // Give info to crypto module - cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator) roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator) } } @@ -420,7 +414,7 @@ internal class RoomSyncHandler @Inject constructor( // It's annoying roomId is not there, but lot of code rely on it. // And had to do it now as copy would delete all decryption results.. val ageLocalTs = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0) - val event = rawEvent.copy(roomId = roomId).also { + val event = rawEvent.copyAll(roomId = roomId).also { it.ageLocalTs = ageLocalTs } if (event.eventId == null || event.senderId == null || event.type == null) { @@ -434,17 +428,6 @@ internal class RoomSyncHandler @Inject constructor( liveEventService.get().dispatchLiveEventReceived(event, roomId) } - if (event.isEncrypted() && !isInitialSync) { - try { - decryptIfNeeded(event, roomId) - // share the decryption result with the rawEvent because the decryption is done on a copy containing the roomId, see previous comment - rawEvent.mxDecryptionResult = event.mxDecryptionResult - rawEvent.mCryptoError = event.mCryptoError - rawEvent.mCryptoErrorReason = event.mCryptoErrorReason - } catch (e: InterruptedException) { - Timber.i("Decryption got interrupted") - } - } var contentToInject: String? = null if (!isInitialSync) { contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event) @@ -499,7 +482,9 @@ internal class RoomSyncHandler @Inject constructor( } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator) +// runBlocking { +// cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) +// } // Try to remove local echo event.unsignedData?.transactionId?.also { txId -> @@ -580,31 +565,6 @@ internal class RoomSyncHandler @Inject constructor( } } - private fun decryptIfNeeded(event: Event, roomId: String) { - try { - val timelineId = generateTimelineId(roomId) - // Event from sync does not have roomId, so add it to the event first - // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching - val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) } - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } catch (e: MXCryptoError) { - if (e is MXCryptoError.Base) { - event.mCryptoError = e.errorType - event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription - } - } - } - - private fun generateTimelineId(roomId: String): String { - return "RoomSyncHandler$roomId" - } - data class EphemeralResult( val typingUserIds: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index a04bc7462..abee36673 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.sync.SyncPresence import org.matrix.android.sdk.internal.session.sync.SyncTask +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -59,6 +60,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, @Inject lateinit var syncTask: SyncTask @Inject lateinit var workManagerProvider: WorkManagerProvider + @Inject lateinit var workManagerConfig: WorkManagerConfig override fun injectWith(injector: SessionComponent) { injector.inject(this) @@ -77,6 +79,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, automaticallyBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, + workManagerConfig = workManagerConfig, serverTimeoutInSeconds = params.timeout, delayInSeconds = params.delay, forceImmediate = hasToDeviceEvents @@ -86,6 +89,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, requireBackgroundSync( workManagerProvider = workManagerProvider, sessionId = params.sessionId, + workManagerConfig = workManagerConfig, serverTimeoutInSeconds = 0 ) } @@ -123,6 +127,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun requireBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, + workManagerConfig: WorkManagerConfig, serverTimeoutInSeconds: Long = 0 ) { val data = WorkerParamsFactory.toData( @@ -134,7 +139,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInputData(data) .startChain(true) @@ -146,6 +151,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, fun automaticallyBackgroundSync( workManagerProvider: WorkManagerProvider, sessionId: String, + workManagerConfig: WorkManagerConfig, serverTimeoutInSeconds: Long = 0, delayInSeconds: Long = 30, forceImmediate: Boolean = false @@ -160,7 +166,7 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters, ) ) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) + .setConstraints(WorkManagerProvider.getWorkConstraints(workManagerConfig)) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt new file mode 100644 index 000000000..804eaeb05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/DefaultWorkManagerConfig.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.workmanager + +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import javax.inject.Inject + +@Suppress("RedundantIf", "IfThenToElvis") +internal class DefaultWorkManagerConfig @Inject constructor( + private val credentials: Credentials, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, +) : WorkManagerConfig { + override fun withNetworkConstraint(): Boolean { + val disableNetworkConstraint = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.disableNetworkConstraint + return if (disableNetworkConstraint != null) { + // Boolean `io.element.disable_network_constraint` explicitly set in the .well-known file + disableNetworkConstraint.not() + } else if (credentials.discoveryInformation?.disableNetworkConstraint == true) { + // Boolean `io.element.disable_network_constraint` explicitly set to `true` in the login response + false + } else { + // Default, use the Network constraint + true + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt similarity index 74% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt index 266c1a274..05523a6cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/workmanager/WorkManagerConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto +package org.matrix.android.sdk.internal.session.workmanager -internal enum class GossipRequestType { - KEY, - SECRET +internal interface WorkManagerConfig { + fun withNetworkConstraint(): Boolean } diff --git a/matrix-sdk-android/src/main/res/values-ar/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ar/strings_sas.xml index 423a8332b..7e09da177 100644 --- a/matrix-sdk-android/src/main/res/values-ar/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-ar/strings_sas.xml @@ -1,19 +1,19 @@ - كَلب + كلب هِرَّة أَسَد حِصَان - حِصَانٌ بِقَرن + حصان وحيد القرن خِنزِير فِيل أَرنَب باندَا دِيك - بِطريق + بطريق سُلحفاة - سَمَكَة + سَمَكة أُخطُبُوط فَرَاشَة زَهرَة diff --git a/matrix-sdk-android/src/main/res/values-cs/strings_sas.xml b/matrix-sdk-android/src/main/res/values-cs/strings_sas.xml index 1ef9d56f6..1c63273e7 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings_sas.xml @@ -48,7 +48,7 @@ Sponka Nůžky Zámek - Klíč + Klíč ke dveřím Kladivo Telefon Vlajka diff --git a/matrix-sdk-android/src/main/res/values-es/strings_sas.xml b/matrix-sdk-android/src/main/res/values-es/strings_sas.xml index b5f062cb6..04ef234d9 100644 --- a/matrix-sdk-android/src/main/res/values-es/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-es/strings_sas.xml @@ -50,7 +50,7 @@ Candado Llave Martillo - Telefono + Teléfono Bandera Tren Bicicleta diff --git a/matrix-sdk-android/src/main/res/values-fa/strings_sas.xml b/matrix-sdk-android/src/main/res/values-fa/strings_sas.xml new file mode 100644 index 000000000..d1c5e96c4 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fa/strings_sas.xml @@ -0,0 +1,68 @@ + + + + سگ + گربه + شیر + اسب + تک شاخ + خوک + فیل + خرگوش + پاندا + خروس + پنگوئن + لاک‌پشت + ماهی + اختاپوس + پروانه + گل + درخت + کاکتوس + قارچ + زمین + ماه + ابر + آتش + موز + سیب + توت فرنگی + ذرت + پیتزا + کیک + قلب + خنده + ربات + کلاه + عینک + آچار + بابا نوئل + لایک + چتر + ساعت شنی + ساعت + هدیه + لامپ + کتاب + مداد + گیره کاغذ + قیچی + قفل + کلید + چکش + تلفن + پرچم + قطار + دوچرخه + هواپیما + موشک + جام + توپ + گیتار + شیپور + زنگ + لنگر + هدفون + پوشه + سنجاق + diff --git a/matrix-sdk-android/src/main/res/values-id/strings_sas.xml b/matrix-sdk-android/src/main/res/values-id/strings_sas.xml new file mode 100644 index 000000000..73270815e --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-id/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Anjing + Kucing + Singa + Kuda + Unicorn + Babi + Gajah + Kelinci + Panda + Ayam + Penguin + Kura-Kura + Ikan + Gurita + Kupu-Kupu + Bunga + Pohon + Kaktus + Jamur + Bola Dunia + Bulan + Awan + Api + Pisang + Apel + Stroberi + Jagung + Pizza + Kue + Hati + Senyuman + Robot + Topi + Kacamata + Kunci Bengkel + Santa + Jempol + Payung + Jam Pasir + Jam + Kado + Bohlam Lampu + Buku + Pensil + Klip Kertas + Gunting + Gembok + Kunci + Palu + Telepon + Bendera + Kereta Api + Sepeda + Pesawat + Roket + Piala + Bola + Gitar + Terompet + Lonceng + Jangkar + Headphone + Map + Pin + diff --git a/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml index 12f90e316..562577bef 100644 --- a/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml @@ -32,7 +32,7 @@ ケーキ ハート スマイル - ロボと + ロボット 帽子 めがね スパナ @@ -63,6 +63,6 @@ ベル いかり ヘッドホン - フォルダ + フォルダー ピン diff --git a/matrix-sdk-android/src/main/res/values-pt/strings_sas.xml b/matrix-sdk-android/src/main/res/values-pt/strings_sas.xml new file mode 100644 index 000000000..d3108551c --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Cão + Gato + Leão + Cavalo + Unicórnio + Porco + Elefante + Coelho + Panda + Galo + Pinguim + Tartaruga + Peixe + Polvo + Borboleta + Flor + Árvore + Cato + Cogumelo + Globo + Lua + Nuvem + Fogo + Banana + Maçã + Morango + Milho + Piza + Bolo + Coração + Sorriso + Robô + Chapéu + Óculos + Chave inglesa + Pai Natal + Polegar para cima + Guarda-chuva + Ampulheta + Relógio + Presente + Lâmpada + Livro + Lápis + Clipe + Tesoura + Cadeado + Chave + Martelo + Telefone + Bandeira + Comboio + Bicicleta + Avião + Foguetão + Troféu + Bola + Guitarra + Trompete + Sino + Âncora + Fones + Pasta + Pionés + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml b/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml index 72fd9cc2a..ea9af6644 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml @@ -1,66 +1,66 @@ - Hlava psa - Hlava mačky - Hlava leva + Pes + Mačka + Lev Kôň - Hlava jednorožca - Hlava prasaťa + Jednorožec + Prasa Slon - Hlava zajaca - Hlava pandy + Zajac + Panda Kohút Tučniak Korytnačka Ryba Chobotnica Motýľ - Tulipán - Listnatý strom + Kvet + Strom Kaktus Huba Zemeguľa - Polmesiac + Mesiac Oblak Oheň Banán - Červené jablko + Jablko Jahoda - Kukuričný klas + Kukurica Pizza - Narodeninová torta - červené srdce - Škeriaca sa tvár + Torta + Srdce + Smajlík Robot - Cilinder + Klobúk Okuliare - Francúzsky kľúč - Santa Claus + Vidlicový kľúč + Mikuláš Palec nahor Dáždnik Presýpacie hodiny Budík - Zabalený darček + Darček Žiarovka - Zatvorená kniha + Kniha Ceruzka - Sponka na papier + Kancelárska sponka Nožnice - Zatvorená zámka + Zámka Kľúč Kladivo Telefón - Kockovaná zástava - Rušeň + Zástava + Vlak Bicykel Lietadlo Raketa Trofej - Futbal + Lopta Gitara Trúbka - Zvon + Zvonec Kotva Slúchadlá Fascikel diff --git a/matrix-sdk-android/src/main/res/values-sq/strings_sas.xml b/matrix-sdk-android/src/main/res/values-sq/strings_sas.xml new file mode 100644 index 000000000..309cec8c9 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sq/strings_sas.xml @@ -0,0 +1,67 @@ + + + + Qen + Mace + Luan + Kalë + Njëbrirësh + Derr + Elefant + Lepur + Panda + Këndes + Pinguin + Breshkë + Peshk + Oktapod + Flutur + Lule + Pemë + Kaktus + Kërpudhë + Rruzull + Hënë + Re + Zjarr + Banane + Mollë + Luleshtrydhe + Misër + Picë + Tortë + Zemër + Emotikon + Robot + Kapë + Syze + Çelës + Babagjyshi i Vitit të Ri + Ombrellë + Klepsidër + Sahat + Dhuratë + Llambë + Libër + Laps + Kapëse + Gërshërë + Dry + Çelës + Çekiç + Telefon + Flamur + Tren + Biçikletë + Avion + Raketë + Trofe + Top + Kitarë + Trombë + Kambanë + Spirancë + Kufje + Dosje + Karficë + diff --git a/matrix-sdk-android/src/main/res/values-vi/strings_sas.xml b/matrix-sdk-android/src/main/res/values-vi/strings_sas.xml new file mode 100644 index 000000000..8ad1a4612 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-vi/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Chó + Mèo + Sư tử + Ngựa + Kỳ lân + Heo + Voi + Thỏ + Gấu trúc + Gà trống + Chim cánh cụt + Rùa + + Bạch tuộc + Bướm + Hoa + Cây + Xương rồng + Nấm + Địa cầu + Mặt trăng + Mây + Lửa + Chuối + Táo + Dâu tây + Bắp + Pizza + Bánh + Tim + Mặt cười + Rô-bô + + Kính mắt + Cờ-lê + ông già Nô-en + Thích + Cái ô + Đồng hồ cát + Đồng hồ + Quà tặng + Bóng đèn tròn + Sách + Viết chì + Kẹp giấy + Cái kéo + Ổ khóa + Chìa khóa + Búa + Điện thoại + Lá cờ + Xe lửa + Xe đạp + Máy bay + Tên lửa + Cúp + Banh + Ghi-ta + Kèn + Chuông + Mỏ neo + Tai nghe + Thư mục + Ghim + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml new file mode 100644 index 000000000..fa4e49776 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml @@ -0,0 +1,68 @@ + + + + + + 獅子 + + 獨角獸 + + 大象 + 兔子 + 熊貓 + 公雞 + 企鵝 + 烏龜 + + 章魚 + 蝴蝶 + + + 仙人掌 + 蘑菇 + 地球 + 月亮 + 雲朵 + + 香蕉 + 蘋果 + 草莓 + 玉米 + 披薩 + 蛋糕 + 愛心 + 笑臉 + 機器人 + 帽子 + 眼鏡 + 扳手 + 聖誕老人 + + 雨傘 + 沙漏 + 時鐘 + 禮物 + 燈泡 + + 鉛筆 + 迴紋針 + 剪刀 + 鎖頭 + 鑰匙 + 鎚子 + 電話 + 旗幟 + 火車 + 腳踏車 + 飛機 + 火箭 + 獎盃 + 足球 + 吉他 + 喇叭 + 鈴鐺 + 船錨 + 耳機 + 資料夾 + 圖釘 + diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt index df6fc5f16..8402a998f 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/DefaultSendToDeviceTaskTest.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams @@ -34,7 +35,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse -import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse @@ -211,7 +211,7 @@ class DefaultSendToDeviceTaskTest { throw java.lang.AssertionError("Should not be called") } - override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse { + override suspend fun uploadKeys(body: JsonDict): KeysUploadResponse { throw java.lang.AssertionError("Should not be called") } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/MoshiNumbersAsInt.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/MoshiNumbersAsInt.kt new file mode 100644 index 000000000..7e10e92f8 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/MoshiNumbersAsInt.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import org.amshove.kluent.shouldNotContain +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType + +class MoshiNumbersAsInt { + + @Test + fun numberShouldNotPutAllAsFloat() { + val event = Event( + type = "m.room.encrypted", + eventId = null, + content = mapOf( + "algorithm" to "m.olm.v1.curve25519-aes-sha2", + "ciphertext" to mapOf( + "cfA3dINwtmMW0DbJmnT6NiGAbOSa299Hxs6KxHgbDBw" to mapOf( + "body" to "Awogc5...", + "type" to 1 + ), + ), + ), + prevContent = null, + originServerTs = null, + senderId = "@web:localhost:8481" + ) + + val toDeviceSyncResponse = ToDeviceSyncResponse(listOf(event)) + + val adapter = MoshiProvider.providesMoshi().adapter(ToDeviceSyncResponse::class.java) + + val jsonString = adapter.toJson(toDeviceSyncResponse) + + jsonString shouldNotContain "1.0" + } + + @Test + fun testParseThenSerialize() { + val raw = """ + {"events":[{"type":"m.room.encrypted","content":{"algorithm":"m.olm.v1.curve25519-aes-sha2","ciphertext":{"cfA3dINwtmMW0DbJmnT6NiGAbOSa299Hxs6KxHgbDBw":{"body":"Awogc5L3QuIyvkluB1O/UAJp0","type":1}},"sender_key":"fqhBEOHXSSQ7ZKt1xlBg+hSTY1NEM8hezMXZ5lyBR1M"},"sender":"@web:localhost:8481"}]} + """.trimIndent() + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(ToDeviceSyncResponse::class.java) + + val content = adapter.fromJson(raw) + + val serialized = MoshiProvider.providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + .adapter(ToDeviceSyncResponse::class.java).toJson(content) + + serialized shouldNotContain "1.0" + + println(serialized) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt deleted file mode 100644 index 5b41ff6da..000000000 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.amshove.kluent.fail -import org.amshove.kluent.shouldBe -import org.junit.Test -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager - -class UnRequestedKeysManagerTest { - - private val aliceMxId = "alice@example.com" - private val bobMxId = "bob@example.com" - private val bobDeviceId = "MKRJDSLYGA" - - private val device1Id = "MGDAADVDMG" - - private val aliceFirstDevice = CryptoDeviceInfo( - deviceId = device1Id, - userId = aliceMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU", - "ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI", - ), - signatures = mapOf( - aliceMxId to mapOf( - "ed25519:$device1Id" - to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ", - "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" - to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA" - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"), - trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) - ) - - private val aBobDevice = CryptoDeviceInfo( - deviceId = bobDeviceId, - userId = bobMxId, - algorithms = MXCryptoAlgorithms.supportedAlgorithms(), - keys = mapOf( - "curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0", - "ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs", - ), - signatures = mapOf( - bobMxId to mapOf( - "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", - ) - ), - unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") - ) - - @Test - fun `test process key request if invite received`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - 1_000 - ) - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId2" - ), - 1_000 - ) - // for now no reason to accept - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { - fail("There should be no key to process") - } - } - - // ACT - // suppose an invite is received but from another user - val inviteTime = 1_000L - unrequestedForwardManager.onInviteReceived(roomId, "@jhon:example.com", inviteTime) - - // we shouldn't process the requests! -// runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - fail("There should be no key to process") - } -// } - - // ACT - // suppose an invite is received from correct user - - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - it.size shouldBe 2 - } - } - } - - @Test - fun `test invite before keys`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, 1_000) - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - 1_000 - ) - - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) { - it.size shouldBe 1 - } - } - } - - @Test - fun `test validity window`() { - val fakeDeviceListManager = mockk { - coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap().apply { - setObject(bobMxId, bobDeviceId, aBobDevice) - } - } - val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager) - - val roomId = "someRoomId" - - val timeOfKeyReception = 1_000L - - unrequestedForwardManager.onUnRequestedKeyForward( - roomId, - createFakeSuccessfullyDecryptedForwardToDevice( - aBobDevice, - aliceFirstDevice, - aBobDevice, - megolmSessionId = "megolmId1" - ), - timeOfKeyReception - ) - - val currentTimeWindow = 10 * 60_000 - - // simulate very late invite - val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000 - unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime) - - runBlocking { - unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) { - fail("There should be no key to process") - } - } - } - - private fun createFakeSuccessfullyDecryptedForwardToDevice( - sentBy: CryptoDeviceInfo, - dest: CryptoDeviceInfo, - sessionInitiator: CryptoDeviceInfo, - algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, - roomId: String = "!zzgDlIhbWOevcdFBXr:example.com", - megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw" - ): Event { - return Event( - type = EventType.ENCRYPTED, - eventId = "!fake", - senderId = sentBy.userId, - content = OlmEventContent( - ciphertext = mapOf( - dest.identityKey()!! to mapOf( - "type" to 0, - "body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+" - ) - ), - senderKey = sentBy.identityKey() - ).toContent(), - - ).apply { - mxDecryptionResult = OlmDecryptionResult( - payload = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to ForwardedRoomKeyContent( - algorithm = algorithm, - roomId = roomId, - senderKey = sessionInitiator.identityKey(), - sessionId = megolmSessionId, - sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..." - ).toContent() - ), - senderKey = sentBy.identityKey() - ) - } - } -} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt index a00ac3a17..503706383 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeRemovePusherTask import org.matrix.android.sdk.test.fakes.FakeTaskExecutor import org.matrix.android.sdk.test.fakes.FakeTogglePusherTask +import org.matrix.android.sdk.test.fakes.FakeWorkManagerConfig import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider import org.matrix.android.sdk.test.fakes.internal.FakePushGatewayNotifyTask import org.matrix.android.sdk.test.fixtures.PusherFixture @@ -41,6 +42,7 @@ class DefaultPushersServiceTest { private val togglePusherTask = FakeTogglePusherTask() private val removePusherTask = FakeRemovePusherTask() private val taskExecutor = FakeTaskExecutor() + private val fakeWorkManagerConfig = FakeWorkManagerConfig() private val pushersService = DefaultPushersService( workManagerProvider.instance, @@ -52,6 +54,7 @@ class DefaultPushersServiceTest { togglePusherTask, removePusherTask, taskExecutor.instance, + fakeWorkManagerConfig, ) @Test diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt index 0ae712bff..113dc4ce8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore class EventEditValidatorTest { @@ -62,7 +62,7 @@ class EventEditValidatorTest { @Test fun `edit should be valid`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -71,7 +71,7 @@ class EventEditValidatorTest { @Test fun `original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -83,7 +83,7 @@ class EventEditValidatorTest { @Test fun `original event and replacement event must have the same room_id`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -101,7 +101,7 @@ class EventEditValidatorTest { @Test fun `replacement and original events must not have a state_key property`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -119,8 +119,8 @@ class EventEditValidatorTest { @Test fun `replacement event must have an new_content property`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -157,8 +157,8 @@ class EventEditValidatorTest { @Test fun `The original event must not itself have a rel_type of m_replace`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -207,8 +207,8 @@ class EventEditValidatorTest { @Test fun `valid e2ee edit`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -224,8 +224,8 @@ class EventEditValidatorTest { @Test fun `If the original event was encrypted, the replacement should be too`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -241,12 +241,12 @@ class EventEditValidatorTest { @Test fun `encrypted, original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } - every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + every { deviceWithIdentityKey("@bob:example.com", "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns mockk { every { userId } returns "@bob:example.com" } @@ -256,7 +256,9 @@ class EventEditValidatorTest { validator .validateEdit( encryptedEvent, - encryptedEditEvent.copy().apply { + encryptedEditEvent.copy( + senderId = "@bob:example.com" + ).apply { mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" ) @@ -269,12 +271,12 @@ class EventEditValidatorTest { @Test fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } - every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + every { deviceWithIdentityKey(any(), "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns null } val validator = EventEditValidator(mockCryptoStore) @@ -288,7 +290,7 @@ class EventEditValidatorTest { ) } - ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + ) shouldBeInstanceOf EventEditValidator.EditValidity.Unknown::class validator .validateEdit( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt similarity index 65% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt index 57de3f5ac..e8b47bc40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/legacy/LegacySessionImporter.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.legacy +package org.matrix.android.sdk.test.fakes -interface LegacySessionImporter { +import org.matrix.android.sdk.internal.session.workmanager.WorkManagerConfig - /** - * Will eventually import a session created by the legacy app. - * @return true if a session has been imported - */ - fun process(): Boolean +class FakeWorkManagerConfig : WorkManagerConfig { + override fun withNetworkConstraint(): Boolean { + return true + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt index 2e7b36ff6..03b0bf0bc 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt @@ -32,7 +32,7 @@ object CredentialsFixture { accessToken, refreshToken, homeServer, - deviceId, + deviceId ?: "", discoveryInformation, ) }