diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml
index 50a9cdf5fc..7cb47fa952 100644
--- a/.github/ISSUE_TEMPLATE/release.yml
+++ b/.github/ISSUE_TEMPLATE/release.yml
@@ -23,7 +23,8 @@ body:
### Do the release
- - [ ] Create release with gitflow, branch name `release/1.2.3`
+ - [ ] Make sure `develop` and `main` are up to date (git pull)
+ - [ ] Checkout develop and create a release with gitflow, branch name `release/1.2.3`
- [ ] Check the crashes from the PlayStore
- [ ] Check the rageshake with the current dev version: https://github.com/matrix-org/element-android-rageshakes/labels/1.2.3-dev
- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()`
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0573461e7a..b6746c77d3 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,6 +10,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ reviewers:
+ - "vector-im/element-android-reviewers"
ignore:
- dependency-name: "*github-script*"
# Updates for Gradle dependencies used in the app
@@ -19,6 +21,6 @@ updates:
interval: "daily"
open-pull-requests-limit: 200
reviewers:
- - "bmarty"
+ - "vector-im/element-android-reviewers"
ignore:
- dependency-name: com.google.zxing:core
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000000..b6333c5940
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,22 @@
+name: Documentation
+
+on:
+ push:
+ branches: [ develop ]
+
+jobs:
+ docs:
+ name: Generate and publish Android Matrix SDK documentation
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Build docs
+ run: ./gradlew dokkaHtml
+
+ - name: Deploy docs
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./matrix-sdk-android/build/dokka/html
diff --git a/.github/workflows/nightly.yml b/.github/workflows/post-pr.yml
similarity index 85%
rename from .github/workflows/nightly.yml
rename to .github/workflows/post-pr.yml
index 40fbac2bf5..54107475c7 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/post-pr.yml
@@ -1,28 +1,43 @@
-name: Nightly Tests
+name: Integration Tests
+
+# This runs for all closed pull requests against main, including those closed without merge.
+# Further filtering occurs in 'should-i-run'
on:
- push:
- branches: [ release/* ]
- schedule:
- # At 20:00 every day UTC
- - cron: '0 20 * * *'
- workflow_dispatch:
+ pull_request:
+ types: [closed]
+ branches: [develop]
# Enrich gradle.properties for CI/CD
env:
CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
+
jobs:
+
+ # More info on should-i-run:
+ # If this fails to run (the IF doesn't complete) then the needs will not be satisfied for any of the
+ # other jobs below, so none will run.
+ # except for the notification job at the bottom which will run all the time, unless should-i-run isn't
+ # successful, or all the other jobs have succeeded
+
+ should-i-run:
+ name: Check if PR is suitable for analysis
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.merged # Additionally require PR to have been completely merged.
+ steps:
+ - run: echo "Run those tests!" # no-op success
+
# Run Android Tests
integration-tests:
name: Matrix SDK - Running Integration Tests
+ needs: should-i-run
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
api-level: [ 28 ]
- # No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
@@ -210,6 +225,7 @@ jobs:
ui-tests:
name: UI Tests (Synapse)
+ needs: should-i-run
runs-on: macos-latest
strategy:
fail-fast: false
@@ -268,7 +284,8 @@ jobs:
codecov-units:
name: Unit tests with code coverage
- runs-on: macos-latest
+ needs: should-i-run
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
@@ -292,49 +309,21 @@ jobs:
path: |
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
- sonarqube:
- name: Sonarqube upload
- runs-on: macos-latest
- if: always() && github.event_name == 'schedule'
- needs:
- - codecov-units
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-java@v3
- with:
- distribution: 'adopt'
- java-version: '11'
- - uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
- - uses: actions/download-artifact@v3
- with:
- name: codecov-xml # will restore to allCodeCoverageReport.xml by default; we restore to the same location in following tasks
- - run: mkdir -p build/reports/jacoco/allCodeCoverageReport/
- - run: mv allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/
- - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
- env:
- ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
-
-# Notify the channel about scheduled runs, or pushes to the release branches, do not notify for manually triggered runs
+# Notify the channel about delayed failures
notify:
name: Notify matrix
runs-on: ubuntu-latest
needs:
+ - should-i-run
- integration-tests
- ui-tests
- - sonarqube
- if: always() && github.event_name != 'workflow_dispatch'
+ - codecov-units
+ if: always() && (needs.should-i-run.result == 'success' ) && ((needs.codecov-units.result != 'success' ) || (needs.ui-tests.result != 'success') || (needs.integration-tests.result != 'success'))
# No concurrency required, runs every time on a schedule.
steps:
- uses: michaelkaye/matrix-hookshot-action@v1.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
- text_template: "{{#if '${{ github.event_name == 'schedule' }}' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
- html_template: "{{#if '${{ github.event_name == 'schedule' }}' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
+ text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
+ html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
new file mode 100644
index 0000000000..6809751d91
--- /dev/null
+++ b/.github/workflows/sonarqube.yml
@@ -0,0 +1,81 @@
+name: Sonarqube nightly
+
+on:
+ schedule:
+ - cron: '0 20 * * *'
+
+# Enrich gradle.properties for CI/CD
+env:
+ CI_GRADLE_ARG_PROPERTIES: >
+ -Porg.gradle.jvmargs=-Xmx4g
+ -Porg.gradle.parallel=false
+jobs:
+ codecov-units:
+ name: Unit tests with code coverage
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+ - run: ./gradlew allCodeCoverageReport $CI_GRADLE_ARG_PROPERTIES
+ - name: Upload Codecov data
+ uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: codecov-xml
+ path: |
+ build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
+
+ sonarqube:
+ name: Sonarqube upload
+ runs-on: ubuntu-latest
+ needs:
+ - codecov-units
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+ - uses: actions/download-artifact@v3
+ with:
+ name: codecov-xml # will restore to allCodeCoverageReport.xml by default; we restore to the same location in following tasks
+ - run: mkdir -p build/reports/jacoco/allCodeCoverageReport/
+ - run: mv allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/
+ - run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
+ env:
+ ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
+
+# Notify the channel about sonarqube failures
+ notify:
+ name: Notify matrix
+ runs-on: ubuntu-latest
+ needs:
+ - sonarqube
+ - codecov-units
+ if: always() && (needs.sonarqube.result != 'success' || needs.codecov-units.result != 'success')
+ steps:
+ - uses: michaelkaye/matrix-hookshot-action@v1.0.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
+ text_template: "Sonarqube run (on ${{ github.ref }}): {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
+ html_template: "Sonarqube run (on ${{ github.ref }}): {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/README.md b/README.md
index 8306fd8593..54dfb7b288 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
[
](https://play.google.com/store/apps/details?id=im.vector.app)
[
](https://f-droid.org/app/im.vector.app)
-Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly test status: [](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
+Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
# New Android SDK
@@ -53,3 +53,4 @@ Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/
Issues are triaged by community members and the Android App Team, following the [triage process](https://github.com/vector-im/element-meta/wiki/Triage-process).
We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues.
+
diff --git a/build.gradle b/build.gradle
index badc1da569..fa26638015 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,10 +5,17 @@ buildscript {
apply from: 'dependencies_groups.gradle'
repositories {
- google()
+ // Do not use `google()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://maven.google.com'
+ }
maven {
url "https://plugins.gradle.org/m2/"
}
+ // Do not use `mavenCentral()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://repo1.maven.org/maven2'
+ }
}
dependencies {
@@ -47,6 +54,7 @@ allprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
repositories {
+ // Do not use `mavenCentral()`, it prevents Dependabot from working properly
maven {
url 'https://repo1.maven.org/maven2'
content {
@@ -71,14 +79,18 @@ allprojects {
groups.jitsi.group.each { includeGroup it }
}
}
- google {
+ // Do not use `google()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://maven.google.com'
content {
groups.google.regex.each { includeGroupByRegex it }
groups.google.group.each { includeGroup it }
}
}
//noinspection JcenterRepositoryObsolete
- jcenter {
+ // Do not use `jcenter`, it prevents Dependabot from working properly
+ maven {
+ url 'https://jcenter.bintray.com'
content {
groups.jcenter.regex.each { includeGroupByRegex it }
groups.jcenter.group.each { includeGroup it }
diff --git a/changelog.d/5494.feature b/changelog.d/5494.feature
new file mode 100644
index 0000000000..59b8a78a2c
--- /dev/null
+++ b/changelog.d/5494.feature
@@ -0,0 +1 @@
+Use key backup before requesting keys + refactor & improvement of key request/forward
\ No newline at end of file
diff --git a/changelog.d/5559.sdk b/changelog.d/5559.sdk
new file mode 100644
index 0000000000..2466fcef48
--- /dev/null
+++ b/changelog.d/5559.sdk
@@ -0,0 +1,4 @@
+- New API to enable/disable key forwarding CryptoService#enableKeyGossiping()
+- New API to limit room key request only to own devices MXCryptoConfig#limitRoomKeyRequestsToMyDevices
+- Event Trail API has changed, now using AuditTrail events
+- New API to manually accept an incoming key request CryptoService#manuallyAcceptRoomKeyRequest()
diff --git a/changelog.d/5825.bugfix b/changelog.d/5825.bugfix
new file mode 100644
index 0000000000..77560027ba
--- /dev/null
+++ b/changelog.d/5825.bugfix
@@ -0,0 +1 @@
+Changed copy and list order in member profile screen.
\ No newline at end of file
diff --git a/changelog.d/5911.feature b/changelog.d/5911.feature
new file mode 100644
index 0000000000..368a3b4056
--- /dev/null
+++ b/changelog.d/5911.feature
@@ -0,0 +1 @@
+Screen sharing over WebRTC
diff --git a/changelog.d/5936.feature b/changelog.d/5936.feature
new file mode 100644
index 0000000000..cbf14aaba1
--- /dev/null
+++ b/changelog.d/5936.feature
@@ -0,0 +1 @@
+Added themed launch icons for Android 13
\ No newline at end of file
diff --git a/changelog.d/5941.bugfix b/changelog.d/5941.bugfix
new file mode 100644
index 0000000000..0ea17668c6
--- /dev/null
+++ b/changelog.d/5941.bugfix
@@ -0,0 +1 @@
+If animations are disable on the System, chat effects and confetti will be disabled too
diff --git a/changelog.d/5965.sdk b/changelog.d/5965.sdk
new file mode 100644
index 0000000000..5bb6c3aac4
--- /dev/null
+++ b/changelog.d/5965.sdk
@@ -0,0 +1 @@
+Including SSL/TLS error handing when doing WellKnown lookups without a custom HomeServerConnectionConfig
diff --git a/changelog.d/5997.misc b/changelog.d/5997.misc
new file mode 100644
index 0000000000..328f3c0079
--- /dev/null
+++ b/changelog.d/5997.misc
@@ -0,0 +1 @@
+Update check for server-side threads support to match spec.
diff --git a/dependencies.gradle b/dependencies.gradle
index 7666a3bf9f..90990810a4 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -7,26 +7,26 @@ ext.versions = [
'targetCompat' : JavaVersion.VERSION_11,
]
-def gradle = "7.0.4"
+def gradle = "7.2.0"
// Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.6.0"
+def kotlin = "1.6.21"
def kotlinCoroutines = "1.6.0"
-def dagger = "2.40.5"
+def dagger = "2.42"
def retrofit = "2.9.0"
def arrow = "0.8.2"
def markwon = "4.6.2"
def moshi = "1.13.0"
-def lifecycle = "2.4.0"
+def lifecycle = "2.4.1"
def flowBinding = "1.2.0"
def epoxy = "4.6.2"
-def mavericks = "2.5.0"
-def glide = "4.12.0"
+def mavericks = "2.6.1"
+def glide = "4.13.2"
def bigImageViewer = "1.8.1"
-def jjwt = "0.11.2"
-def vanniktechEmoji = "0.8.0"
+def jjwt = "0.11.5"
+def vanniktechEmoji = "0.9.0"
// Testing
-def mockk = "1.12.1"
+def mockk = "1.12.4"
def espresso = "3.4.0"
def androidxTest = "1.4.0"
def androidxOrchestrator = "1.4.1"
@@ -45,15 +45,15 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
],
androidx : [
- 'appCompat' : "androidx.appcompat:appcompat:1.4.0",
+ 'appCompat' : "androidx.appcompat:appcompat:1.4.1",
'core' : "androidx.core:core-ktx:1.7.0",
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
- 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.0",
- 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.2",
+ 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
+ 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3",
'work' : "androidx.work:work-runtime-ktx:2.7.1",
'autoFill' : "androidx.autofill:autofill:1.1.0",
- 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1",
+ 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0",
'junit' : "androidx.test.ext:junit:1.1.3",
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
@@ -72,7 +72,7 @@ ext.libs = [
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
],
google : [
- 'material' : "com.google.android.material:material:1.5.0"
+ 'material' : "com.google.android.material:material:1.6.0"
],
dagger : [
'dagger' : "com.google.dagger:dagger:$dagger",
diff --git a/docs/ui-tests.md b/docs/ui-tests.md
index 05eb50f525..667a6ed7fb 100644
--- a/docs/ui-tests.md
+++ b/docs/ui-tests.md
@@ -176,4 +176,4 @@ class SettingsAdvancedRobot {
clickOn(R.string.settings_developer_mode_summary)
}
}
-```
\ No newline at end of file
+```
diff --git a/library/core-utils/build.gradle b/library/core-utils/build.gradle
index ad3a948808..d3afd8d29b 100644
--- a/library/core-utils/build.gradle
+++ b/library/core-utils/build.gradle
@@ -44,7 +44,7 @@ android {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += [
- "-Xopt-in=kotlin.RequiresOptIn"
+ "-opt-in=kotlin.RequiresOptIn"
]
}
}
@@ -52,4 +52,4 @@ android {
dependencies {
implementation libs.androidx.appCompat
implementation libs.jetbrains.coroutinesAndroid
-}
\ No newline at end of file
+}
diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle
index d5486911bc..2110747feb 100644
--- a/library/jsonviewer/build.gradle
+++ b/library/jsonviewer/build.gradle
@@ -6,8 +6,14 @@ apply plugin: 'com.jakewharton.butterknife'
buildscript {
repositories {
- google()
- maven { url 'https://repo1.maven.org/maven2' }
+ // Do not use `google()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://maven.google.com'
+ }
+ // Do not use `mavenCentral()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://repo1.maven.org/maven2'
+ }
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index f0a8e33124..c840b9a6e9 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -7,7 +7,10 @@ apply plugin: "org.jetbrains.dokka"
buildscript {
repositories {
- maven { url 'https://repo1.maven.org/maven2' }
+ // Do not use `mavenCentral()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://repo1.maven.org/maven2'
+ }
}
dependencies {
classpath "io.realm:realm-gradle-plugin:10.9.0"
@@ -98,6 +101,9 @@ android {
freeCompilerArgs += [
// Disabled for now, there are too many errors. Could be handled in another dedicated PR
// '-Xexplicit-api=strict', // or warning
+ "-opt-in=kotlin.RequiresOptIn",
+ // Opt in for kotlinx.coroutines.FlowPreview
+ "-opt-in=kotlinx.coroutines.FlowPreview",
]
}
@@ -175,7 +181,7 @@ dependencies {
implementation libs.arrow.instances
// olm lib is now hosted in MavenCentral
- implementation 'org.matrix.android:olm-sdk:3.2.10'
+ implementation 'org.matrix.android:olm-sdk:3.2.11'
// DI
implementation libs.dagger.dagger
diff --git a/matrix-sdk-android/docs/modules.md b/matrix-sdk-android/docs/modules.md
index edf5af64d0..b19bc73534 100644
--- a/matrix-sdk-android/docs/modules.md
+++ b/matrix-sdk-android/docs/modules.md
@@ -1,5 +1,8 @@
# Module matrix-sdk-android
+
+**Note**: You are viewing the nightly documentation of the Android Matrix SDK library. The documentation of the released library can be found here: [https://matrix-org.github.io/matrix-android-sdk2/](https://matrix-org.github.io/matrix-android-sdk2/)
+
## Welcome to the matrix-sdk-android documentation!
This pages list the complete API that this SDK is exposing to a client application.
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 4ead511c4d..dfb4863d5b 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
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.Observer
+import org.amshove.kluent.fail
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.extensions.orFalse
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.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.KeysVersion
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.VerificationMethod
@@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
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.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.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+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.SharedSecretStorageService
import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.awaitCallback
+import org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@@ -296,33 +311,94 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
}
}
+ /**
+ * Initialize cross-signing, set up megolm backup and save all in 4S
+ */
+ fun bootstrapSecurity(session: Session) {
+ initializeCrossSigning(session)
+ val ssssService = session.sharedSecretStorageService()
+ testHelper.runBlockingTest {
+ val keyInfo = ssssService.generateKey(
+ UUID.randomUUID().toString(),
+ null,
+ "ssss_key",
+ EmptyKeySigner()
+ )
+ ssssService.setDefaultKey(keyInfo.keyId)
+
+ ssssService.storeSecret(
+ MASTER_KEY_SSSS_NAME,
+ session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
+ listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ )
+
+ ssssService.storeSecret(
+ SELF_SIGNING_KEY_SSSS_NAME,
+ session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
+ listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ )
+
+ ssssService.storeSecret(
+ USER_SIGNING_KEY_SSSS_NAME,
+ session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
+ listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ )
+
+ // set up megolm backup
+ val creationInfo = awaitCallback {
+ session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
+ }
+ val version = awaitCallback {
+ session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
+ }
+ // Save it for gossiping
+ session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
+
+ extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
+ ssssService.storeSecret(
+ KEYBACKUP_SECRET_SSSS_NAME,
+ secret,
+ listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
+ )
+ }
+ }
+ }
+
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
- val requestID = UUID.randomUUID().toString()
val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService()
- aliceVerificationService.beginKeyVerificationInDMs(
- VerificationMethod.SAS,
- requestID,
- roomId,
- bob.myUserId,
- bob.sessionParams.credentials.deviceId!!
- )
+ val localId = UUID.randomUUID().toString()
+ aliceVerificationService.requestKeyVerificationInDMs(
+ localId = localId,
+ methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
+ otherUserId = bob.myUserId,
+ roomId = roomId
+ ).transactionId
- // we should reach SHOW SAS on both
- var alicePovTx: OutgoingSasVerificationTransaction? = null
- var bobPovTx: IncomingSasVerificationTransaction? = null
-
- // wait for alice to get the ready
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
- bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
- Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
- if (bobPovTx?.state == VerificationTxState.OnStarted) {
- bobPovTx?.performAccept()
+ 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
+ }
+ bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
+
+ var requestID: String? = null
+ // wait for it to be readied
+ testHelper.waitWithLatch {
+ testHelper.retryPeriodicallyWithLatch(it) {
+ val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
+ .firstOrNull { it.localId == localId }
+ if (outgoingRequest?.isReady == true) {
+ requestID = outgoingRequest.transactionId!!
true
} else {
false
@@ -330,9 +406,20 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
}
}
+ aliceVerificationService.beginKeyVerificationInDMs(
+ VerificationMethod.SAS,
+ requestID!!,
+ roomId,
+ bob.myUserId,
+ bob.sessionParams.credentials.deviceId!!)
+
+ // we should reach SHOW SAS on both
+ var alicePovTx: OutgoingSasVerificationTransaction? = null
+ var bobPovTx: IncomingSasVerificationTransaction? = null
+
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
- alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
+ alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
@@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
// wait for alice to get the ready
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
- bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
+ bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
@@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
return CryptoTestData(roomId, sessions)
}
+
+ fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) {
+ sentEventIds.forEachIndexed { index, sentEventId ->
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
+ testHelper.runBlockingTest {
+ try {
+ session.cryptoService().decryptEvent(event, "").let { result ->
+ event.mxDecryptionResult = OlmDecryptionResult(
+ payload = result.clearEvent,
+ senderKey = result.senderCurve25519Key,
+ keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+ forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+ )
+ }
+ } catch (error: MXCryptoError) {
+ // nop
+ }
+ }
+ Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
+ event.getClearType() == EventType.MESSAGE &&
+ messagesText[index] == event.getClearContent()?.toModel()?.body
+ }
+ }
+ }
+ }
+
+ fun ensureCannotDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
+ sentEventIds.forEach { sentEventId ->
+ val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
+ testHelper.runBlockingTest {
+ try {
+ session.cryptoService().decryptEvent(event, "")
+ fail("Should not be able to decrypt event")
+ } catch (error: MXCryptoError) {
+ val errorType = (error as? MXCryptoError.Base)?.errorType
+ if (expectedError == null) {
+ assertNotNull(errorType)
+ } else {
+ assertEquals("Unexpected reason", expectedError, errorType)
+ }
+ }
+ }
+ }
+ }
}
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 ed922fdddc..ebe4c5ff6f 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
@@ -30,13 +30,21 @@ 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.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.RequestResult
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.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.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.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
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
@@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
+import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2eeSanityTests : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
-
/**
* Simple test that create an e2ee room.
* Some new members are added, and a message is sent.
@@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testSendingE2EEMessages() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
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)
// add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map {
- testHelper.createAccount(it, SessionTestParams(true))
+ testHelper.createAccount(it, SessionTestParams(true)).also {
+ it.cryptoService().enableKeyGossiping(false)
+ }
}
Log.v("#E2E TEST", "All accounts created")
@@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
// All user should accept invite
otherAccounts.forEach { otherSession ->
- waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
+ waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
// check that alice see them as joined (not really necessary?)
- ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
+ 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(aliceRoomPOV, text)
+ val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text)
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null)
@@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
+ val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
@@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
}
newAccount.forEach {
- waitForAndAcceptInviteInRoom(it, e2eRoomID)
+ waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
}
- ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
+ ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit
testHelper.runBlockingTest {
@@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message"
- val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
+ val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage)
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
@@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
+ @Test
+ fun testKeyGossipingIsEnabledByDefault() {
+ val testHelper = CommonTestHelper(context())
+ val session = testHelper.createAccount("alice", SessionTestParams(true))
+ Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
+ testHelper.signOutAndClose(session)
+ }
+
/**
* Quick test for basic key backup
* 1. Create e2e between Alice and Bob
@@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testBasicBackupImport() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
@@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
val sentEventIds = mutableListOf()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text ->
- val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+ val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
+ val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
// we want more so let's discard the session
@@ -289,22 +314,23 @@ class E2eeSanityTests : InstrumentedTest {
}
}
// after initial sync events are not decrypted, so we have to try manually
- ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+ cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// Let's now import keys from backup
- newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
+ newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.doSync {
- keysBackupService.getVersion(version.version, it)
+ kbs.getVersion(version.version, it)
}
val importedResult = testHelper.doSync {
- keysBackupService.restoreKeyBackupWithPassword(
+ kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
- null, it
+ null,
+ it
)
}
@@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
}
// ensure bob can now decrypt
- ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+ cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
testHelper.signOutAndClose(newBobSession)
}
@@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testSimpleGossip() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
@@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
- cryptoTestHelper.initializeCrossSigning(bobSession)
-
// let's send a few message to bob
val sentEventIds = mutableListOf()
val messagesText = listOf("1. Hello", "2. Bob")
Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text ->
- val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+ val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
+ val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
- ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
+ ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
// Let's now add a new bob session
// Create a new session for bob
@@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
- ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+ cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
+// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
+// .firstOrNull {
+// it.sessionId ==
+// }
// Try to request
sentEventIds.forEach { sentEventId ->
@@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
}
// wait a bit
- testHelper.runBlockingTest {
- delay(10_000)
- }
+ // we need to wait a couple of syncs to let sharing occurs
+// testHelper.waitFewSyncs(newBobSession, 6)
// Ensure that new bob still can't decrypt (keys must have been withheld)
- ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
+ sentEventIds.forEach { sentEventId ->
+ val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
+ .getTimelineEvent(sentEventId)!!
+ .root.content.toModel()!!.sessionId
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ 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
+ }
+ }
+ }
+
+ cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Now mark new bob session as verified
@@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
}
- // wait a bit
- testHelper.runBlockingTest {
- delay(10_000)
- }
-
- ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+ cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(newBobSession)
@@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testForwardBetterKey() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
@@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
- cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
-
// let's send a few message to bob
var firstEventId: String
val firstMessage = "1. Hello"
Log.v("#E2E TEST", "Alice sends some messages")
firstMessage.let { text ->
- firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+ firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
+ val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
- ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
+ ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
// Let's add a new unverified session from bob
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
- ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+ cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null)
// Now let alice send a new message. this time the new bob session will be able to decrypt
var secondEventId: String
@@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages")
secondMessage.let { text ->
- secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+ secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
+ val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
@@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
try {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
fail("Should not be able to decrypt event")
- } catch (error: MXCryptoError) {
- val errorType = (error as? MXCryptoError.Base)?.errorType
- assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
+ } catch (_: MXCryptoError) {
}
}
@@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
// now let new session request
- newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
+ newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
- // wait a bit
- testHelper.runBlockingTest {
- delay(10_000)
- }
+ // We need to wait for the key request to be sent out and then a reply to be received
// old session should have shared the key at earliest known index now
// we should be able to decrypt both
- testHelper.runBlockingTest {
- try {
- newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
- } catch (error: MXCryptoError) {
- fail("Should be able to decrypt first event now $error")
- }
- }
- testHelper.runBlockingTest {
- try {
- newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
- } catch (error: MXCryptoError) {
- fail("Should be able to decrypt event $error")
+ testHelper.waitWithLatch {
+ testHelper.retryPeriodicallyWithLatch(it) {
+ val canDecryptFirst = try {
+ testHelper.runBlockingTest {
+ newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+ }
+ true
+ } catch (error: MXCryptoError) {
+ false
+ }
+ val canDecryptSecond = try {
+ testHelper.runBlockingTest {
+ newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+ }
+ true
+ } catch (error: MXCryptoError) {
+ false
+ }
+ canDecryptFirst && canDecryptSecond
}
}
- cryptoTestData.cleanUp(testHelper)
+ testHelper.signOutAndClose(aliceSession)
+ testHelper.signOutAndClose(bobSessionWithBetterKey)
testHelper.signOutAndClose(newBobSession)
}
- private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
+ private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendService().sendTextMessage(text)
var sentEventId: String? = null
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
-
testHelper.retryPeriodicallyWithLatch(latch) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
@@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
return sentEventId
}
- private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) {
+ /**
+ * Test that if a better key is forwared (lower index, it is then used)
+ */
+ @Test
+ fun testSelfInteractiveVerificationAndGossip() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = 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 oldCompleteLatch = CountDownLatch(1)
+ lateinit var oldCode: String
+ aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
+
+ override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
+ val readyInfo = pr.readyInfo
+ if (readyInfo != null) {
+ aliceSession.cryptoService().verificationService().beginKeyVerification(
+ VerificationMethod.SAS,
+ aliceSession.myUserId,
+ 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 -> {
+ // we can release this latch?
+ oldCompleteLatch.countDown()
+ }
+ else -> Unit
+ }
+ }
+ })
+
+ val newCompleteLatch = CountDownLatch(1)
+ lateinit var newCode: String
+ aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
+
+ override fun verificationRequestCreated(pr: PendingVerificationRequest) {
+ // let's ready
+ aliceNewSession.cryptoService().verificationService().readyPendingVerification(
+ listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
+ aliceSession.myUserId,
+ 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 -> {
+ newCompleteLatch.countDown()
+ }
+ else -> Unit
+ }
+ }
+ })
+
+ // initiate self verification
+ aliceSession.cryptoService().verificationService().requestKeyVerification(
+ listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
+ aliceNewSession.myUserId,
+ listOf(aliceNewSession.sessionParams.deviceId!!)
+ )
+ testHelper.await(oldCompleteLatch)
+ testHelper.await(newCompleteLatch)
+ assertEquals("Decimal code should have matched", oldCode, newCode)
+
+ // Assert that devices are verified
+ val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
+ val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(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.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
+ }
+ }
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ 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
+ )
+
+ testHelper.signOutAndClose(aliceSession)
+ testHelper.signOutAndClose(aliceNewSession)
+ }
+
+ private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
otherAccounts.map {
@@ -564,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest {
}
}
- private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
+ private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
@@ -576,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest {
}
}
- testHelper.runBlockingTest(60_000) {
+ // not sure why it's taking so long :/
+ testHelper.runBlockingTest(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
@@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
}
}
- private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) {
- sentEventIds.forEachIndexed { index, sentEventId ->
- testHelper.waitWithLatch { latch ->
- testHelper.retryPeriodicallyWithLatch(latch) {
- val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
- testHelper.runBlockingTest {
- try {
- session.cryptoService().decryptEvent(event, "").let { result ->
- event.mxDecryptionResult = OlmDecryptionResult(
- payload = result.clearEvent,
- senderKey = result.senderCurve25519Key,
- keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
- )
- }
- } catch (error: MXCryptoError) {
- // nop
- }
- }
- event.getClearType() == EventType.MESSAGE &&
- messagesText[index] == event.getClearContent()?.toModel()?.body
- }
- }
- }
- }
-
- private fun ensureIsDecrypted(sentEventIds: List, session: Session, e2eRoomID: String) {
+ private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
- timelineEvent != null &&
- timelineEvent.isEncrypted() &&
- timelineEvent.root.getClearType() == EventType.MESSAGE
- }
- }
- }
- }
-
- private fun ensureCannotDecrypt(sentEventIds: List, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
- sentEventIds.forEach { sentEventId ->
- val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
- testHelper.runBlockingTest {
- try {
- newBobSession.cryptoService().decryptEvent(event, "")
- fail("Should not be able to decrypt event")
- } catch (error: MXCryptoError) {
- val errorType = (error as? MXCryptoError.Base)?.errorType
- if (expectedError == null) {
- Assert.assertNotNull(errorType)
- } else {
- assertEquals(expectedError, errorType, "Message expected to be UISI")
- }
+ val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
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
index 8a1edec5e3..93aa78a305 100644
--- 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
@@ -27,7 +27,6 @@ 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.content.RoomKeyContent
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
@@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
// clear any outbound session
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
- val preShareCount = bobSession.cryptoService().getGossipingEvents().count {
- it.senderId == aliceSession.myUserId &&
- it.getClearType() == EventType.ROOM_KEY
- }
+ 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")
@@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
- val newGossipCount = bobSession.cryptoService().getGossipingEvents().count {
- it.senderId == aliceSession.myUserId &&
- it.getClearType() == EventType.ROOM_KEY
- }
- newGossipCount > preShareCount
+ val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
+ newKeysCount > preShareCount
}
}
- val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull {
- it.senderId == aliceSession.myUserId &&
- it.getClearType() == EventType.ROOM_KEY
- }
+ val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
+ val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
- val content = latest?.getClearContent().toModel()
- assertNotNull("Bob should have received and decrypted a room key event from alice", content)
- assertEquals("Wrong room", e2eRoomID, content!!.roomId)
- val megolmSessionId = content.sessionId!!
+ 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.olmInboundGroupSession!!.sessionIdentifier()
+
+ assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
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 5066a4339f..2e4fd62822 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,59 +19,45 @@ 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 junit.framework.TestCase.fail
+import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
+import org.junit.Assert.assertNull
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.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.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
+import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
-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.api.session.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
-import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
-import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
-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.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
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.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
-import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class KeyShareTests : InstrumentedTest {
- private val commonTestHelper = CommonTestHelper(context())
-
@Test
- @Ignore("This test will be ignored until it is fixed")
fun test_DoNotSelfShareIfNotTrusted() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
+ Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message
val roomId = commonTestHelper.runBlockingTest {
@@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(room)
Thread.sleep(4_000)
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
- val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
- // Open a new sessionx
+ val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first()
+ val sentEventId = sentEvent.eventId
+ val sentEventText = sentEvent.getLastMessageContent()?.body
- val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
+ // Open a new session
+ val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
+ // block key requesting for now as decrypt will send requests (room summary is trying to decrypt)
+ aliceSession2.cryptoService().enableKeyGossiping(false)
+ commonTestHelper.syncSession(aliceSession2)
+
+ Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
@@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
}
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
+ assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
+
// Try to request
+ aliceSession2.cryptoService().enableKeyGossiping(true)
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId
@@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
- .filter { req ->
- // filter out request that was known before
- !outgoingRequestsBefore.any { req.requestId == it.requestId }
- }
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
outGoingRequestId = outgoing?.requestId
@@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
- aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
- Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
+// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
+// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
+// Log.v("TEST", "=========================")
+// it.forEach { keyRequest ->
+// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
+// }
+// Log.v("TEST", "=========================")
+// }
+
+ val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
+ incoming != null
+ }
+ }
+
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ // DEBUG LOGS
+ aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
- it.forEach { keyRequest ->
- Log.v(
- "TEST",
- "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
- )
- }
+ Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
+ Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "=========================")
}
- val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
- incoming?.state == GossipingRequestState.REJECTED
+ val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
+ val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ val resultCode = (reply?.result as? RequestResult.Failure)?.code
+ resultCode == WithHeldCode.UNVERIFIED
}
}
@@ -175,254 +181,301 @@ class KeyShareTests : InstrumentedTest {
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
- commonTestHelper.waitWithLatch { latch ->
- commonTestHelper.retryPeriodicallyWithLatch(latch) {
- aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
- Log.v("TEST", "Incoming request Session 1")
- Log.v("TEST", "=========================")
- it.forEach {
- Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
- }
- Log.v("TEST", "=========================")
-
- it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
- }
- }
- }
-
- Thread.sleep(6_000)
- commonTestHelper.waitWithLatch { latch ->
- commonTestHelper.retryPeriodicallyWithLatch(latch) {
- aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
- it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
- }
- }
- }
-
- try {
- commonTestHelper.runBlockingTest {
- aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
- }
- } catch (failure: Throwable) {
- fail("should have been able to decrypt")
- }
+ cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(aliceSession2)
}
+ // See E2ESanityTest for a test regarding secret sharing
+
+ /**
+ * Test that the sender of a message accepts to re-share to another user
+ * if the key was originally shared with him
+ */
@Test
- @Ignore("This test will be ignored until it is fixed")
- fun test_ShareSSSSSecret() {
- val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
+ fun test_reShareIfWasIntendedToBeShared() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
- commonTestHelper.doSync {
- aliceSession1.cryptoService().crossSigningService()
- .initializeCrossSigning(
- object : UserInteractiveAuthInterceptor {
- override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) {
- promise.resume(
- UserPasswordAuth(
- user = aliceSession1.myUserId,
- password = TestConstants.PASSWORD
- )
- )
- }
- }, it
- )
- }
+ val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = testData.firstSession
+ val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
+ val bobSession = testData.secondSession!!
- // Also bootstrap keybackup on first session
- val creationInfo = commonTestHelper.doSync {
- aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
- }
- val version = commonTestHelper.doSync {
- aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
- }
- // Save it for gossiping
- aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
+ val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
+ val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!!
- val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
+ // bob should be able to decrypt
+ cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: ""))
- val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
- val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
-
- // force keys download
- commonTestHelper.doSync> {
- aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
- }
- commonTestHelper.doSync> {
- aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
- }
-
- var session1ShortCode: String? = null
- var session2ShortCode: String? = null
-
- aliceVerificationService1.addListener(object : VerificationService.Listener {
- override fun transactionUpdated(tx: VerificationTransaction) {
- Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
- if (tx is SasVerificationTransaction) {
- if (tx.state == VerificationTxState.OnStarted) {
- (tx as IncomingSasVerificationTransaction).performAccept()
- }
- if (tx.state == VerificationTxState.ShortCodeReady) {
- session1ShortCode = tx.getDecimalCodeRepresentation()
- Thread.sleep(500)
- tx.userHasVerifiedShortCode()
- }
- }
- }
- })
-
- aliceVerificationService2.addListener(object : VerificationService.Listener {
- override fun transactionUpdated(tx: VerificationTransaction) {
- Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
- if (tx is SasVerificationTransaction) {
- if (tx.state == VerificationTxState.ShortCodeReady) {
- session2ShortCode = tx.getDecimalCodeRepresentation()
- Thread.sleep(500)
- tx.userHasVerifiedShortCode()
- }
- }
- }
- })
-
- val txId = "m.testVerif12"
- aliceVerificationService2.beginKeyVerification(
- VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
- ?: "", txId
- )
+ // Let's try to request any how.
+ // As it was share previously alice should accept to reshare
+ bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
- aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
+ val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ aliceReply != null && aliceReply.result is RequestResult.Success
}
}
-
- assertNotNull(session1ShortCode)
- Log.d("#TEST", "session1ShortCode: $session1ShortCode")
- assertNotNull(session2ShortCode)
- Log.d("#TEST", "session2ShortCode: $session2ShortCode")
- assertEquals(session1ShortCode, session2ShortCode)
-
- // SSK and USK private keys should have been shared
-
- commonTestHelper.waitWithLatch(60_000) { latch ->
- commonTestHelper.retryPeriodicallyWithLatch(latch) {
- Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
- aliceSession2.cryptoService().crossSigningService().canCrossSign()
- }
- }
-
- // Test that key backup key has been shared to
- commonTestHelper.waitWithLatch(60_000) { latch ->
- val keysBackupService = aliceSession2.cryptoService().keysBackupService()
- commonTestHelper.retryPeriodicallyWithLatch(latch) {
- Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
- keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
- }
- }
-
- commonTestHelper.signOutAndClose(aliceSession1)
- commonTestHelper.signOutAndClose(aliceSession2)
}
+ /**
+ * Test that our own devices accept to reshare to unverified device if it was shared initialy
+ * if the key was originally shared with him
+ */
@Test
- @Ignore("This test will be ignored until it is fixed")
- fun test_ImproperKeyShareBug() {
- val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
+ fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
- commonTestHelper.doSync {
- aliceSession.cryptoService().crossSigningService()
- .initializeCrossSigning(
- object : UserInteractiveAuthInterceptor {
- override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) {
- promise.resume(
- UserPasswordAuth(
- user = aliceSession.myUserId,
- password = TestConstants.PASSWORD,
- session = flowResponse.session
- )
- )
- }
- }, it
- )
+ val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
+ val aliceSession = testData.firstSession
+ val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
+
+ val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
+
+ // we wait for alice first session to be aware of that session?
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
+ .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+ newSession != null
+ }
}
+ val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
+ val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!!
- // Create an encrypted room and send a couple of messages
- val roomId = commonTestHelper.runBlockingTest {
- aliceSession.roomService().createRoom(
- CreateRoomParams().apply {
- visibility = RoomDirectoryVisibility.PRIVATE
- enableEncryption()
- }
- )
+ // Let's try to request any how.
+ // As it was share previously alice should accept to reshare
+ aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
+
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val ownDeviceReply =
+ outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
+ }
}
- val roomAlicePov = aliceSession.getRoom(roomId)
- assertNotNull(roomAlicePov)
- Thread.sleep(1_000)
- assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true)
- val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
+ }
- // Create bob session
+ /**
+ * Tests that keys reshared with own verified session are done from the earliest known index
+ */
+ @Test
+ fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
- val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
- commonTestHelper.doSync {
- 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
- )
- }
+ val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = testData.firstSession
+ val bobSession = testData.secondSession!!
+ val roomFromBob = bobSession.getRoom(testData.roomId)!!
- // Let alice invite bob
- commonTestHelper.runBlockingTest {
- roomAlicePov.membershipService().invite(bobSession.myUserId, null)
- }
+ val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
+ val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!!
- commonTestHelper.runBlockingTest {
- bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList())
- }
+ // Let alice now add a new session
+ val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
+ aliceNewSession.cryptoService().enableKeyGossiping(false)
+ commonTestHelper.syncSession(aliceNewSession)
- // we want to discard alice outbound session
- aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
-
- // and now resend a new message to reset index to 0
- commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
-
- val roomRoomBobPov = aliceSession.getRoom(roomId)
- val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
-
- var dRes = tryOrNull {
- commonTestHelper.runBlockingTest {
- bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
+ // we wait bob first session to be aware of that session?
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
+ .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+ newSession != null
}
}
- assert(dRes == null)
+ val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
+ val newEventId = newEvent.eventId
+ val newEventText = newEvent.getLastMessageContent()!!.body
- // Try to re-ask the keys
+ // alice should be able to decrypt the new one
+ cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
+ // but not the first one!
+ cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
- bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
+ // All should be using the same session id
+ sentEvents.forEach {
+ assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId)
+ }
+ assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId)
- Thread.sleep(3_000)
+ // Request a first time, bob should reply with unauthorized and alice should reply with unverified
+ aliceNewSession.cryptoService().enableKeyGossiping(true)
+ aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
- // With the bug the first session would have improperly reshare that key :/
- dRes = tryOrNull {
- commonTestHelper.runBlockingTest {
- bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val ownDeviceReply = outgoing?.results
+ ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ val result = ownDeviceReply?.result
+ Log.v("TEST", "own device result is $result")
+ result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
}
}
- Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}")
- assert(dRes?.clearEvent == null)
+
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val bobDeviceReply = outgoing?.results
+ ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
+ val result = bobDeviceReply?.result
+ Log.v("TEST", "bob device result is $result")
+ result != null && result is RequestResult.Success && result.chainIndex > 0
+ }
+ }
+
+ // it's a success but still can't decrypt first message
+ cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
+
+ // Mark the new session as verified
+ aliceSession.cryptoService()
+ .verificationService()
+ .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
+
+ // Let's now try to request
+ aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
+
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ // DEBUG LOGS
+ aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
+ Log.v("TEST", "=========================")
+ Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
+ Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
+ Log.v("TEST", "=========================")
+ }
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val ownDeviceReply =
+ outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ val result = ownDeviceReply?.result
+ result != null && result is RequestResult.Success && result.chainIndex == 0
+ }
+ }
+
+ // now the new session should be able to decrypt all!
+ cryptoTestHelper.ensureCanDecrypt(
+ sentEvents.map { it.eventId },
+ aliceNewSession,
+ testData.roomId,
+ sentEvents.map { it.getLastMessageContent()!!.body }
+ )
+
+ // Additional test, can we check that bob replied successfully but with a ratcheted key
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
+ val result = bobReply?.result
+ result != null && result is RequestResult.Success && result.chainIndex == 3
+ }
+ }
+
+ 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() {
+ val commonTestHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
+ val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+ val aliceSession = testData.firstSession
+ val bobSession = testData.secondSession!!
+ val roomFromBob = bobSession.getRoom(testData.roomId)!!
+
+ val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
+ val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!!
+
+ // Let alice now add a new session
+ val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
+
+ // we wait bob first session to be aware of that session?
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
+ .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+ newSession != null
+ }
+ }
+
+ val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
+ val newEventId = newEvent.eventId
+ val newEventText = newEvent.getLastMessageContent()!!.body
+
+ // alice should be able to decrypt the new one
+ cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
+ // but not the first one!
+ cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
+
+ // All should be using the same session id
+ sentEvents.forEach {
+ assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId)
+ }
+ assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId)
+
+ // Mark the new session as verified
+ aliceSession.cryptoService()
+ .verificationService()
+ .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
+
+ // /!\ Stop initial alice session syncing so that it can't reply
+ aliceSession.cryptoService().enableKeyGossiping(false)
+ aliceSession.stopSync()
+
+ // Let's now try to request
+ aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
+
+ // Should get a reply from bob and not from alice
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
+ val result = bobReply?.result
+ result != null && result is RequestResult.Success && result.chainIndex == 3
+ }
+ }
+
+ val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+
+ assertNull("We should not have a reply from first session", outgoingReq!!.results.firstOrNull { it.fromDevice == aliceSession.sessionParams.deviceId })
+ assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state)
+
+ // let's wake up alice
+ aliceSession.cryptoService().enableKeyGossiping(true)
+ aliceSession.startSync(true)
+
+ // We should now get a reply from first session
+ commonTestHelper.waitWithLatch { latch ->
+ commonTestHelper.retryPeriodicallyWithLatch(latch) {
+ val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+ val ownDeviceReply =
+ outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+ val result = ownDeviceReply?.result
+ result != null && result is RequestResult.Success && result.chainIndex == 0
+ }
+ }
+
+ // It should be in sent then cancel
+ 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)
}
}
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 b3896b02de..cb31a2232f 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
@@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Assert
import org.junit.FixMethodOrder
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -29,6 +28,7 @@ import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.RequestResult
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.WithHeldCode
@@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants
@LargeTest
class WithHeldTests : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
-
@Test
- @Ignore("This test will be ignored until it is fixed")
fun test_WithHeldUnverifiedReason() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
// =============================
// ARRANGE
// =============================
@@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest {
val roomAlicePOV = aliceSession.getRoom(roomId)!!
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
-
// =============================
// ACT
// =============================
@@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest {
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
+ val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!!
// =============================
// ASSERT
// =============================
@@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
- Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
+ Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
}
+ // Let's see if the reply we got from bob first session is unverified
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ 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)
@@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
- Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
+ Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
}
testHelper.signOutAndClose(aliceSession)
@@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest {
}
@Test
- @Ignore("This test will be ignored until it is fixed")
fun test_WithHeldNoOlm() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
@@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest {
}
@Test
- @Ignore("This test will be ignored until it is fixed")
fun test_WithHeldKeyRequest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
@@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest {
wc?.code == WithHeldCode.UNAUTHORISED
}
}
+
+ testHelper.signOutAndClose(aliceSession)
+ testHelper.signOutAndClose(bobSecondSession)
}
}
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 6097bf8c93..df3b2ffe27 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
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -40,7 +39,6 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
-@Ignore("This test is flaky ; see issue #5449")
class VerificationTest : InstrumentedTest {
data class ExpectedResult(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
index 9a686de2e1..9507ddda65 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
@@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
* If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user.
*/
- val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
+ val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true,
+
+ /**
+ * Currently megolm keys are requested to the sender device and to all of our devices.
+ * You can limit request only to your sessions by turning this setting to `true`
+ */
+ val limitRoomKeyRequestsToMyDevices: Boolean = false,
)
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 d6d1248de7..b8c08d23dc 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
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
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
@@ -35,8 +36,6 @@ 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.OutgoingRoomKeyRequest
-import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
@@ -76,6 +75,15 @@ interface CryptoService {
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
+
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int
@@ -94,8 +102,6 @@ interface CryptoService {
fun reRequestRoomKeyForEvent(event: Event)
- fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
-
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
@@ -142,14 +148,20 @@ interface CryptoService {
fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)
- fun getOutgoingRoomKeyRequests(): List
- fun getOutgoingRoomKeyRequestsPaged(): LiveData>
+ fun getOutgoingRoomKeyRequests(): List
+ fun getOutgoingRoomKeyRequestsPaged(): LiveData>
fun getIncomingRoomKeyRequests(): List
fun getIncomingRoomKeyRequestsPaged(): LiveData>
- fun getGossipingEventsTrail(): LiveData>
- fun getGossipingEvents(): List
+ /**
+ * Can be called by the app layer to accept a request manually
+ * Use carefully as it is prone to social attacks
+ */
+ suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest)
+
+ fun getGossipingEventsTrail(): LiveData>
+ fun getGossipingEvents(): List
// For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt
new file mode 100644
index 0000000000..855f17a34f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.api.session.crypto
+
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
+
+data class RequestReply(
+ val userId: String,
+ val fromDevice: String?,
+ val result: RequestResult
+)
+
+sealed class RequestResult {
+ data class Success(val chainIndex: Int) : RequestResult()
+ data class Failure(val code: WithHeldCode) : RequestResult()
+}
+
+data class OutgoingKeyRequest(
+ var requestBody: RoomKeyRequestBody?,
+ // recipients for the request map of users to list of deviceId
+ val recipients: Map>,
+ val fromIndex: Int,
+ // Unique id for this request. Used for both
+ // an id within the request for later pairing with a cancellation, and for
+ // the transaction id when sending the to_device messages to our local
+ val requestId: String, // current state of this request
+ val state: OutgoingRoomKeyRequestState,
+ val results: List
+) {
+ /**
+ * Used only for log.
+ *
+ * @return the room id.
+ */
+ val roomId = requestBody?.roomId
+
+ /**
+ * Used only for log.
+ *
+ * @return the session id
+ */
+ val sessionId = requestBody?.sessionId
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt
similarity index 57%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt
index 8c1bdf6768..6e80bdc133 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.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,14 +14,20 @@
* limitations under the License.
*/
-package org.matrix.android.sdk.api.session.crypto.model
+package org.matrix.android.sdk.api.session.crypto
-enum class OutgoingGossipingRequestState {
+enum class OutgoingRoomKeyRequestState {
UNSENT,
- SENDING,
SENT,
- CANCELLING,
- CANCELLED,
- FAILED_TO_SEND,
- FAILED_TO_CANCEL
+ SENT_THEN_CANCELED,
+ CANCELLATION_PENDING,
+ CANCELLATION_PENDING_AND_WILL_RESEND;
+
+ companion object {
+ fun pendingStates() = setOf(
+ UNSENT,
+ CANCELLATION_PENDING_AND_WILL_RESEND,
+ CANCELLATION_PENDING
+ )
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
index 3cd36c2ce8..24d3cf4004 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
@@ -16,9 +16,8 @@
package org.matrix.android.sdk.api.session.crypto.keyshare
-import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
-import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
/**
* Room keys events listener
@@ -35,12 +34,12 @@ interface GossipingRequestListener {
* Returns the secret value to be shared
* @return true if is handled
*/
- fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean
+ fun onSecretShareRequest(request: SecretShareRequest): Boolean
/**
* A room key request cancellation has been received.
*
* @param request the cancellation request
*/
- fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
+ fun onRequestCancelled(request: IncomingRoomKeyRequest)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt
new file mode 100644
index 0000000000..861f3bd30b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.model
+
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
+
+enum class TrailType {
+ OutgoingKeyForward,
+ IncomingKeyForward,
+ OutgoingKeyWithheld,
+ IncomingKeyRequest,
+ Unknown
+}
+
+interface AuditInfo {
+ val roomId: String
+ val sessionId: String
+ val senderKey: String
+ val alg: String
+ val userId: String
+ val deviceId: String
+}
+
+@JsonClass(generateAdapter = true)
+data class ForwardInfo(
+ override val roomId: String,
+ override val sessionId: String,
+ override val senderKey: String,
+ override val alg: String,
+ override val userId: String,
+ override val deviceId: String,
+ val chainIndex: Long?
+) : AuditInfo
+
+object UnknownInfo : AuditInfo {
+ override val roomId: String = ""
+ override val sessionId: String = ""
+ override val senderKey: String = ""
+ override val alg: String = ""
+ override val userId: String = ""
+ override val deviceId: String = ""
+}
+
+@JsonClass(generateAdapter = true)
+data class WithheldInfo(
+ override val roomId: String,
+ override val sessionId: String,
+ override val senderKey: String,
+ override val alg: String,
+ val code: WithHeldCode,
+ override val userId: String,
+ override val deviceId: String
+) : AuditInfo
+
+@JsonClass(generateAdapter = true)
+data class IncomingKeyRequestInfo(
+ override val roomId: String,
+ override val sessionId: String,
+ override val senderKey: String,
+ override val alg: String,
+ override val userId: String,
+ override val deviceId: String,
+ val requestId: String
+) : AuditInfo
+
+data class AuditTrail(
+ val ageLocalTs: Long,
+ val type: TrailType,
+ val info: AuditInfo
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt
deleted file mode 100755
index ad11ef9a5e..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.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.crypto.model
-
-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.internal.crypto.IncomingShareRequestCommon
-import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
-
-/**
- * IncomingRequestCancellation describes the incoming room key cancellation.
- */
-data class IncomingRequestCancellation(
- /**
- * The user id
- */
- override val userId: String? = null,
-
- /**
- * The device id
- */
- override val deviceId: String? = null,
-
- /**
- * The request id
- */
- override val requestId: String? = null,
- override val localCreationTimestamp: Long?
-) : IncomingShareRequestCommon {
- companion object {
- /**
- * Factory
- *
- * @param event the event
- * @param currentTimeMillis the current time in milliseconds
- */
- fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRequestCancellation? {
- return event.getClearContent()
- .toModel()
- ?.let {
- IncomingRequestCancellation(
- userId = event.senderId,
- deviceId = it.requestingDeviceId,
- requestId = it.requestId,
- localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
- )
- }
- }
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
index 0b2c32284b..0a28478a10 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
@@ -16,9 +16,7 @@
package org.matrix.android.sdk.api.session.crypto.model
-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.internal.crypto.IncomingShareRequestCommon
+import org.matrix.android.sdk.internal.util.time.Clock
/**
* IncomingRoomKeyRequest class defines the incoming room keys request.
@@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest(
/**
* The user id
*/
- override val userId: String? = null,
+ val userId: String? = null,
/**
* The device id
*/
- override val deviceId: String? = null,
+ val deviceId: String? = null,
/**
* The request id
*/
- override val requestId: String? = null,
+ val requestId: String? = null,
/**
* The request body
*/
val requestBody: RoomKeyRequestBody? = null,
- val state: GossipingRequestState = GossipingRequestState.NONE,
-
- /**
- * The runnable to call to accept to share the keys
- */
- @Transient
- var share: Runnable? = null,
-
- /**
- * The runnable to call to ignore the key share request.
- */
- @Transient
- var ignore: Runnable? = null,
- override val localCreationTimestamp: Long?
-) : IncomingShareRequestCommon {
+ val localCreationTimestamp: Long?
+) {
companion object {
/**
* Factory
@@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest(
* @param event the event
* @param currentTimeMillis the current time in milliseconds
*/
- fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? {
- return event.getClearContent()
- .toModel()
+ fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
+ return trail
+ .takeIf { it.type == TrailType.IncomingKeyRequest }
+ ?.let {
+ it.info as? IncomingKeyRequestInfo
+ }
?.let {
IncomingRoomKeyRequest(
- userId = event.senderId,
- deviceId = it.requestingDeviceId,
+ userId = it.userId,
+ deviceId = it.deviceId,
requestId = it.requestId,
- requestBody = it.body ?: RoomKeyRequestBody(),
- localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
+ requestBody = RoomKeyRequestBody(
+ algorithm = it.alg,
+ roomId = it.roomId,
+ senderKey = it.senderKey,
+ sessionId = it.sessionId
+ ),
+ localCreationTimestamp = trail.ageLocalTs
)
}
}
+
+ internal fun fromRestRequest(senderId: String, request: RoomKeyShareRequest, clock: Clock): IncomingRoomKeyRequest? {
+ return IncomingRoomKeyRequest(
+ userId = senderId,
+ deviceId = request.requestingDeviceId,
+ requestId = request.requestId,
+ requestBody = request.body,
+ localCreationTimestamp = clock.epochMillis()
+ )
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt
deleted file mode 100755
index 80f70c83f3..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.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.api.session.crypto.model
-
-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.internal.crypto.IncomingShareRequestCommon
-
-/**
- * IncomingSecretShareRequest class defines the incoming secret keys request.
- */
-data class IncomingSecretShareRequest(
- /**
- * The user id
- */
- override val userId: String? = null,
-
- /**
- * The device id
- */
- override val deviceId: String? = null,
-
- /**
- * The request id
- */
- override val requestId: String? = null,
-
- /**
- * The request body
- */
- val secretName: String? = null,
-
- /**
- * The runnable to call to accept to share the keys
- */
- @Transient
- var share: ((String) -> Unit)? = null,
-
- /**
- * The runnable to call to ignore the key share request.
- */
- @Transient
- var ignore: Runnable? = null,
-
- override val localCreationTimestamp: Long?
-
-) : IncomingShareRequestCommon {
- companion object {
- /**
- * Factory
- *
- * @param event the event
- * @param currentTimeMillis the current time in milliseconds
- */
- fun fromEvent(event: Event, currentTimeMillis: Long): IncomingSecretShareRequest? {
- return event.getClearContent()
- .toModel()
- ?.let {
- IncomingSecretShareRequest(
- userId = event.senderId,
- deviceId = it.requestingDeviceId,
- requestId = it.requestId,
- secretName = it.secretName,
- localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
- )
- }
- }
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt
deleted file mode 100755
index 5f35cc908f..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.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.api.session.crypto.model
-
-import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
-
-/**
- * Represents an outgoing room key request
- */
-@JsonClass(generateAdapter = true)
-data class OutgoingRoomKeyRequest(
- // RequestBody
- val requestBody: RoomKeyRequestBody?,
- // list of recipients for the request
- override val recipients: Map>,
- // Unique id for this request. Used for both
- // an id within the request for later pairing with a cancellation, and for
- // the transaction id when sending the to_device messages to our local
- override val requestId: String, // current state of this request
- override val state: OutgoingGossipingRequestState
- // transaction id for the cancellation, if any
- // override var cancellationTxnId: String? = null
-) : OutgoingGossipingRequest {
-
- /**
- * Used only for log.
- *
- * @return the room id.
- */
- val roomId: String?
- get() = requestBody?.roomId
-
- /**
- * Used only for log.
- *
- * @return the session id
- */
- val sessionId: String?
- get() = requestBody?.sessionId
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
index a577daf9e4..1eac1d6b2d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
@@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent(
/**
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
*/
- @Json(name = "reason") val reason: String? = null
+ @Json(name = "reason") val reason: String? = null,
+
+ /**
+ * the device ID of the device sending the m.room_key.withheld message
+ * MSC3735
+ */
+ @Json(name = "from_device") val fromDevice: String? = null
) {
val code: WithHeldCode?
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 721a2bc8af..3bb8fad810 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
@@ -131,7 +131,7 @@ interface SharedSecretStorageService {
fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult
- fun requestSecret(name: String, myOtherDeviceId: String)
+ suspend fun requestSecret(name: String, myOtherDeviceId: String)
data class KeyRef(
val keyId: String?,
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 946f882f1a..f1cfe3fee5 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
@@ -382,11 +382,16 @@ internal class DefaultAuthenticationService @Inject constructor(
return getWellknownTask.execute(
GetWellknownTask.Params(
domain = matrixId.getDomain(),
- homeServerConnectionConfig = homeServerConnectionConfig
+ homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults()
)
)
}
+ private fun HomeServerConnectionConfig?.orWellKnownDefaults() = this ?: HomeServerConnectionConfig.Builder()
+ // server uri is ignored when doing a wellknown lookup as we use the matrix id domain instead
+ .withHomeServerUri("https://dummy.org")
+ .build()
+
override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
matrixId: String,
password: String,
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 d07d5ecd64..203dbcc60e 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
@@ -74,8 +74,8 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
* Indicate if the homeserver support MSC3440 for threads
*/
internal fun Versions.doesServerSupportThreads(): Boolean {
- return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
- unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
+ // TODO: Check for v1.3 or whichever spec version formally specifies MSC3440.
+ return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt
deleted file mode 100644
index aaf23d17b3..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt
+++ /dev/null
@@ -1,123 +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.work.WorkerParameters
-import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.api.auth.data.Credentials
-import org.matrix.android.sdk.api.failure.shouldBeRetried
-import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
-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.internal.SessionManager
-import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
-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.SessionComponent
-import org.matrix.android.sdk.internal.util.time.Clock
-import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
-import org.matrix.android.sdk.internal.worker.SessionWorkerParams
-import javax.inject.Inject
-
-internal class CancelGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
- SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) {
-
- @JsonClass(generateAdapter = true)
- internal data class Params(
- override val sessionId: String,
- val requestId: String,
- val recipients: Map>,
- // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
- // to use the same value if this worker is retried.
- val txnId: String? = null,
- override val lastFailureMessage: String? = null
- ) : SessionWorkerParams {
- companion object {
- fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
- return Params(
- sessionId = sessionId,
- requestId = request.requestId,
- recipients = request.recipients,
- txnId = createUniqueTxnId(),
- lastFailureMessage = null
- )
- }
- }
- }
-
- @Inject lateinit var sendToDeviceTask: SendToDeviceTask
- @Inject lateinit var cryptoStore: IMXCryptoStore
- @Inject lateinit var credentials: Credentials
- @Inject lateinit var clock: Clock
-
- override fun injectWith(injector: SessionComponent) {
- injector.inject(this)
- }
-
- override suspend fun doSafeWork(params: Params): Result {
- // params.txnId should be provided in all cases now. But Params can be deserialized by
- // the WorkManager from data serialized in a previous version of the application, so without the txnId field.
- // So if not present, we create a txnId
- val txnId = params.txnId ?: createUniqueTxnId()
- val contentMap = MXUsersDevicesMap()
- val toDeviceContent = ShareRequestCancellation(
- requestingDeviceId = credentials.deviceId,
- requestId = params.requestId
- )
- cryptoStore.saveGossipingEvent(Event(
- type = EventType.ROOM_KEY_REQUEST,
- content = toDeviceContent.toContent(),
- senderId = credentials.userId
- ).also {
- it.ageLocalTs = clock.epochMillis()
- })
-
- params.recipients.forEach { userToDeviceMap ->
- userToDeviceMap.value.forEach { deviceId ->
- contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
- }
- }
-
- try {
- cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
- sendToDeviceTask.execute(
- SendToDeviceTask.Params(
- eventType = EventType.ROOM_KEY_REQUEST,
- contentMap = contentMap,
- transactionId = txnId
- )
- )
- cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
- return Result.success()
- } catch (throwable: Throwable) {
- return if (throwable.shouldBeRetried()) {
- Result.retry()
- } else {
- cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
- buildErrorResult(params, throwable.localizedMessage ?: "error")
- }
- }
- }
-
- 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/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index cb65ede1a6..fd4bf6adfd 100755
--- 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
@@ -42,12 +42,14 @@ 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.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
@@ -57,15 +59,13 @@ 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.OutgoingRoomKeyRequest
-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.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.EncryptedEventContent
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
@@ -76,11 +76,9 @@ 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.IMXWithHeldExtension
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.crosssigning.DefaultCrossSigningService
-import org.matrix.android.sdk.internal.crypto.dehydration.DehydrationManager
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.toRest
@@ -92,6 +90,7 @@ 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
@@ -157,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
private val crossSigningService: DefaultCrossSigningService,
//
- private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
+ private val incomingKeyRequestManager: IncomingKeyRequestManager,
+ private val secretShareManager: SecretShareManager,
//
- private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
+ private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
// Actions
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val megolmSessionDataImporter: MegolmSessionDataImporter,
@@ -179,7 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor,
- private val dehydrationManager: DehydrationManager,
+ private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy
) : CryptoService {
@@ -194,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor(
}
}
- fun onLiveEvent(roomId: String, event: Event) {
+ fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
// handle state events
if (event.isStateEvent()) {
when (event.type) {
@@ -203,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
+
+ // handle verification
+ if (!isInitialSync) {
+ if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
+ verificationMessageProcessor.process(event)
+ }
+ }
+ }
}
- val gossipingBuffer = mutableListOf()
+// val gossipingBuffer = mutableListOf()
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) {
setDeviceNameTask
@@ -381,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
// Open the store
cryptoStore.open()
- runCatching {
-// if (isInitialSync) {
-// // refresh the devices list for each known room members
-// deviceListManager.invalidateAllDeviceLists()
-// deviceListManager.refreshOutdatedDeviceLists()
-// } else {
-
- // Why would we do that? it will be called at end of syn
- incomingGossipingRequestManager.processReceivedGossipingRequests()
-// }
- }.fold(
- {
- isStarting.set(false)
- isStarted.set(true)
- },
- {
- isStarting.set(false)
- isStarted.set(false)
- Timber.tag(loggerTag.value).e(it, "Start failed")
- }
- )
+ isStarting.set(false)
+ isStarted.set(true)
}
/**
@@ -409,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
- incomingGossipingRequestManager.close()
+ incomingKeyRequestManager.close()
+ outgoingKeyRequestManager.close()
olmDevice.release()
cryptoStore.close()
}
@@ -474,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
- incomingGossipingRequestManager.processReceivedGossipingRequests()
}
- }
- tryOrNull {
- gossipingBuffer.toList().let {
- cryptoStore.saveGossipingEvents(it)
+ // 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")
}
- gossipingBuffer.clear()
}
}
}
@@ -596,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
- if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
+ if (existingAlgorithm == algorithm) {
// ignore
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
return false
@@ -744,11 +748,6 @@ internal class DefaultCryptoService @Inject constructor(
*/
@Throws(MXCryptoError::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
- Timber.i("-------> Dehydrate device will be called here")
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- dehydrationManager.dehydrateDevice("asd2343eae21eadsada".toByteArray())
- }
-
return internalDecryptEvent(event, timeline)
}
@@ -794,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
- gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event)
}
- EventType.REQUEST_SECRET,
+ EventType.REQUEST_SECRET -> {
+ secretShareManager.handleSecretRequest(event)
+ }
EventType.ROOM_KEY_REQUEST -> {
- // save audit trail
- gossipingBuffer.add(event)
- // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
- incomingGossipingRequestManager.onGossipingRequestEvent(event)
+ 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 -> {
- gossipingBuffer.add(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
@@ -844,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
val withHeldContent = event.getClearContent().toModel() ?: return Unit.also {
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
}
- Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
- val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
- if (alg is IMXWithHeldExtension) {
- alg.onRoomKeyWithHeldEvent(withHeldContent)
- } else {
- Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
- return
+ 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,
+ senderId = senderId,
+ content = event.getClearContent()
+ )
+ )
}
- private fun onSecretSendReceived(event: Event) {
- Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
- if (!event.isEncrypted()) {
- // secret send messages must be encrypted
- Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
- return
- }
-
- // Was that sent by us?
- if (event.senderId != userId) {
- Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
- return
- }
-
- val secretContent = event.getClearContent().toModel() ?: return
-
- val existingRequest = cryptoStore
- .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
-
- if (existingRequest == null) {
- Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
- return
- }
-
- if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
- // TODO Ask to application layer?
- Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
+ 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 {
+ private fun handleSDKLevelGossip(secretName: String?,
+ secretValue: String): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
@@ -1102,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
}
+ override fun enableKeyGossiping(enable: Boolean) {
+ cryptoStore.enableKeyGossiping(enable)
+ }
+
+ override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
+
/**
* Tells whether the client should ever send encrypted messages to unverified devices.
* The default value is false.
@@ -1165,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
setRoomBlacklistUnverifiedDevices(roomId, false)
}
-// TODO Check if this method is still necessary
- /**
- * Cancel any earlier room key request
- *
- * @param requestBody requestBody
- */
- override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
- outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
- }
-
/**
* Re request the encryption keys required to decrypt an event.
*
* @param event the event to decrypt again.
*/
override fun reRequestRoomKeyForEvent(event: Event) {
- val wireContent = event.content.toModel() ?: return Unit.also {
- Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
- }
-
- val requestBody = RoomKeyRequestBody(
- algorithm = wireContent.algorithm,
- roomId = event.roomId,
- senderKey = wireContent.senderKey,
- sessionId = wireContent.sessionId
- )
-
- outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
+ outgoingKeyRequestManager.requestKeyForEvent(event, true)
}
override fun requestRoomKeyForEvent(event: Event) {
- val wireContent = event.content.toModel() ?: return Unit.also {
- Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
- }
-
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-// if (!isStarted()) {
-// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
-// internalStart(false)
-// }
- roomDecryptorProvider
- .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
- ?.requestKeysForEvent(event, false) ?: run {
- Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
- }
- }
+ outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
/**
@@ -1219,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
- incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
+ incomingKeyRequestManager.addRoomKeysRequestListener(listener)
+ secretShareManager.addListener(listener)
}
/**
@@ -1228,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
- incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
+ incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
+ secretShareManager.removeListener(listener)
}
-// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
-// val deviceKey = deviceInfo.identityKey()
-//
-// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
-// val now = clock.epochMillis()
-// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
-// return
-// }
-//
-// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
-// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
-//
-// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
-//
-// // Now send a blank message on that session so the other side knows about it.
-// // (The keyshare request is sent in the clear so that won't do)
-// // We send this first such that, as long as the toDevice messages arrive in the
-// // same order we sent them, the other end will get this first, set up the new session,
-// // then get the keyshare request and send the key over this new session (because it
-// // is the session it has most recently received a message on).
-// val payloadJson = mapOf("type" to EventType.DUMMY)
-//
-// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
-// val sendToDeviceMap = MXUsersDevicesMap()
-// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
-// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
-// sendToDeviceTask.execute(sendToDeviceParams)
-// }
-// }
-
/**
* Provides the list of unknown devices
*
@@ -1309,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
return "DefaultCryptoService of $userId ($deviceId)"
}
- override fun getOutgoingRoomKeyRequests(): List {
+ override fun getOutgoingRoomKeyRequests(): List {
return cryptoStore.getOutgoingRoomKeyRequests()
}
- override fun getOutgoingRoomKeyRequestsPaged(): LiveData> {
+ override fun getOutgoingRoomKeyRequestsPaged(): LiveData> {
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
}
- override fun getIncomingRoomKeyRequestsPaged(): LiveData> {
- return cryptoStore.getIncomingRoomKeyRequestsPaged()
- }
-
override fun getIncomingRoomKeyRequests(): List {
- return cryptoStore.getIncomingRoomKeyRequests()
+ return cryptoStore.getGossipingEvents()
+ .mapNotNull {
+ IncomingRoomKeyRequest.fromEvent(it)
+ }
}
- override fun getGossipingEventsTrail(): LiveData> {
+ 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 {
+ override fun getGossipingEvents(): List {
return cryptoStore.getGossipingEvents()
}
@@ -1353,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
- callback.onFailure(failure)
- return@launch
+ // we probably shouldn't block sending on that (but questionable)
+ // but some members won't be able to decrypt
}
val userIds = getRoomUserIds(roomId)
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
index 535999373b..f546b35fcf 100755
--- 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
@@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
} else {
Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = clock.epochMillis()
- val result = doKeyDownloadForUsers(downloadUsers)
- Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
- result.also {
- it.addEntriesFromMap(stored)
+ 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
+ }
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt
deleted file mode 100644
index a2c85e5ceb..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt
+++ /dev/null
@@ -1,58 +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.work.BackoffPolicy
-import androidx.work.Data
-import androidx.work.ExistingWorkPolicy
-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.SessionScope
-import org.matrix.android.sdk.internal.util.CancelableWork
-import org.matrix.android.sdk.internal.worker.startChain
-import java.util.UUID
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@SessionScope
-internal class GossipingWorkManager @Inject constructor(
- private val workManagerProvider: WorkManagerProvider
-) {
-
- inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
- return workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .setConstraints(WorkManagerProvider.workConstraints)
- .startChain(startChain)
- .setInputData(data)
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
- .build()
- }
-
- // Prevent sending queue to stay broken after app restart
- // The unique queue id will stay the same as long as this object is instantiated
- private val queueSuffixApp = UUID.randomUUID()
-
- fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
- workManagerProvider.workManager
- .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
- .enqueue()
-
- return CancelableWork(workManagerProvider.workManager, workRequest.id)
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
deleted file mode 100644
index 1612caba9f..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
+++ /dev/null
@@ -1,475 +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.launch
-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.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.GossipingRequestState
-import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
-import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
-import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
-import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
-import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
-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.util.toBase64NoPadding
-import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
-import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
-import org.matrix.android.sdk.internal.di.SessionId
-import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.util.time.Clock
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import timber.log.Timber
-import java.util.concurrent.Executors
-import javax.inject.Inject
-
-@SessionScope
-internal class IncomingGossipingRequestManager @Inject constructor(
- @SessionId private val sessionId: String,
- private val credentials: Credentials,
- private val cryptoStore: IMXCryptoStore,
- private val cryptoConfig: MXCryptoConfig,
- private val gossipingWorkManager: GossipingWorkManager,
- private val roomEncryptorsStore: RoomEncryptorsStore,
- private val roomDecryptorProvider: RoomDecryptorProvider,
- private val coroutineDispatchers: MatrixCoroutineDispatchers,
- private val cryptoCoroutineScope: CoroutineScope,
- private val clock: Clock,
-) {
-
- private val executor = Executors.newSingleThreadExecutor()
-
- // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
- // we received in the current sync.
- private val receivedGossipingRequests = ArrayList()
- private val receivedRequestCancellations = ArrayList()
-
- // the listeners
- private val gossipingRequestListeners: MutableSet = HashSet()
-
- init {
- receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
- }
-
- fun close() {
- executor.shutdownNow()
- }
-
- // Recently verified devices (map of deviceId and timestamp)
- private val recentlyVerifiedDevices = HashMap()
-
- /**
- * Called when a session has been verified.
- * This information can be used by the manager to decide whether or not to fullfil gossiping requests
- */
- fun onVerificationCompleteForDevice(deviceId: String) {
- // For now we just keep an in memory cache
- synchronized(recentlyVerifiedDevices) {
- recentlyVerifiedDevices[deviceId] = clock.epochMillis()
- }
- }
-
- private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
- val verifTimestamp: Long?
- synchronized(recentlyVerifiedDevices) {
- verifTimestamp = recentlyVerifiedDevices[deviceId]
- }
- if (verifTimestamp == null) return false
-
- val age = clock.epochMillis() - verifTimestamp
-
- return age < FIVE_MINUTES_IN_MILLIS
- }
-
- /**
- * Called when we get an m.room_key_request event
- * It must be called on CryptoThread
- *
- * @param event the announcement event.
- */
- fun onGossipingRequestEvent(event: Event) {
- val roomKeyShare = event.getClearContent().toModel()
- Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare")
- // val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
- when (roomKeyShare?.action) {
- GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
- if (event.getClearType() == EventType.REQUEST_SECRET) {
- IncomingSecretShareRequest.fromEvent(event, clock.epochMillis())?.let {
- if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
- // ignore, it was sent by me as *
- Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
- } else {
-// // save in DB
-// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
- receivedGossipingRequests.add(it)
- }
- }
- } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
- IncomingRoomKeyRequest.fromEvent(event, clock.epochMillis())?.let {
- if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
- // ignore, it was sent by me as *
- Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
- } else {
-// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
- receivedGossipingRequests.add(it)
- }
- }
- }
- }
- GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
- IncomingRequestCancellation.fromEvent(event, clock.epochMillis())?.let {
- receivedRequestCancellations.add(it)
- }
- }
- else -> {
- Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
- }
- }
- }
-
- /**
- * Process any m.room_key_request or m.secret.request events which were queued up during the
- * current sync.
- * It must be called on CryptoThread
- */
- fun processReceivedGossipingRequests() {
- val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
- receivedGossipingRequests.clear()
-
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
-
- var receivedRequestCancellations: List? = null
-
- synchronized(this.receivedRequestCancellations) {
- if (this.receivedRequestCancellations.isNotEmpty()) {
- receivedRequestCancellations = this.receivedRequestCancellations.toList()
- this.receivedRequestCancellations.clear()
- }
- }
-
- executor.execute {
- cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
- for (request in roomKeyRequestsToProcess) {
- if (request is IncomingRoomKeyRequest) {
- processIncomingRoomKeyRequest(request)
- } else if (request is IncomingSecretShareRequest) {
- processIncomingSecretShareRequest(request)
- }
- }
-
- receivedRequestCancellations?.forEach { request ->
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
- // we should probably only notify the app of cancellations we told it
- // about, but we don't currently have a record of that, so we just pass
- // everything through.
- if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
- // ignore remote echo
- return@forEach
- }
- val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
- if (matchingIncoming == null) {
- // ignore that?
- return@forEach
- } else {
- // If it was accepted from this device, keep the information, do not mark as cancelled
- if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
- onRoomKeyRequestCancellation(request)
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
- }
- }
- }
- }
- }
-
- private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
- val userId = request.userId ?: return
- val deviceId = request.deviceId ?: return
- val body = request.requestBody ?: return
- val roomId = body.roomId ?: return
- val alg = body.algorithm ?: return
-
- Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
- if (credentials.userId != userId) {
- handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId)
- return
- }
- // TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
- // if we don't have a decryptor for this room/alg, we don't have
- // the keys for the requested events, and can drop the requests.
- val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
- if (null == decryptor) {
- Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
- if (!decryptor.hasKeysForKeyRequest(request)) {
- Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
-
- if (credentials.deviceId == deviceId && credentials.userId == userId) {
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
- request.share = Runnable {
- decryptor.shareKeysWithDevice(request)
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
- }
- request.ignore = Runnable {
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- }
- // if the device is verified already, share the keys
- val device = cryptoStore.getUserDevice(userId, deviceId)
- if (device != null) {
- if (device.isVerified) {
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
- request.share?.run()
- return
- }
-
- if (device.isBlocked) {
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
- }
-
- // As per config we automatically discard untrusted devices request
- if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
- Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
- // At this point the device is unknown, we don't want to bother user with that
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
-
- // Pass to application layer to decide what to do
- onRoomKeyRequest(request)
- }
-
- private fun handleKeyRequestFromOtherUser(body: RoomKeyRequestBody,
- request: IncomingRoomKeyRequest,
- alg: String,
- roomId: String,
- userId: String,
- deviceId: String) {
- Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
- val senderKey = body.senderKey ?: return Unit
- .also { Timber.w("missing senderKey") }
- .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
- val sessionId = body.sessionId ?: return Unit
- .also { Timber.w("missing sessionId") }
- .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
-
- if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
- return Unit
- .also { Timber.w("Only megolm is accepted here") }
- .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
- }
-
- val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
- .also { Timber.w("no room Encryptor") }
- .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
-
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- if (roomEncryptor is IMXGroupEncryption) {
- val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
-
- if (isSuccess) {
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
- } else {
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
- }
- } else {
- Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg")
- }
- }
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
- }
-
- private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
- val secretName = request.secretName ?: return Unit.also {
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name")
- }
-
- val userId = request.userId
- if (userId == null || credentials.userId != userId) {
- Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
-
- val deviceId = request.deviceId
- ?: return Unit.also {
- Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- }
-
- val device = cryptoStore.getUserDevice(userId, deviceId)
- ?: return Unit.also {
- Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- }
-
- if (!device.isVerified || device.isBlocked) {
- Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- return
- }
-
- val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
-
- 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
- }?.let { secretValue ->
- Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
- if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
- val params = SendGossipWorker.Params(
- sessionId = sessionId,
- secretValue = secretValue,
- requestUserId = request.userId,
- requestDeviceId = request.deviceId,
- requestId = request.requestId,
- txnId = createUniqueTxnId()
- )
-
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
- val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true)
- gossipingWorkManager.postWork(workRequest)
- } else {
- Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- }
- return
- }
-
- Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
-
- request.ignore = Runnable {
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
- }
-
- request.share = { secretValue ->
- val params = SendGossipWorker.Params(
- sessionId = userId,
- secretValue = secretValue,
- requestUserId = request.userId,
- requestDeviceId = request.deviceId,
- requestId = request.requestId,
- txnId = createUniqueTxnId()
- )
-
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
- val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true)
- gossipingWorkManager.postWork(workRequest)
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
- }
-
- onShareRequest(request)
- }
-
- /**
- * Dispatch onRoomKeyRequest
- *
- * @param request the request
- */
- private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
- synchronized(gossipingRequestListeners) {
- for (listener in gossipingRequestListeners) {
- try {
- listener.onRoomKeyRequest(request)
- } catch (e: Exception) {
- Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed")
- }
- }
- }
- }
-
- /**
- * Ask for a value to the listeners, and take the first one
- */
- private fun onShareRequest(request: IncomingSecretShareRequest) {
- synchronized(gossipingRequestListeners) {
- for (listener in gossipingRequestListeners) {
- try {
- if (listener.onSecretShareRequest(request)) {
- return
- }
- } catch (e: Exception) {
- Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed")
- }
- }
- }
- // Not handled, ignore
- request.ignore?.run()
- }
-
- /**
- * A room key request cancellation has been received.
- *
- * @param request the cancellation request
- */
- private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
- synchronized(gossipingRequestListeners) {
- for (listener in gossipingRequestListeners) {
- try {
- listener.onRoomKeyRequestCancellation(request)
- } catch (e: Exception) {
- Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed")
- }
- }
- }
- }
-
- fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
- synchronized(gossipingRequestListeners) {
- gossipingRequestListeners.add(listener)
- }
- }
-
- fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
- synchronized(gossipingRequestListeners) {
- gossipingRequestListeners.remove(listener)
- }
- }
-
- companion object {
- private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
- }
-}
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
new file mode 100644
index 0000000000..13f2fb861a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
@@ -0,0 +1,463 @@
+/*
+ * 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,
+ 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/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt
deleted file mode 100644
index 97c369db3e..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt
+++ /dev/null
@@ -1,36 +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
-
-internal interface IncomingShareRequestCommon {
- /**
- * The user id
- */
- val userId: String?
-
- /**
- * The device id
- */
- val deviceId: String?
-
- /**
- * The request id
- */
- val requestId: String?
-
- val localCreationTimestamp: Long?
-}
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
index 7eec83abdd..68a1519670 100755
--- 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
@@ -43,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession
import org.matrix.olm.OlmUtility
import timber.log.Timber
-import java.net.URLEncoder
import javax.inject.Inject
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
@@ -331,14 +330,6 @@ internal class MXOlmDevice @Inject constructor(
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
}
- Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
- try {
- val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
- Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
- } catch (e: Exception) {
- Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
- }
-
val olmMessage = OlmMessage()
olmMessage.mCipherText = ciphertext
olmMessage.mType = messageType.toLong()
@@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor(
// 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.
*
@@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor(
senderKey: String,
forwardingCurve25519KeyChain: List,
keysClaimed: Map,
- exportFormat: Boolean): Boolean {
+ exportFormat: Boolean): AddSessionResult {
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper
@@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor(
if (existingSession != null) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
try {
- val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
+ val existingFirstKnown = existingSession.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.olmInboundGroupSession?.releaseSession()
@@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor(
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
candidateSession.olmInboundGroupSession?.releaseSession()
- return false
+ return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
}
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession.olmInboundGroupSession?.releaseSession()
- return false
+ return AddSessionResult.NotImported
}
}
@@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor(
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
if (null == candidateOlmInboundSession) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ")
- return false
+ return AddSessionResult.NotImported
}
try {
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
candidateOlmInboundSession.releaseSession()
- return false
+ return AddSessionResult.NotImported
}
} catch (e: Throwable) {
candidateOlmInboundSession.releaseSession()
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
- return false
+ return AddSessionResult.NotImported
}
candidateSession.senderKey = senderKey
@@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor(
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
}
- return true
+ return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
}
/**
@@ -790,7 +788,7 @@ internal class MXOlmDevice @Inject constructor(
if (timelineSet.contains(messageIndexKey)) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
- Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
+ Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt
deleted file mode 100644
index 2438e01102..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt
+++ /dev/null
@@ -1,27 +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.session.crypto.model.OutgoingGossipingRequestState
-
-internal interface OutgoingGossipingRequest {
- val recipients: Map>
- val requestId: String
- val state: OutgoingGossipingRequestState
- // transaction id for the cancellation, if any
- // var cancellationTxnId: String?
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
deleted file mode 100755
index e6f6ac5053..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
+++ /dev/null
@@ -1,167 +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.delay
-import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
-import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
-import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
-import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
-import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
-import org.matrix.android.sdk.internal.di.SessionId
-import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import timber.log.Timber
-import javax.inject.Inject
-
-@SessionScope
-internal class OutgoingGossipingRequestManager @Inject constructor(
- @SessionId private val sessionId: String,
- private val cryptoStore: IMXCryptoStore,
- private val coroutineDispatchers: MatrixCoroutineDispatchers,
- private val cryptoCoroutineScope: CoroutineScope,
- private val gossipingWorkManager: GossipingWorkManager) {
-
- /**
- * Send off a room key request, if we haven't already done so.
- *
- *
- * The `requestBody` is compared (with a deep-equality check) against
- * previous queued or sent requests and if it matches, no change is made.
- * Otherwise, a request is added to the pending list, and a job is started
- * in the background to send it.
- *
- * @param requestBody requestBody
- * @param recipients recipients
- */
- fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
- // Don't resend if it's already done, you need to cancel first (reRequest)
- if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
- Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
- return@launch
- }
-
- sendOutgoingGossipingRequest(it)
- }
- }
- }
-
- fun sendSecretShareRequest(secretName: String, recipients: Map>) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- // A bit dirty, but for better stability give other party some time to mark
- // devices trusted :/
- delay(1500)
- cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
- // TODO check if there is already one that is being sent?
- if (it.state == OutgoingGossipingRequestState.SENDING
- /**|| it.state == OutgoingGossipingRequestState.SENT*/
- ) {
- Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
- return@launch
- }
-
- sendOutgoingGossipingRequest(it)
- }
- }
- }
-
- /**
- * Cancel room key requests, if any match the given details
- *
- * @param requestBody requestBody
- */
- fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
- cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
- cancelRoomKeyRequest(requestBody, false)
- }
- }
-
- /**
- * Cancel room key requests, if any match the given details, and resend
- *
- * @param requestBody requestBody
- */
- fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
- cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
- cancelRoomKeyRequest(requestBody, true)
- }
- }
-
- /**
- * Cancel room key requests, if any match the given details, and resend
- *
- * @param requestBody requestBody
- * @param andResend true to resend the key request
- */
- private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
- val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) // no request was made for this key
- ?: return Unit.also {
- Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody")
- }
-
- sendOutgoingRoomKeyRequestCancellation(req, andResend)
- }
-
- /**
- * Send the outgoing key request.
- *
- * @param request the request
- */
- private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
- Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
-
- val params = SendGossipRequestWorker.Params(
- sessionId = sessionId,
- keyShareRequest = request as? OutgoingRoomKeyRequest,
- secretShareRequest = request as? OutgoingSecretRequest,
- txnId = createUniqueTxnId()
- )
- cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
- val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true)
- gossipingWorkManager.postWork(workRequest)
- }
-
- /**
- * Given a OutgoingRoomKeyRequest, cancel it and delete the request record
- *
- * @param request the request
- */
- private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
- Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request")
- val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
- cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
-
- val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true)
- gossipingWorkManager.postWork(workRequest)
-
- if (resend) {
- val reSendParams = SendGossipRequestWorker.Params(
- sessionId = sessionId,
- keyShareRequest = request.copy(requestId = RequestIdHelper.createUniqueRequestId()),
- txnId = createUniqueTxnId()
- )
- val reSendWorkRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(reSendParams), true)
- gossipingWorkManager.postWork(reSendWorkRequest)
- }
- }
-}
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
new file mode 100755
index 0000000000..09a9868428
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt
@@ -0,0 +1,518 @@
+/*
+ * 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