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/build.yml b/.github/workflows/build.yml
index 22b3a1727d..9517a4f3a7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -67,4 +67,4 @@ jobs:
path: |
vector/build/outputs/apk/*/release/*.apk
-# TODO: add exodus checks
+# TODO add exodus checks
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 84%
rename from .github/workflows/nightly.yml
rename to .github/workflows/post-pr.yml
index 502e3e275f..9cd33143ad 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
@@ -43,11 +58,12 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
- uses: michaelkaye/setup-matrix-synapse@v1.0.1
+ uses: michaelkaye/setup-matrix-synapse@v1.0.3
with:
uploadLogs: true
httpPort: 8080
disableRateLimiting: true
+ public_baseurl: "http://10.0.2.2:8080/"
# package: org.matrix.android.sdk.session
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
uses: reactivecircus/android-emulator-runner@v2
@@ -209,6 +225,7 @@ jobs:
ui-tests:
name: UI Tests (Synapse)
+ needs: should-i-run
runs-on: macos-latest
strategy:
fail-fast: false
@@ -230,11 +247,12 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Start synapse server
- uses: michaelkaye/setup-matrix-synapse@v1.0.1
+ uses: michaelkaye/setup-matrix-synapse@v1.0.3
with:
uploadLogs: true
httpPort: 8080
disableRateLimiting: true
+ public_baseurl: "http://10.0.2.2:8080/"
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
@@ -266,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
@@ -290,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.pull_request.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.pull_request.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/quality.yml b/.github/workflows/quality.yml
index dee596980f..fab98e8e91 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -147,3 +147,23 @@ jobs:
name: release-lint-report-${{ matrix.target }}
path: |
vector/build/reports/*.*
+
+ detekt:
+ name: Detekt Analysis
+ runs-on: ubuntu-latest
+ # Allow all jobs on main and develop. Just one per PR.
+ concurrency:
+ group: ${{ github.ref == 'refs/heads/main' && format('detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('detekt-develop-{0}', github.sha) || format('detekt-{0}', github.ref) }}
+ cancel-in-progress: true
+ steps:
+ - uses: actions/checkout@v3
+ - name: Run detekt
+ run: |
+ ./gradlew detekt
+ - name: Upload reports
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: detekt-report
+ path: |
+ */build/reports/detekt/detekt.html
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 f8b47a87a3..2a4d19c4e8 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 {
@@ -28,9 +35,11 @@ buildscript {
}
}
-// ktlint Plugin
plugins {
+ // ktlint Plugin
id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
+ // Detekt
+ id "io.gitlab.arturbosch.detekt" version "1.20.0"
}
// https://github.com/jeremylong/DependencyCheck
@@ -45,9 +54,12 @@ dependencyCheck {
allprojects {
apply plugin: "org.jlleitschuh.gradle.ktlint"
+ apply plugin: "io.gitlab.arturbosch.detekt"
repositories {
- mavenCentral {
+ // Do not use `mavenCentral()`, it prevents Dependabot from working properly
+ maven {
+ url 'https://repo1.maven.org/maven2'
content {
groups.mavenCentral.regex.each { includeGroupByRegex it }
groups.mavenCentral.group.each { includeGroup it }
@@ -70,14 +82,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 }
@@ -106,7 +122,7 @@ allprojects {
// display the corresponding rule
verbose = true
disabledRules = [
- // TODO: Re-enable these 4 rules after reformatting project
+ // TODO Re-enable these 4 rules after reformatting project
"indent",
"experimental:argument-list-wrapping",
"max-line-length",
@@ -127,6 +143,15 @@ allprojects {
"experimental:kdoc-wrapping",
]
}
+
+ detekt {
+ // preconfigure defaults
+ buildUponDefaultConfig = true
+ // activate all available (even unstable) rules.
+ allRules = true
+ // point to your custom config defining rules to run, overwriting default behavior
+ config = files("$rootDir/tools/detekt/detekt.yml")
+ }
}
task clean(type: Delete) {
diff --git a/changelog.d/5151.misc b/changelog.d/5151.misc
new file mode 100644
index 0000000000..b785c4229c
--- /dev/null
+++ b/changelog.d/5151.misc
@@ -0,0 +1 @@
+Improve threads rendering in the main timeline
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/5906.bugfix b/changelog.d/5906.bugfix
new file mode 100644
index 0000000000..be1379c6e4
--- /dev/null
+++ b/changelog.d/5906.bugfix
@@ -0,0 +1 @@
+Desynchronized 4S | Megolm backup causing Unusable backup
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/5932.feature b/changelog.d/5932.feature
new file mode 100644
index 0000000000..dcfc6615b0
--- /dev/null
+++ b/changelog.d/5932.feature
@@ -0,0 +1 @@
+Allow using the latest user Avatar and name for all messages in the timeline
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/5973.doc b/changelog.d/5973.doc
new file mode 100644
index 0000000000..cd3b31dd21
--- /dev/null
+++ b/changelog.d/5973.doc
@@ -0,0 +1 @@
+Note public_baseurl requirement in integration tests documentation.
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/changelog.d/6038.misc b/changelog.d/6038.misc
new file mode 100644
index 0000000000..881aae5ca3
--- /dev/null
+++ b/changelog.d/6038.misc
@@ -0,0 +1 @@
+Setup detekt
diff --git a/changelog.d/6047.feature b/changelog.d/6047.feature
new file mode 100644
index 0000000000..59d37e21e9
--- /dev/null
+++ b/changelog.d/6047.feature
@@ -0,0 +1 @@
+Add presence indicator busy and away.
diff --git a/changelog.d/6073.feature b/changelog.d/6073.feature
new file mode 100644
index 0000000000..295f45f3ce
--- /dev/null
+++ b/changelog.d/6073.feature
@@ -0,0 +1 @@
+Adds up navigation in spaces
diff --git a/dependencies.gradle b/dependencies.gradle
index 7666a3bf9f..10f9539e5a 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 kotlinCoroutines = "1.6.0"
-def dagger = "2.40.5"
+def kotlin = "1.6.21"
+def kotlinCoroutines = "1.6.1"
+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/dependencies_groups.gradle b/dependencies_groups.gradle
index 8422e05930..76869fccf1 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -123,6 +123,7 @@ ext.groups = [
'io.github.detekt.sarif4k',
'io.github.microutils',
'io.github.reactivecircus.flowbinding',
+ 'io.gitlab.arturbosch.detekt',
'io.grpc',
'io.jsonwebtoken',
'io.kindedj',
@@ -195,6 +196,7 @@ ext.groups = [
'org.testng',
'org.threeten',
'org.webjars',
+ 'org.yaml',
'ru.noties',
'xerces',
'xml-apis',
diff --git a/docs/integration_tests.md b/docs/integration_tests.md
index 0fa1998499..e79f966d1f 100644
--- a/docs/integration_tests.md
+++ b/docs/integration_tests.md
@@ -43,14 +43,17 @@ virtualenv -p python3 env
source env/bin/activate
pip install -e .
demo/start.sh --no-rate-limit
+
```
-Alternatively, to install the latest Synapse release package (and not a cloned branch) you can run the following instead of `pip install -e .`:
+Alternatively, to install the latest Synapse release package (and not a cloned branch) you can run the following instead of `git clone` and `pip install -e .`:
```bash
pip install matrix-synapse
```
+On your first run, you will want to stop the demo and edit the config to correct the `public_baseurl` to http://10.0.2.2:8080 and restart the server.
+
You should now have 3 running federated Synapse instances 🎉, at http://127.0.0.1:8080/, http://127.0.0.1:8081/ and http://127.0.0.1:8082/, which should display a "It Works! Synapse is running" message.
## Run the test
@@ -87,6 +90,18 @@ You'll need python3 to be able to run synapse
Try on the Emulator browser to open "http://10.0.2.2:8080". You should see the "Synapse is running" message.
+### Tests partially run but some fail with "Unable to contact localhost:8080"
+
+This is because the `public_baseurl` of synapse is not consistent with the endpoint that the tests are connecting to.
+
+Ensure you have the following configuration in `demo/etc/8080.config`.
+
+```
+public_baseurl: http://10.0.2.2:8080/
+```
+
+After changing this you will need to restart synapse using `demo/stop.sh` and `demo/start.sh` to load the new configuration.
+
### virtualenv command fails
You can try using
diff --git a/docs/pull_request.md b/docs/pull_request.md
index 4775f292ee..473d5a259b 100644
--- a/docs/pull_request.md
+++ b/docs/pull_request.md
@@ -30,6 +30,19 @@ In any case, it is better to explicitly declare in the description why the PR is
Also, draft PR should not stay indefinitely in this state. It may be removed if it is the case and the submitter does not update it after a few days.
+##### Base branch
+
+The `develop` branch is generally the base branch for every PRs.
+
+Exceptions can occur:
+
+- if a feature implementation is split into multiple PRs. We can have a chain of PRs in this case. PR can be merged one by one on develop, and GitHub change the target branch to `develop` for the next PR automatically.
+- we want to merge a PR from the community, but there is still work to do, and the PR is not updated by the submitter. First, we can kindly ask the submitter if they will update their PR, by commenting it. If there is no answer after a few days (including a week-end), we can create a new branch, push it, and change the target branch of the PR to this new branch. The PR can then be merged, and we can add more commits to fix the issues. After that a new PR can be created with `develop` as a target branch.
+
+**Important notice 1:** Releases are created from the `develop` branch. So `develop` branch should always contain a "releasable" source code. So when a feature is being implemented with several PRs, it has to be disabled by default (using a feature flag for instance), until the feature is fully implemented. A last PR to enable the feature can then be created.
+
+**Important notice 2:** Database migration: some developers and some people from the community are using the nightly build from `develop`. Multiple database migrations should be properly handled for them. This is OK to have multiple migrations between 2 releases, this is not OK to add steps to the pending database migration on `develop`. So for instance `develop` users will migrate from version 11 to version 12, then 13, then 14, and `main` users will do all those steps after they get the app upgrade.
+
##### PR Review Assignment
We use automatic assignment for PR reviews. A PR is automatically routed by GitHub to 2 team members using the round robin algorithm. The process is the following:
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 0cad8ac171..2110747feb 100644
--- a/library/jsonviewer/build.gradle
+++ b/library/jsonviewer/build.gradle
@@ -6,8 +6,14 @@ apply plugin: 'com.jakewharton.butterknife'
buildscript {
repositories {
- google()
- mavenCentral()
+ // 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/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt
index 821c2f0d4c..e7883c9e53 100644
--- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt
+++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt
@@ -16,7 +16,7 @@
package im.vector.lib.multipicker
-class MultiPicker {
+class MultiPicker private constructor() {
companion object Type {
val IMAGE by lazy { MultiPicker() }
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index a5c24feee8..39c69993cd 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -128,6 +128,14 @@
@color/palette_element_green
@color/palette_element_green
+
+ @color/element_alert_light
+ @color/element_alert_dark
+
+
+ @color/palette_element_orange
+ @color/palette_element_orange
+
@color/palette_prune
diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml
index e6cee80b59..73ac768919 100644
--- a/library/ui-styles/src/main/res/values/palette.xml
+++ b/library/ui-styles/src/main/res/values/palette.xml
@@ -17,6 +17,7 @@
#FF812D
#0DBD8B
+ #D9B072
#FFFFFF
#FF5B55
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index 7177687fdd..eeff039b71 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -44,6 +44,8 @@
- @color/vctr_presence_indicator_offline_dark
- @color/vctr_presence_indicator_online_dark
+ - @color/vctr_presence_indicator_busy_dark
+ - @color/vctr_presence_indicator_away_dark
- ?vctr_system
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index c90c021591..0c363b583d 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -44,6 +44,8 @@
- @color/vctr_presence_indicator_offline_light
- @color/vctr_presence_indicator_online_light
+ - @color/vctr_presence_indicator_busy_light
+ - @color/vctr_presence_indicator_away_light
- ?vctr_system
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 65824476a0..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 {
- mavenCentral()
+ // 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..348841313b 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
@@ -188,17 +203,49 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
// Alice sends a message
- testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
+ testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1).first().eventId.let { sentEventId ->
+ // ensure bob got it
+ ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
+ }
// Bob send 3 messages
- testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
- testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
- testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
+ testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1).first().eventId.let { sentEventId ->
+ // ensure alice got it
+ ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
+ }
+
+ testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1).first().eventId.let { sentEventId ->
+ // ensure alice got it
+ ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
+ }
+ testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1).first().eventId.let { sentEventId ->
+ // ensure alice got it
+ ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
+ }
+
// Alice sends a message
- testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
+ testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1).first().eventId.let { sentEventId ->
+ // ensure bob got it
+ ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
+ }
return cryptoTestData
}
+ private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
+ if (andCanDecrypt) {
+ timeLineEvent != null &&
+ timeLineEvent.isEncrypted() &&
+ timeLineEvent.root.getClearType() == EventType.MESSAGE
+ } else {
+ timeLineEvent != null
+ }
+ }
+ }
+ }
+
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
assertEquals(EventType.ENCRYPTED, event.type)
assertNotNull(event.content)
@@ -296,17 +343,104 @@ 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()
+ 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
+
+ testHelper.waitWithLatch {
+ testHelper.retryPeriodicallyWithLatch(it) {
+ 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
+ }
+ }
+ }
+
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
- requestID,
+ requestID!!,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!
@@ -316,23 +450,9 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
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()
- true
- } else {
- false
- }
- }
- }
-
- 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 +460,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 +512,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/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index a7ddb6c553..9136272b1e 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@@ -37,7 +36,9 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersio
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
@@ -54,18 +55,16 @@ import java.util.concurrent.CountDownLatch
@LargeTest
class KeysBackupTest : InstrumentedTest {
- private val testHelper = CommonTestHelper(context())
- private val cryptoTestHelper = CryptoTestHelper(testHelper)
- private val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
-
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
* - Check backup keys after having marked one as backed up
* - Reset keys backup markers
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun roomKeysTest_testBackupStore_ok() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
// From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
@@ -104,6 +103,8 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun prepareKeysBackupVersionTest() {
+ val testHelper = CommonTestHelper(context())
+
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
assertNotNull(bobSession.cryptoService().keysBackupService())
@@ -132,7 +133,11 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun createKeysBackupVersionTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
+ cryptoTestHelper.initializeCrossSigning(bobSession)
val keysBackup = bobSession.cryptoService().keysBackupService()
@@ -147,13 +152,46 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
// Create the version
- testHelper.doSync {
+ val version = testHelper.doSync {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
// Backup must be enable now
assertTrue(keysBackup.isEnabled)
+ // Check that it's signed with MSK
+ val versionResult = testHelper.doSync {
+ keysBackup.getVersion(version.version, it)
+ }
+ val trust = testHelper.doSync {
+ keysBackup.getKeysBackupTrust(versionResult!!, it)
+ }
+
+ assertEquals("Should have 2 signatures", 2, trust.signatures.size)
+
+ trust.signatures
+ .firstOrNull { it is KeysBackupVersionTrustSignature.DeviceSignature }
+ .let {
+ assertNotNull("Should be signed by a device", it)
+ it as KeysBackupVersionTrustSignature.DeviceSignature
+ }.let {
+ assertEquals("Should be signed by current device", bobSession.sessionParams.deviceId, it.deviceId)
+ assertTrue("Signature should be valid", it.valid)
+ }
+
+ trust.signatures
+ .firstOrNull { it is KeysBackupVersionTrustSignature.UserSignature }
+ .let {
+ assertNotNull("Should be signed by a user", it)
+ it as KeysBackupVersionTrustSignature.UserSignature
+ }.let {
+ val msk = bobSession.cryptoService().crossSigningService()
+ .getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey
+ assertEquals("Should be signed by my msk 1", msk, it.keyId)
+ assertEquals("Should be signed by my msk 2", msk, it.cryptoCrossSigningKey?.unpaddedBase64PublicKey)
+ assertTrue("Signature should be valid", it.valid)
+ }
+
stateObserver.stopAndCheckStates(null)
testHelper.signOutAndClose(bobSession)
}
@@ -163,8 +201,11 @@ class KeysBackupTest : InstrumentedTest {
* - Check the backup completes
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun backupAfterCreateKeysBackupVersionTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
keysBackupTestHelper.waitForKeybackUpBatching()
@@ -204,8 +245,11 @@ class KeysBackupTest : InstrumentedTest {
* Check that backupAllGroupSessions() returns valid data
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun backupAllGroupSessionsTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
@@ -249,8 +293,11 @@ class KeysBackupTest : InstrumentedTest {
* - Compare the decrypted megolm key with the original one
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testEncryptAndDecryptKeysBackupData() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
@@ -293,8 +340,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver
@@ -378,8 +428,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -438,8 +491,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithRecoveryKeyTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -496,8 +552,11 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@@ -538,8 +597,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithPasswordTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val password = "Password"
// - Do an e2e backup to the homeserver with a password
@@ -598,8 +660,11 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithWrongPasswordTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val password = "Password"
val badPassword = "Bad Password"
@@ -639,8 +704,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a wrong recovery key
@@ -673,8 +741,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testBackupWithPassword() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val password = "password"
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
@@ -730,8 +801,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupWithAWrongPasswordTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val password = "password"
val wrongPassword = "passw0rd"
@@ -767,8 +841,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val password = "password"
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
@@ -797,8 +874,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a password
@@ -829,8 +909,11 @@ class KeysBackupTest : InstrumentedTest {
* - Check the returned KeysVersionResult is trusted
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testIsKeysBackupTrusted() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -855,7 +938,7 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(keysBackupVersionTrust.usable)
assertEquals(1, keysBackupVersionTrust.signatures.size)
- val signature = keysBackupVersionTrust.signatures[0]
+ val signature = keysBackupVersionTrust.signatures[0] as KeysBackupVersionTrustSignature.DeviceSignature
assertTrue(signature.valid)
assertNotNull(signature.device)
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
@@ -865,66 +948,6 @@ class KeysBackupTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
- /**
- * Check backup starts automatically if there is an existing and compatible backup
- * version on the homeserver.
- * - Create a backup version
- * - Restart alice session
- * -> The new alice session must back up to the same version
- */
- @Test
- @Ignore("This test will be ignored until it is fixed")
- fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() {
- // - Create a backup version
- val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
-
- val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
-
- val stateObserver = StateObserver(keysBackup)
-
- assertFalse(keysBackup.isEnabled)
-
- val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
-
- assertTrue(keysBackup.isEnabled)
-
- // - Restart alice session
- // - Log Alice on a new device
- val aliceSession2 = testHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
-
- cryptoTestData.cleanUp(testHelper)
-
- val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
-
- val stateObserver2 = StateObserver(keysBackup2)
-
- // -> The new alice session must back up to the same version
- val latch = CountDownLatch(1)
- var count = 0
- keysBackup2.addListener(object : KeysBackupStateListener {
- override fun onStateChange(newState: KeysBackupState) {
- // Check the backup completes
- if (newState == KeysBackupState.ReadyToBackUp) {
- count++
-
- if (count == 2) {
- // Remove itself from the list of listeners
- keysBackup2.removeListener(this)
-
- latch.countDown()
- }
- }
- }
- })
- testHelper.await(latch)
-
- assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion)
-
- stateObserver.stopAndCheckStates(null)
- stateObserver2.stopAndCheckStates(null)
- testHelper.signOutAndClose(aliceSession2)
- }
-
/**
* Check WrongBackUpVersion state
*
@@ -935,6 +958,10 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun testBackupWhenAnotherBackupWasCreated() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -1005,8 +1032,11 @@ class KeysBackupTest : InstrumentedTest {
* -> It must success
*/
@Test
- @Ignore("This test will be ignored until it is fixed")
fun testBackupAfterVerifyingADevice() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@@ -1039,6 +1069,8 @@ class KeysBackupTest : InstrumentedTest {
// - Try to backup all in aliceSession2, it must fail
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
+ assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
+
val stateObserver2 = StateObserver(keysBackup2)
var isSuccessful = false
@@ -1056,8 +1088,8 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(isSuccessful)
// Backup state must be NotTrusted
- assertEquals(KeysBackupState.NotTrusted, keysBackup2.state)
- assertFalse(keysBackup2.isEnabled)
+ assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.state)
+ assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
// - Validate the old device from the new one
aliceSession2.cryptoService().setDeviceVerification(
@@ -1103,6 +1135,10 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun deleteKeysBackupTest() {
+ val testHelper = CommonTestHelper(context())
+ val cryptoTestHelper = CryptoTestHelper(testHelper)
+ val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
+
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
index 90e7fc1e45..2220536e28 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -106,14 +106,14 @@ internal class KeysBackupTestHelper(
Assert.assertNotNull(megolmBackupCreationInfo)
- Assert.assertFalse(keysBackup.isEnabled)
+ Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled)
// Create the version
val keysVersion = testHelper.doSync {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
- Assert.assertNotNull(keysVersion.version)
+ Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
// Backup must be enable now
Assert.assertTrue(keysBackup.isEnabled)
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/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
index 44ac439d7b..ae65963f37 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
@@ -22,15 +22,15 @@ package org.matrix.android.sdk.api.logger
* val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
* Timber.tag(loggerTag.value).v("My log message")
*/
-open class LoggerTag(_value: String, parentTag: LoggerTag? = null) {
+open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
object SYNC : LoggerTag("SYNC")
object VOIP : LoggerTag("VOIP")
object CRYPTO : LoggerTag("CRYPTO")
val value: String = if (parentTag == null) {
- _value
+ name
} else {
- "${parentTag.value}/$_value"
+ "${parentTag.value}/$name"
}
}
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/keysbackup/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
index 219a328cfd..7127c8d3f4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
@@ -16,25 +16,35 @@
package org.matrix.android.sdk.api.session.crypto.keysbackup
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
/**
* A signature in a `KeysBackupVersionTrust` object.
*/
-data class KeysBackupVersionTrustSignature(
- /**
- * The id of the device that signed the backup version.
- */
- val deviceId: String?,
- /**
- * The device that signed the backup version.
- * Can be null if the device is not known.
- */
- val device: CryptoDeviceInfo?,
+sealed class KeysBackupVersionTrustSignature {
- /**
- * Flag to indicate the signature from this device is valid.
- */
- val valid: Boolean,
-)
+ data class DeviceSignature(
+ /**
+ * The id of the device that signed the backup version.
+ */
+ val deviceId: String?,
+
+ /**
+ * The device that signed the backup version.
+ * Can be null if the device is not known.
+ */
+ val device: CryptoDeviceInfo?,
+
+ /**
+ * Flag to indicate the signature from this device is valid.
+ */
+ val valid: Boolean) : KeysBackupVersionTrustSignature()
+
+ data class UserSignature(
+ val keyId: String?,
+ val cryptoCrossSigningKey: CryptoCrossSigningKey?,
+ val valid: Boolean
+ ) : KeysBackupVersionTrustSignature()
+}
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/presence/model/PresenceEnum.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
index 6d9994ef1c..c678e2a706 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
@@ -28,7 +28,10 @@ enum class PresenceEnum(val value: String) {
OFFLINE("offline"),
@Json(name = "unavailable")
- UNAVAILABLE("unavailable");
+ UNAVAILABLE("unavailable"),
+
+ @Json(name = "org.matrix.msc3026.busy")
+ BUSY("busy");
companion object {
fun from(s: String): PresenceEnum? = values().find { it.value == s }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
index 020e7ed39e..ba274325bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
@@ -27,13 +27,13 @@ import timber.log.Timber
@JsonClass(generateAdapter = true)
data class RoomGuestAccessContent(
// Required. Whether guests can join the room. One of: ["can_join", "forbidden"]
- @Json(name = "guest_access") val _guestAccess: String? = null
+ @Json(name = "guest_access") val guestAccessStr: String? = null
) {
- val guestAccess: GuestAccess? = when (_guestAccess) {
+ val guestAccess: GuestAccess? = when (guestAccessStr) {
"can_join" -> GuestAccess.CanJoin
"forbidden" -> GuestAccess.Forbidden
else -> {
- Timber.w("Invalid value for GuestAccess: `$_guestAccess`")
+ Timber.w("Invalid value for GuestAccess: `$guestAccessStr`")
null
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt
index 3ac14e48de..da5c90ff05 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibilityContent.kt
@@ -22,15 +22,15 @@ import timber.log.Timber
@JsonClass(generateAdapter = true)
data class RoomHistoryVisibilityContent(
- @Json(name = "history_visibility") val _historyVisibility: String? = null
+ @Json(name = "history_visibility") val historyVisibilityStr: String? = null
) {
- val historyVisibility: RoomHistoryVisibility? = when (_historyVisibility) {
+ val historyVisibility: RoomHistoryVisibility? = when (historyVisibilityStr) {
"world_readable" -> RoomHistoryVisibility.WORLD_READABLE
"shared" -> RoomHistoryVisibility.SHARED
"invited" -> RoomHistoryVisibility.INVITED
"joined" -> RoomHistoryVisibility.JOINED
else -> {
- Timber.w("Invalid value for RoomHistoryVisibility: `$_historyVisibility`")
+ Timber.w("Invalid value for RoomHistoryVisibility: `$historyVisibilityStr`")
null
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
index 5237b10d52..7b7582c9a9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
@@ -27,7 +27,7 @@ import timber.log.Timber
*/
@JsonClass(generateAdapter = true)
data class RoomJoinRulesContent(
- @Json(name = "join_rule") val _joinRules: String? = null,
+ @Json(name = "join_rule") val joinRulesStr: String? = null,
/**
* If the allow key is an empty list (or not a list at all),
* then no users are allowed to join without an invite.
@@ -35,14 +35,14 @@ data class RoomJoinRulesContent(
*/
@Json(name = "allow") val allowList: List? = null
) {
- val joinRules: RoomJoinRules? = when (_joinRules) {
+ val joinRules: RoomJoinRules? = when (joinRulesStr) {
"public" -> RoomJoinRules.PUBLIC
"invite" -> RoomJoinRules.INVITE
"knock" -> RoomJoinRules.KNOCK
"private" -> RoomJoinRules.PRIVATE
"restricted" -> RoomJoinRules.RESTRICTED
else -> {
- Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
+ Timber.w("Invalid value for RoomJoinRules: `$joinRulesStr`")
null
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt
index f5f722d783..6487ad947f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt
@@ -47,7 +47,7 @@ class RestrictedRoomPreset(val homeServerCapabilities: HomeServerCapabilities, v
type = EventType.STATE_ROOM_JOIN_RULES,
stateKey = "",
content = RoomJoinRulesContent(
- _joinRules = RoomJoinRules.RESTRICTED.value,
+ joinRulesStr = RoomJoinRules.RESTRICTED.value,
allowList = restrictedList
).toContent()
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
index b45f3ecb71..bdda23b8e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt
@@ -32,6 +32,10 @@ data class TimelineSettings(
* The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
*/
val rootThreadEventId: String? = null,
+ /**
+ * If true Sender Info shown in room will get the latest data information (avatar + displayName)
+ */
+ val useLiveSenderInfo: Boolean = false,
) {
/**
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/api/session/sync/model/LazyRoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt
index 087a5f52dc..5bd7719d01 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/LazyRoomSyncEphemeral.kt
@@ -20,6 +20,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
sealed class LazyRoomSyncEphemeral {
- data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
+ data class Parsed(val roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
object Stored : LazyRoomSyncEphemeral()
}
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..1bec227f1f 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 8c09da72de..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,7 +76,6 @@ 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
@@ -91,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
@@ -156,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,
@@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor,
+ private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy
) : CryptoService {
@@ -192,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) {
@@ -201,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
@@ -379,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)
}
/**
@@ -407,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()
}
@@ -472,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()
}
}
}
@@ -594,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
@@ -787,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 -> {
@@ -837,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)
@@ -1095,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.
@@ -1158,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)
}
/**
@@ -1212,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)
}
/**
@@ -1221,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
*
@@ -1302,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()
}
@@ -1346,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