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 [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](https://f-droid.org/app/im.vector.app) -Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](https://github.com/vector-im/element-android/actions/workflows/nightly.yml) +Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [![allScreensTest](https://github.com/vector-im/element-android/actions/workflows/nightly.yml/badge.svg)](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>, RoomKeyRequestBody>? { + val sender = event.senderId ?: return null + val encryptedEventContent = event.content.toModel() ?: return null.also { + Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") + } + if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + + val senderDevice = encryptedEventContent.deviceId + val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + mapOf( + myUserId to listOf("*") + ) + } else { + if (event.senderId == myUserId) { + mapOf( + myUserId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + myUserId to listOf("*"), + + // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 + // so in this case query to all + sender to listOf(senderDevice ?: "*") + ) + } + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + return recipients to requestBody + } + + private fun ratchetIndexForMessage(event: Event): Int? { + val encryptedContent = event.content.toModel() ?: return null + if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { + tryOrNull { + val megolmVersion = it.read() + if (megolmVersion != 3) return@tryOrNull null + /** Int tag */ + if (it.read() != 8) return@tryOrNull null + it.read() + } + } + } + + fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueRequest(requestBody, recipients, fromIndex, force) + } + } + } + + /** + * Typically called when we the session as been imported or received meanwhile + */ + fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) + } + } + } + + fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { + if (newTrust) { + // we were previously not cross signed, but we are now + // so there is now more chances to get better replies for existing request + // Let's forget about sent request so that next time we try to decrypt we will resend requests + // We don't resend all because we don't want to generate a bulk of traffic + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) + } + + sequencer.post { + delay(1000) + perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) + } + } + } + } + + fun onRoomKeyForwarded(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + fromIndex: Int, + event: Event) { + Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + // strip out encrypted stuff as it's just a trail? + event = event.copy( + type = event.getClearType(), + content = mapOf( + "chain_index" to fromIndex + ) + ) + ) + } + } + } + + fun onRoomKeyWithHeld(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + event: Event) { + outgoingRequestScope.launch { + sequencer.post { + Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") + Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") + + // We want to store withheld code from the sender of the message (owner of the megolm session), not from + // other devices that might gossip the key. If not the initial reason might be overridden + // by a request to one of our session. + event.getClearContent().toModel()?.let { withheld -> + withContext(coroutineDispatchers.crypto) { + tryOrNull { + deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) + } + cryptoStore.getUserDeviceList(event.senderId ?: "") + .also { devices -> + Timber.tag(loggerTag.value) + .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") + } + ?.firstOrNull { + it.identityKey() == senderKey + } + }.also { + Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") + }?.let { + if (it.userId == event.senderId) { + if (fromDevice != null) { + if (it.deviceId == fromDevice) { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } else { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } + } + } + + // Here we store the replies from a given request + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + event = event + ) + } + } + } + + /** + * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those) + */ + fun requireProcessAllPendingKeyRequests() { + outgoingRequestScope.launch { + sequencer.post { + internalProcessPendingKeyRequests() + } + } + } + + private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { + // do we have known requests for that session?? + Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") + val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey + ) + if (knownRequest.isEmpty()) return Unit.also { + Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") + } + if (knownRequest.size > 1) { + // It's worth logging, there should be only one + Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") + } + knownRequest.forEach { request -> + when (request.state) { + OutgoingRoomKeyRequestState.UNSENT -> { + if (request.fromIndex >= localKnownChainIndex) { + // we have a good index we can cancel + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + } + } + OutgoingRoomKeyRequestState.SENT -> { + // It was already sent, and index satisfied we can cancel + if (request.fromIndex >= localKnownChainIndex) { + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // It is already marked to be cancelled + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + if (request.fromIndex >= localKnownChainIndex) { + // we just want to cancel now + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + // was already canceled + // if we need a better index, should we resend? + } + } + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } + + private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { + if (!cryptoStore.isKeyGossipingEnabled()) { + // we might want to try backup? + if (requestBody.roomId != null && requestBody.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) + } + Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") + return + } + + Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") + val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") + when (existing?.state) { + null -> { + // create a new one + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + OutgoingRoomKeyRequestState.UNSENT -> { + // nothing it's new or not yet handled + } + OutgoingRoomKeyRequestState.SENT -> { + // it was already requested + Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") + if (force) { + // update to UNSENT + Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } else { + if (existing.roomId != null && existing.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) + } + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // request is canceled only if I got the keys so what to do here... + if (force) { + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + // It's already going to resend + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + if (force) { + cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + } + } + + if (existing != null && existing.fromIndex >= fromIndex) { + // update the required index + cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) + } + } + + private suspend fun internalProcessPendingKeyRequests() { + val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) + Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") + + measureTimeMillis { + toProcess.forEach { + when (it.state) { + OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, + OutgoingRoomKeyRequestState.SENT -> { + // these are filtered out + } + } + } + }.let { + Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") + } + + val maxBackupCallsBySync = 60 + var currentCalls = 0 + measureTimeMillis { + while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> + // we want to rate limit that somehow :/ + perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) + } + currentCalls++ + } + }.let { + Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") + } + } + + private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { + // In order to avoid generating to_device traffic, we can first check if the key is backed up + Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") + val sessionId = request.sessionId ?: return + val roomId = request.roomId ?: return + if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { + // let's see what's the index + val knownIndex = tryOrNull { + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + } + if (knownIndex != null && knownIndex <= request.fromIndex) { + // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request + Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + return + } + } + + // we need to send the request + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = request.requestBody + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") + // The request was sent, so update state + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") + } + } + + private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { + Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") + // we have to cancel this + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + return try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + // The request cancellation was sent, we don't delete yet because we want + // to keep trace of the sent replies + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") + false + } + } + + private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { + if (handleRequestToCancel(request)) { + // this will create a new unsent request with no replies that will be process in the following call + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt deleted file mode 100755 index 2ba2f5c817..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -internal class OutgoingSecretRequest( - // Secret Name - val secretName: String?, - // list of recipients for the request - override var 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 var requestId: String, - // current state of this request - override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { - - // transaction id for the cancellation, if any -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt new file mode 100644 index 0000000000..1a2f128f25 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt @@ -0,0 +1,133 @@ +/* + * 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 dagger.Lazy +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.util.awaitCallback +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +// I keep the same name as OutgoingGossipingRequestManager to ease filtering of logs +private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.CRYPTO) + +/** + * Used to try to get the key for UISI messages before sending room key request. + * We are adding some rate limiting to avoid querying too much for a key not in backup. + * Nonetheless the backup can be updated so we might want to retry from time to time. + */ +internal class PerSessionBackupQueryRateLimiter @Inject constructor( + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val keysBackupService: Lazy, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, +) { + + companion object { + const val MIN_TRY_BACKUP_PERIOD_MILLIS = 60 * 60_000 // 1 hour + } + + data class Info( + val megolmSessionId: String, + val roomId: String + ) + + data class LastTry( + val backupVersion: String, + val timestamp: Long + ) + + /** + * Remember what we already tried (a key not in backup or some server issue) + * We might want to retry from time to time as the backup could have been updated + */ + private val lastFailureMap = mutableMapOf() + + private var backupVersion: KeysVersionResult? = null + private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null + var backupWasCheckedFromServer: Boolean = false + val now = clock.epochMillis() + + fun refreshBackupInfoIfNeeded(force: Boolean = false) { + if (backupWasCheckedFromServer && !force) return + Timber.tag(loggerTag.value).v("Checking if can access a backup") + backupWasCheckedFromServer = true + val knownBackupSecret = cryptoStore.getKeyBackupRecoveryKeyInfo() + ?: return Unit.also { + Timber.tag(loggerTag.value).v("We don't have the backup secret!") + } + this.backupVersion = keysBackupService.get().keysBackupVersion + this.savedKeyBackupKeyInfo = knownBackupSecret + } + + suspend fun tryFromBackupIfPossible(sessionId: String, roomId: String): Boolean { + Timber.tag(loggerTag.value).v("tryFromBackupIfPossible for session:$sessionId in $roomId") + refreshBackupInfoIfNeeded() + val currentVersion = backupVersion + if (savedKeyBackupKeyInfo?.version == null || + currentVersion == null || + currentVersion.version != savedKeyBackupKeyInfo?.version) { + // We can't access the backup + Timber.tag(loggerTag.value).v("Can't get backup version info") + return false + } + val cacheKey = Info(sessionId, roomId) + val lastTry = lastFailureMap[cacheKey] + val shouldQuery = + lastTry == null || + lastTry.backupVersion != currentVersion.version || + (now - lastTry.timestamp) > MIN_TRY_BACKUP_PERIOD_MILLIS + + if (!shouldQuery) return false + + val successfullyImported = withContext(coroutineDispatchers.io) { + try { + awaitCallback { + keysBackupService.get().restoreKeysWithRecoveryKey( + currentVersion, + savedKeyBackupKeyInfo?.recoveryKey ?: "", + roomId, + sessionId, + null, + it + ) + }.successfullyNumberOfImportedKeys + } catch (failure: Throwable) { + // Fail silently + Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}") + 0 + } + } + if (successfullyImported == 1) { + Timber.tag(loggerTag.value).v("Found key in backup session:$sessionId in $roomId") + lastFailureMap.remove(cacheKey) + return true + } else { + Timber.tag(loggerTag.value).v("Failed to find key in backup session:$sessionId in $roomId") + lastFailureMap[cacheKey] = LastTry(currentVersion.version, clock.epochMillis()) + return false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt index dab806a565..c2f494b4b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt @@ -79,7 +79,7 @@ internal class RoomDecryptorProvider @Inject constructor( newSessionListeners.toList().forEach { try { it.onNewSession(roomId, senderKey, sessionId) - } catch (e: Throwable) { + } catch (ignore: Throwable) { } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt index ba97d96133..1a8c160d9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -16,12 +16,21 @@ package org.matrix.android.sdk.internal.crypto +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject @SessionScope -internal class RoomEncryptorsStore @Inject constructor() { +internal class RoomEncryptorsStore @Inject constructor( + private val cryptoStore: IMXCryptoStore, + private val megolmEncryptionFactory: MXMegolmEncryptionFactory, + private val olmEncryptionFactory: MXOlmEncryptionFactory, +) { // MXEncrypting instance for each room. private val roomEncryptors = mutableMapOf() @@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() { fun get(roomId: String): IMXEncrypting? { return synchronized(roomEncryptors) { - roomEncryptors[roomId] + val cache = roomEncryptors[roomId] + if (cache != null) { + return@synchronized cache + } else { + val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) + MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) + else -> null + } + alg?.let { roomEncryptors.put(roomId, it) } + return@synchronized alg + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt new file mode 100644 index 0000000000..6fb6914206 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) + +@SessionScope +internal class SecretShareManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, +) { + + companion object { + private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes + } + + /** + * Secret gossiping only occurs during a limited window period after interactive verification. + * We keep track of recent verification in memory for that purpose (no need to persist) + */ + private val recentlyVerifiedDevices = mutableMapOf() + private val verifMutex = Mutex() + + /** + * Secrets are exchanged as part of interactive verification, + * so we can just store in memory. + */ + private val outgoingSecretRequests = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + fun addListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfill gossiping requests. + * This should be called as fast as possible after a successful self interactive verification + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + cryptoCoroutineScope.launch { + verifMutex.withLock { + recentlyVerifiedDevices[deviceId] = clock.epochMillis() + } + } + } + + suspend fun handleSecretRequest(toDevice: Event) { + val request = toDevice.getClearContent().toModel() + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request") + } + +// val (action, requestingDeviceId, requestId, secretName) = it + val secretName = request.secretName ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing secret name") + } + + val userId = toDevice.senderId ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing senderId") + } + + if (userId != credentials.userId) { + // secrets are only shared between our own devices + Timber.tag(loggerTag.value) + .e("Ignoring secret share request from other users $userId") + return + } + + val deviceId = request.requestingDeviceId + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request norequestingDeviceId ") + } + + val device = cryptoStore.getUserDevice(credentials.userId, deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value) + .e("Received secret share request from unknown device $deviceId") + } + + val isRequestingDeviceTrusted = device.isVerified + val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) + if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { + // we can share the secret + + val secretValue = when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + } + if (secretValue == null) { + Timber.tag(loggerTag.value) + .i("The secret is unknown $secretName, passing to app layer") + val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } + toList.onEach { listener -> + listener.onSecretShareRequest(request) + } + return + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to mapOf( + "request_id" to request.requestId, + "secret" to secretValue + ) + ) + + // Is it possible that we don't have an olm session? + val devicesByUser = mapOf(device.userId to listOf(device)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Can't share secret ${request.secretName}: Failed to establish olm session") + return + } + + val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") + return + } + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + try { + // raise the retries for secret + sendToDeviceTask.executeRetry(sendToDeviceParams, 6) + Timber.tag(loggerTag.value) + .i("successfully shared secret $secretName to ${device.shortDebugString()}") + // TODO add a trail for that in audit logs + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") + } + } else { + Timber.tag(loggerTag.value) + .d(" Received secret share request from un-authorised device ${device.deviceId}") + } + } + + private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp = verifMutex.withLock { + recentlyVerifiedDevices[deviceId] + } ?: return false + + val age = clock.epochMillis() - verifTimestamp + + return age < SECRET_SHARE_WINDOW_DURATION + } + + suspend fun requestSecretTo(deviceId: String, secretName: String) { + val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { + Timber.tag(loggerTag.value) + .d("Can't request secret for $secretName unknown device $deviceId") + } + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + secretName = secretName, + requestId = createUniqueTxnId() + ) + + verifMutex.withLock { + outgoingSecretRequests.add(toDeviceContent) + } + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.REQUEST_SECRET, + contentMap = contentMap + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value) + .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") + } + } + + suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { + Timber.tag(loggerTag.value) + .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") + if (!toDevice.isEncrypted()) { + // secret send messages must be encrypted + Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (toDevice.senderId != credentials.userId) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") + return + } + + val secretContent = toDevice.getClearContent().toModel() ?: return + + val existingRequest = verifMutex.withLock { + outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } + } + + // As per spec: + // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. + if (existingRequest?.secretName == null) { + Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + // we don't need to cancel the request as we only request to one device + // just forget about the request now + verifMutex.withLock { + outgoingSecretRequests.remove(existingRequest) + } + + if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt deleted file mode 100644 index 3b43ad672b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ /dev/null @@ -1,153 +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.GossipingToDeviceObject -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.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -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 timber.log.Timber -import javax.inject.Inject - -internal class SendGossipRequestWorker(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 keyShareRequest: OutgoingRoomKeyRequest? = null, - val secretShareRequest: OutgoingSecretRequest? = null, - // 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 - - @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 eventType: String - val requestId: String - when { - params.keyShareRequest != null -> { - eventType = EventType.ROOM_KEY_REQUEST - requestId = params.keyShareRequest.requestId - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = credentials.deviceId, - requestId = params.keyShareRequest.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - body = params.keyShareRequest.requestBody - ) - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = clock.epochMillis() - }) - - params.keyShareRequest.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - } - params.secretShareRequest != null -> { - eventType = EventType.REQUEST_SECRET - requestId = params.secretShareRequest.requestId - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - requestId = params.secretShareRequest.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - secretName = params.secretShareRequest.secretName - ) - - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = clock.epochMillis() - }) - - params.secretShareRequest.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - } - else -> { - return buildErrorResult(params, "Unknown empty gossiping request").also { - Timber.e("Unknown empty gossiping request: $params") - } - } - } - try { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = eventType, - contentMap = contentMap, - transactionId = txnId - ) - ) - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) - 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/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt deleted file mode 100644 index 113d71d387..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ /dev/null @@ -1,170 +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.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.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 timber.log.Timber -import javax.inject.Inject - -internal class SendGossipWorker( - 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 secretValue: String, - val requestUserId: String?, - val requestDeviceId: String?, - val requestId: String?, - // 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 - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - @Inject lateinit var messageEncrypter: MessageEncrypter - @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction - @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 eventType: String = EventType.SEND_SECRET - - val toDeviceContent = SecretSendEventContent( - requestId = params.requestId ?: "", - secretValue = params.secretValue - ) - - val requestingUserId = params.requestUserId ?: "" - val requestingDeviceId = params.requestDeviceId ?: "" - val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) - ?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.requestDeviceId}") - } - - val sendToDeviceMap = MXUsersDevicesMap() - - val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - return buildErrorResult(params, "no session with this device").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("no session with this device $requestingDeviceId, probably because there were no one-time keys.") - } - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to toDeviceContent.toContent() - ) - - try { - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) - } catch (failure: Throwable) { - Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") - } - - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = clock.epochMillis() - }) - - try { - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = EventType.ENCRYPTED, - contentMap = sendToDeviceMap, - transactionId = txnId - ) - ) - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.ACCEPTED - ) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - 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/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index 86674b4ac5..22c4e59b18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -17,12 +17,13 @@ package org.matrix.android.sdk.internal.crypto.actions import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class MegolmSessionDataImporter @Inject constructor( - private val olmDevice: MXOlmDevice, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val cryptoStore: IMXCryptoStore, - private val clock: Clock, +private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO) + +internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, ) { /** @@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor( if (null != decrypting) { try { val sessionId = megolmSessionData.sessionId - Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId") totalNumbersOfImportedKeys++ // cancel any outstanding room key requests for this session - val roomKeyRequestBody = RoomKeyRequestBody( - algorithm = megolmSessionData.algorithm, - roomId = megolmSessionData.roomId, - senderKey = megolmSessionData.senderKey, - sessionId = megolmSessionData.sessionId - ) - outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}") + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded( + megolmSessionData.sessionId ?: "", + megolmSessionData.roomId ?: "", + megolmSessionData.senderKey ?: "", + tryOrNull { + olmInboundGroupSessionWrappers + .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } + ?.firstKnownIndex?.toInt() + } ?: 0 + ) // Have another go at decrypting events sent with this session when (decrypting) { @@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor( } } } catch (e: Exception) { - Timber.e(e, "## importRoomKeys() : onNewSession failed") + Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed") } } @@ -109,7 +115,7 @@ internal class MegolmSessionDataImporter @Inject constructor( val t1 = clock.epochMillis() - Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 2ea4e1dd29..d555062442 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.crypto.algorithms import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService @@ -44,23 +42,4 @@ internal interface IMXDecrypting { * @param event the key event. */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} - - /** - * Determine if we have the keys necessary to respond to a room key request - * - * @param request keyRequest - * @return true if we have the keys and could (theoretically) share - */ - fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false - - /** - * Send the response to a room key request. - * - * @param request keyRequest - */ - fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} - - fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {} - - fun requestKeysForEvent(event: Event, withHeld: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 8ba4f6b56b..d65b05f655 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,51 +17,32 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest 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.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.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.toModel -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -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.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService 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.StreamEventsManager import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) -internal class MXMegolmDecryption(private val userId: String, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val liveEventManager: Lazy -) : IMXDecrypting, IMXWithHeldExtension { +internal class MXMegolmDecryption( + private val olmDevice: MXOlmDevice, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val liveEventManager: Lazy +) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - // If cross signing is enabled, we don't send request until the keys are trusted - // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once - val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true - return decryptEvent(event, timeline, requestOnFail) + return decryptEvent(event, timeline, true) } @Throws(MXCryptoError::class) @@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String, if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // addEventToPendingList(event, timeline) - // The session might has been partially withheld (and only pass ratcheted) + // So we know that session, but it's ratcheted and we can't decrypt at that index + + if (requestKeysOnFail) { + requestKeysForEvent(event) + } + // Check if partially withheld val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event, true) - } // Encapsulate as withHeld exception throw MXCryptoError.Base( MXCryptoError.ErrorType.KEYS_WITHHELD, @@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String, ) } - if (requestKeysOnFail) { - requestKeysForEvent(event, false) - } - throw MXCryptoError.Base( MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, "UNKNOWN_MESSAGE_INDEX", @@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String, ) } if (throwable is MXCryptoError.Base) { - if ( - /** if the session is unknown*/ - throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID - ) { + if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + // Check if it was withheld by sender to enrich error code val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { if (requestKeysOnFail) { - requestKeysForEvent(event, true) + requestKeysForEvent(event) } // Encapsulate as withHeld exception throw MXCryptoError.Base( MXCryptoError.ErrorType.KEYS_WITHHELD, withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } else { - // This is un-used in Matrix Android SDK2, not sure if needed - // addEventToPendingList(event, timeline) - if (requestKeysOnFail) { - requestKeysForEvent(event, false) - } + withHeldInfo.reason) + } + + if (requestKeysOnFail) { + requestKeysForEvent(event) } } } @@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - val sender = event.senderId ?: return - val encryptedEventContent = event.content.toModel() - val senderDevice = encryptedEventContent?.deviceId ?: return - - val recipients = if (event.senderId == userId || withHeld) { - mapOf( - userId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - userId to listOf("*"), - sender to listOf(senderDevice) - ) - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - - outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) + private fun requestKeysForEvent(event: Event) { + outgoingKeyRequestManager.requestKeyForEvent(event, false) } -// /** -// * Add an event to the list of those we couldn't decrypt the first time we -// * saw them. -// * -// * @param event the event to try to decrypt later -// * @param timelineId the timeline identifier -// */ -// private fun addEventToPendingList(event: Event, timelineId: String) { -// val encryptedEventContent = event.content.toModel() ?: return -// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" -// -// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } -// val events = timeline.getOrPut(timelineId) { ArrayList() } -// -// if (event !in events) { -// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") -// events.add(event) -// } -// } - /** * Handle a key event. * @@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String, return } if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + return + } Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") val forwardedRoomKeyContent = event.getClearContent().toModel() ?: return @@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String, } Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val added = olmDevice.addInboundGroupSession( + val addSessionResult = olmDevice.addInboundGroupSession( roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, @@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String, exportFormat ) - if (added) { + when (addSessionResult) { + is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex + is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex + else -> null + }?.let { index -> + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey -> + cryptoStore.getUserDeviceList(event.senderId ?: "") + ?.firstOrNull { + it.identityKey() == senderDeviceIdentityKey + } + }?.deviceId + + outgoingKeyRequestManager.onRoomKeyForwarded( + sessionId = roomKeyContent.sessionId, + algorithm = roomKeyContent.algorithm ?: "", + roomId = roomKeyContent.roomId, + senderKey = senderKey, + fromIndex = index, + fromDevice = fromDevice, + event = event) + + cryptoStore.saveIncomingForwardKeyAuditTrail( + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey, + algorithm = roomKeyContent.algorithm ?: "", + userId = event.senderId ?: "", + deviceId = fromDevice ?: "", + chainIndex = index.toLong()) + + // The index is used to decide if we cancel sent request or if we wait for a better key + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index) + } + } + + if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { + Timber.tag(loggerTag.value) + .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() - val content = RoomKeyRequestBody( - algorithm = roomKeyContent.algorithm, - roomId = roomKeyContent.roomId, - sessionId = roomKeyContent.sessionId, - senderKey = senderKey - ) - - outgoingGossipingRequestManager.cancelRoomKeyRequest(content) - onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) } } @@ -343,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String, Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") newSessionListener?.onNewSession(roomId, senderKey, sessionId) } - - override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { - val roomId = request.requestBody?.roomId ?: return false - val senderKey = request.requestBody.senderKey ?: return false - val sessionId = request.requestBody.sessionId ?: return false - return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) - } - - override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { - // sanity checks - if (request.requestBody == null) { - return - } - val userId = request.userId ?: return - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val body = request.requestBody - val sessionHolder = try { - olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body") - return@launch - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys() - } ?: return@launch Unit.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}") - } - - runCatching { deviceListManager.downloadKeys(listOf(userId), false) } - .mapCatching { - val deviceId = request.deviceId - val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") - if (deviceInfo == null) { - throw RuntimeException() - } else { - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.") - return@mapCatching - } - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - sendToDeviceTask.execute(sendToDeviceParams) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") - } - } - } - } - } - - override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.addWithHeldMegolmSession(withHeldInfo) - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 8ad9f191d7..096773a959 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -17,46 +17,24 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -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.OutgoingKeyRequestManager 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.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( - @UserId private val userId: String, private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, private val eventsManager: Lazy ) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - userId, olmDevice, - deviceListManager, - outgoingGossipingRequestManager, - messageEncrypter, - ensureOlmSessionsForDevicesAction, + outgoingKeyRequestManager, cryptoStore, - sendToDeviceTask, - coroutineDispatchers, - cryptoCoroutineScope, - eventsManager - ) + eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 4ceef4fa5c..bd65482f61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.forEach import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event 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 @@ -80,7 +79,7 @@ internal class MXMegolmEncryption( } // Default rotation periods - // TODO: Make it configurable via parameters + // TODO Make it configurable via parameters // Session rotation periods private var sessionRotationPeriodMsgs: Int = 100 private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000 @@ -285,25 +284,14 @@ internal class MXMegolmEncryption( // attempted to share with) rather than the contentMap (those we did // share with), because we don't want to try to claim a one-time-key // for dead devices on every message. - val gossipingEventBuffer = arrayListOf() - for ((userId, devicesToShareWith) in devicesByUser) { + for ((_, devicesToShareWith) in devicesByUser) { for (deviceInfo in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) - gossipingEventBuffer.add( - Event( - type = EventType.ROOM_KEY, - senderId = myUserId, - content = submap.apply { - this["session_key"] = "" - // we add a fake key for trail - this["_dest"] = "$userId|${deviceInfo.deviceId}" - } - )) + // XXX is it needed to add it to the audit trail? + // For now decided that no, we are more interested by forward trail } } - cryptoStore.saveGossipingEvents(gossipingEventBuffer) - if (haveTargets) { t0 = clock.epochMillis() Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") @@ -346,7 +334,8 @@ internal class MXMegolmEncryption( senderKey = senderKey, algorithm = MXCRYPTO_ALGORITHM_MEGOLM, sessionId = sessionId, - codeString = code.value + codeString = code.value, + fromDevice = myDeviceId ) val params = SendToDeviceTask.Params( EventType.ROOM_KEY_WITHHELD, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 13eeefd1a6..1e66fe84c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -265,8 +265,4 @@ internal class MXOlmDecryption( return res["payload"] } - - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - // nop - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt index c842c54041..fcb8309828 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -38,7 +38,7 @@ internal class MXOlmEncryption( override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content { // pick the list of recipients based on the membership list. // - // TODO: there is a race condition here! What if a new user turns up + // TODO there is a race condition here! What if a new user turns up ensureSession(userIds) val deviceInfos = ArrayList() for (userId in userIds) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt new file mode 100644 index 0000000000..4fa355cd2a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility +import javax.inject.Inject + +/** + * Holds the OlmPkSigning for cross signing. + * Can be injected without having to get the full cross signing service + */ +@SessionScope +internal class CrossSigningOlm @Inject constructor( + private val cryptoStore: IMXCryptoStore, +) { + + enum class KeyType { + SELF, + USER, + MASTER + } + + var olmUtility: OlmUtility = OlmUtility() + + var masterPkSigning: OlmPkSigning? = null + var userPkSigning: OlmPkSigning? = null + var selfSigningPkSigning: OlmPkSigning? = null + + fun release() { + olmUtility.releaseUtility() + listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } + } + + fun signObject(type: KeyType, strToSign: String): Map { + val myKeys = cryptoStore.getMyCrossSigningInfo() + val pubKey = when (type) { + KeyType.SELF -> myKeys?.selfSigningKey() + KeyType.USER -> myKeys?.userKey() + KeyType.MASTER -> myKeys?.masterKey() + }?.unpaddedBase64PublicKey + val pkSigning = when (type) { + KeyType.SELF -> selfSigningPkSigning + KeyType.USER -> userPkSigning + KeyType.MASTER -> masterPkSigning + } + if (pubKey == null || pkSigning == null) { + throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning") + } + val signature = pkSigning.sign(strToSign) + return mapOf( + "ed25519:$pubKey" to signature + ) + } + + fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map>) { + val myKeys = cryptoStore.getMyCrossSigningInfo() + ?: throw NoSuchElementException("Cross Signing not configured") + val myUserID = myKeys.userId + val pubKey = when (type) { + KeyType.SELF -> myKeys.selfSigningKey() + KeyType.USER -> myKeys.userKey() + KeyType.MASTER -> myKeys.masterKey() + }?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured") + val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me + ?.get("ed25519:$pubKey") + + if (signaturesMadeByMyKey.isNullOrBlank()) { + throw IllegalArgumentException("Not signed with my key $type") + } + + // Check that Alice USK signature of Bob MSK is valid + olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index e2887e5826..f4b389846c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask @@ -53,7 +54,6 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.logLimit import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning -import org.matrix.olm.OlmUtility import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -70,19 +70,14 @@ internal class DefaultCrossSigningService @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, private val workManagerProvider: WorkManagerProvider, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val crossSigningOlm: CrossSigningOlm, private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository ) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { - private var olmUtility: OlmUtility? = null - - private var masterPkSigning: OlmPkSigning? = null - private var userPkSigning: OlmPkSigning? = null - private var selfSigningPkSigning: OlmPkSigning? = null - init { try { - olmUtility = OlmUtility() // Try to get stored keys if they exist cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo -> @@ -95,7 +90,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?.let { privateKeySeed -> val pkSigning = OlmPkSigning() if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - masterPkSigning = pkSigning + crossSigningOlm.masterPkSigning = pkSigning Timber.i("## CrossSigning - Loading master key success") } else { Timber.w("## CrossSigning - Public master key does not match the private key") @@ -108,7 +103,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?.let { privateKeySeed -> val pkSigning = OlmPkSigning() if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - userPkSigning = pkSigning + crossSigningOlm.userPkSigning = pkSigning Timber.i("## CrossSigning - Loading User Signing key success") } else { Timber.w("## CrossSigning - Public User key does not match the private key") @@ -121,7 +116,7 @@ internal class DefaultCrossSigningService @Inject constructor( ?.let { privateKeySeed -> val pkSigning = OlmPkSigning() if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - selfSigningPkSigning = pkSigning + crossSigningOlm.selfSigningPkSigning = pkSigning Timber.i("## CrossSigning - Loading Self Signing key success") } else { Timber.w("## CrossSigning - Public Self Signing key does not match the private key") @@ -143,8 +138,7 @@ internal class DefaultCrossSigningService @Inject constructor( } fun release() { - olmUtility?.releaseUtility() - listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } + crossSigningOlm.release() deviceListManager.removeListener(this) } @@ -177,9 +171,9 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.setMyCrossSigningInfo(crossSigningInfo) setUserKeysAsTrusted(userId, true) cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK) - masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } - userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } - selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } + crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) } + crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) } + crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) } callback.onSuccess(Unit) } @@ -198,8 +192,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - masterPkSigning?.releaseSigning() - masterPkSigning = pkSigning + crossSigningOlm.masterPkSigning?.releaseSigning() + crossSigningOlm.masterPkSigning = pkSigning Timber.i("## CrossSigning - Loading MSK success") cryptoStore.storeMSKPrivateKey(mskPrivateKey) return @@ -225,8 +219,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - selfSigningPkSigning?.releaseSigning() - selfSigningPkSigning = pkSigning + crossSigningOlm.selfSigningPkSigning?.releaseSigning() + crossSigningOlm.selfSigningPkSigning = pkSigning Timber.i("## CrossSigning - Loading SSK success") cryptoStore.storeSSKPrivateKey(sskPrivateKey) return @@ -252,8 +246,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - userPkSigning?.releaseSigning() - userPkSigning = pkSigning + crossSigningOlm.userPkSigning?.releaseSigning() + crossSigningOlm.userPkSigning = pkSigning Timber.i("## CrossSigning - Loading USK success") cryptoStore.storeUSKPrivateKey(uskPrivateKey) return @@ -282,8 +276,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { - masterPkSigning?.releaseSigning() - masterPkSigning = pkSigning + crossSigningOlm.masterPkSigning?.releaseSigning() + crossSigningOlm.masterPkSigning = pkSigning masterKeyIsTrusted = true Timber.i("## CrossSigning - Loading master key success") } else { @@ -299,8 +293,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { - userPkSigning?.releaseSigning() - userPkSigning = pkSigning + crossSigningOlm.userPkSigning?.releaseSigning() + crossSigningOlm.userPkSigning = pkSigning userKeyIsTrusted = true Timber.i("## CrossSigning - Loading master key success") } else { @@ -316,8 +310,8 @@ internal class DefaultCrossSigningService @Inject constructor( val pkSigning = OlmPkSigning() try { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { - selfSigningPkSigning?.releaseSigning() - selfSigningPkSigning = pkSigning + crossSigningOlm.selfSigningPkSigning?.releaseSigning() + crossSigningOlm.selfSigningPkSigning = pkSigning selfSignedKeyIsTrusted = true Timber.i("## CrossSigning - Loading master key success") } else { @@ -405,7 +399,11 @@ internal class DefaultCrossSigningService @Inject constructor( // Check that Alice USK signature of Bob MSK is valid try { - olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature( + masterKeySignaturesMadeByMyUserKey, + myUserKey.unpaddedBase64PublicKey, + otherMasterKey.canonicalSignable() + ) } catch (failure: Throwable) { return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) } @@ -459,7 +457,7 @@ internal class DefaultCrossSigningService @Inject constructor( if (potentialDevice != null && potentialDevice.isVerified) { // Check signature validity? try { - olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) isMaterKeyTrusted = true return@forEach } catch (failure: Throwable) { @@ -488,7 +486,11 @@ internal class DefaultCrossSigningService @Inject constructor( // Check that Alice USK signature of Alice MSK is valid try { - olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature( + userKeySignaturesMadeByMyMasterKey, + myMasterKey.unpaddedBase64PublicKey, + myUserKey.canonicalSignable() + ) } catch (failure: Throwable) { return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) } @@ -507,7 +509,11 @@ internal class DefaultCrossSigningService @Inject constructor( // Check that Alice USK signature of Alice MSK is valid try { - olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature( + ssKeySignaturesMadeByMyMasterKey, + myMasterKey.unpaddedBase64PublicKey, + mySSKey.canonicalSignable() + ) } catch (failure: Throwable) { return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) } @@ -560,7 +566,7 @@ internal class DefaultCrossSigningService @Inject constructor( return@launch } val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey - if (userPubKey == null || userPkSigning == null) { + if (userPubKey == null || crossSigningOlm.userPkSigning == null) { callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) return@launch } @@ -569,7 +575,7 @@ internal class DefaultCrossSigningService @Inject constructor( val newSignature = JsonCanonicalizer.getCanonicalJson( Map::class.java, otherMasterKeys.signalableJSONDictionary() - ).let { userPkSigning?.sign(it) } + ).let { crossSigningOlm.userPkSigning?.sign(it) } if (newSignature == null) { // race?? @@ -616,13 +622,13 @@ internal class DefaultCrossSigningService @Inject constructor( } val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey - if (ssPubKey == null || selfSigningPkSigning == null) { + if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) { callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) return@launch } // Sign with self signing - val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable()) if (newSignature == null) { // race?? @@ -695,7 +701,11 @@ internal class DefaultCrossSigningService @Inject constructor( // Check bob's device is signed by bob's SSK try { - olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature( + otherSSKSignature, + otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, + otherDevice.canonicalSignable() + ) } catch (e: Throwable) { return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) } @@ -745,7 +755,11 @@ internal class DefaultCrossSigningService @Inject constructor( // Check bob's device is signed by bob's SSK try { - olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + crossSigningOlm.olmUtility.verifyEd25519Signature( + otherSSKSignature, + otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, + otherDevice.canonicalSignable() + ) } catch (e: Throwable) { return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e)) } @@ -785,7 +799,8 @@ internal class DefaultCrossSigningService @Inject constructor( // If it's me, recheck trust of all users and devices? val users = ArrayList() if (otherUserId == userId && currentTrust != trusted) { -// reRequestAllPendingRoomKeyRequest() + // notify key requester + outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { users.add(it) checkUserTrust(it).isVerified() @@ -800,19 +815,4 @@ internal class DefaultCrossSigningService @Inject constructor( } } } - -// private fun reRequestAllPendingRoomKeyRequest() { -// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") -// cryptoStore.getOutgoingRoomKeyRequests().forEach { -// it.requestBody?.let { requestBody -> -// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { -// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) -// } else { -// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) -// } -// } -// } -// } -// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index e6352809c4..9754e1e7c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.ObjectSigner import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter +import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody @@ -63,16 +64,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBack import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 @@ -107,21 +103,17 @@ internal class DefaultKeysBackupService @Inject constructor( private val cryptoStore: IMXCryptoStore, private val olmDevice: MXOlmDevice, private val objectSigner: ObjectSigner, + private val crossSigningOlm: CrossSigningOlm, // Actions private val megolmSessionDataImporter: MegolmSessionDataImporter, // Tasks private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val deleteBackupTask: DeleteBackupTask, - private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask, - private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, - private val deleteSessionDataTask: DeleteSessionsDataTask, private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, private val getKeysBackupVersionTask: GetKeysBackupVersionTask, private val getRoomSessionDataTask: GetRoomSessionDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getSessionsDataTask: GetSessionsDataTask, - private val storeRoomSessionDataTask: StoreRoomSessionDataTask, - private val storeSessionsDataTask: StoreRoomSessionsDataTask, private val storeSessionDataTask: StoreSessionsDataTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor @@ -168,58 +160,76 @@ internal class DefaultKeysBackupService @Inject constructor( override fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - withContext(coroutineDispatchers.crypto) { - val olmPkDecryption = OlmPkDecryption() - val signalableMegolmBackupAuthData = if (password != null) { - // Generate a private key from the password - val backgroundProgressListener = if (progressListener == null) { - null - } else { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - try { - progressListener.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "prepareKeysBackupVersion: onProgress failure") - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + try { + val olmPkDecryption = OlmPkDecryption() + val signalableMegolmBackupAuthData = if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") } } } } - - val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) - SignalableMegolmBackupAuthData( - publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), - privateKeySalt = generatePrivateKeyResult.salt, - privateKeyIterations = generatePrivateKeyResult.iterations - ) - } else { - val publicKey = olmPkDecryption.generateKey() - - SignalableMegolmBackupAuthData( - publicKey = publicKey - ) } - - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) - - val signedMegolmBackupAuthData = MegolmBackupAuthData( - publicKey = signalableMegolmBackupAuthData.publicKey, - privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, - privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, - signatures = objectSigner.signObject(canonicalJson) + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + SignalableMegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations ) - - MegolmBackupCreationInfo( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, - authData = signedMegolmBackupAuthData, - recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + } else { + val publicKey = olmPkDecryption.generateKey() + SignalableMegolmBackupAuthData( + publicKey = publicKey ) } - }.foldToCallback(callback) + + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) + + val signatures = mutableMapOf>() + + val deviceSignature = objectSigner.signObject(canonicalJson) + deviceSignature.forEach { (userID, content) -> + signatures[userID] = content.toMutableMap() + } + + // If we have cross signing add signature, will throw if cross signing not properly configured + try { + val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson) + signatures[credentials.userId]?.putAll(crossSign) + } catch (failure: Throwable) { + // ignore and log + Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys") + } + + val signedMegolmBackupAuthData = MegolmBackupAuthData( + publicKey = signalableMegolmBackupAuthData.publicKey, + privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, + privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, + signatures = signatures + ) + val creationInfo = MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) + uiHandler.post { + callback.onSuccess(creationInfo) + } + } catch (failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } } } @@ -267,41 +277,39 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun deleteBackup(version: String, callback: MatrixCallback?) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeysBackupVersion so this is symmetrical). - if (keysBackupVersion != null && version == keysBackupVersion?.version) { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Unknown - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (keysBackupVersion != null && version == keysBackupVersion?.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } - deleteBackupTask - .configureWith(DeleteBackupTask.Params(version)) { - this.callback = object : MatrixCallback { - private fun eventuallyRestartBackup() { - // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (state == KeysBackupState.Unknown) { - checkAndStartKeysBackup() - } - } - - override fun onSuccess(data: Unit) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onSuccess(Unit) } - } - - override fun onFailure(failure: Throwable) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onFailure(failure) } + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() } } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } + } } - .executeBy(taskExecutor) - } + } + .executeBy(taskExecutor) } } @@ -339,6 +347,10 @@ internal class DefaultKeysBackupService @Inject constructor( override fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback?) { + if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) { + callback?.onFailure(Throwable("Backup not enabled")) + return + } // Get a status right now getBackupProgress(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { @@ -427,18 +439,41 @@ internal class DefaultKeysBackupService @Inject constructor( for ((keyId, mySignature) in mySigs) { // XXX: is this how we're supposed to get the device id? - var deviceId: String? = null + var deviceOrCrossSigningKeyId: String? = null val components = keyId.split(":") if (components.size == 2) { - deviceId = components[1] + deviceOrCrossSigningKeyId = components[1] } - if (deviceId != null) { - val device = cryptoStore.getUserDevice(userId, deviceId) + // Let's check if it's my master key + val myMSKPKey = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.unpaddedBase64PublicKey + if (deviceOrCrossSigningKeyId == myMSKPKey) { + // we have to check if we can trust + + var isSignatureValid = false + try { + crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures) + isSignatureValid = true + } catch (failure: Throwable) { + Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK") + } + val mskTrusted = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.isVerified() == true + if (isSignatureValid && mskTrusted) { + keysBackupVersionTrustIsUsable = true + } + val signature = KeysBackupVersionTrustSignature.UserSignature( + keyId = deviceOrCrossSigningKeyId, + cryptoCrossSigningKey = cryptoStore.getMyCrossSigningInfo()?.masterKey(), + valid = isSignatureValid + ) + + keysBackupVersionTrustSignatures.add(signature) + } else if (deviceOrCrossSigningKeyId != null) { + val device = cryptoStore.getUserDevice(userId, deviceOrCrossSigningKeyId) var isSignatureValid = false if (device == null) { - Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId") + Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId") } else { val fingerprint = device.fingerprint() if (fingerprint != null) { @@ -455,8 +490,8 @@ internal class DefaultKeysBackupService @Inject constructor( } } - val signature = KeysBackupVersionTrustSignature( - deviceId = deviceId, + val signature = KeysBackupVersionTrustSignature.DeviceSignature( + deviceId = deviceOrCrossSigningKeyId, device = device, valid = isSignatureValid, ) @@ -480,10 +515,11 @@ internal class DefaultKeysBackupService @Inject constructor( if (authData == null) { Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() @@ -536,11 +572,15 @@ internal class DefaultKeysBackupService @Inject constructor( checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - callback.onSuccess(data) + uiHandler.post { + callback.onSuccess(data) + } } override fun onFailure(failure: Throwable) { - callback.onFailure(failure) + uiHandler.post { + callback.onFailure(failure) + } } } } @@ -554,15 +594,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val isValid = withContext(coroutineDispatchers.crypto) { - isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) if (!isValid) { Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") - - callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } } else { trustKeysBackupVersion(keysBackupVersion, true, callback) } @@ -574,15 +613,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val recoveryKey = withContext(coroutineDispatchers.crypto) { - recoveryKeyFromPassword(password, keysBackupVersion, null) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) if (recoveryKey == null) { Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { // Check trust using the recovery key trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) @@ -593,30 +631,28 @@ internal class DefaultKeysBackupService @Inject constructor( override fun onSecretKeyGossip(secret: String) { Timber.i("## CrossSigning - onSecretKeyGossip") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { try { - when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) { - KeysBackupLastVersionResult.NoKeysBackup -> { - Timber.d("No keys backup found") - } - is KeysBackupLastVersionResult.KeysBackup -> { - val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult - val recoveryKey = computeRecoveryKey(secret.fromBase64()) - if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - awaitCallback { - trustKeysBackupVersion(keysBackupVersion, true, it) - } - val importResult = awaitCallback { - restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) - } - withContext(coroutineDispatchers.crypto) { - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) - } - Timber.i("onSecretKeyGossip: Recovered keys $importResult") - } else { - Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult() + ?: return@launch Unit.also { + Timber.d("Failed to get backup last version") } + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback { + trustKeysBackupVersion(keysBackupVersion, true, it) } + // we don't want to start immediately downloading all as it can take very long + +// val importResult = awaitCallback { +// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) +// } + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } + Timber.i("onSecretKeyGossip: saved valid backup key") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") } } catch (failure: Throwable) { Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") @@ -679,9 +715,9 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { - val decryption = withContext(coroutineDispatchers.crypto) { + val decryption = withContext(coroutineDispatchers.computation) { // Check if the recovery is valid before going any further if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") @@ -754,7 +790,19 @@ internal class DefaultKeysBackupService @Inject constructor( } result } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -766,7 +814,7 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { val progressListener = if (stepProgressListener != null) { object : ProgressListener { @@ -791,7 +839,19 @@ internal class DefaultKeysBackupService @Inject constructor( restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) } } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -817,12 +877,16 @@ internal class DefaultKeysBackupService @Inject constructor( ) } else if (roomId != null) { // Get all keys for the room - val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + val data = withContext(coroutineDispatchers.io) { + getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + } // Convert to KeysBackupData KeysBackupData(mutableMapOf(roomId to data)) } else { // Get all keys - getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + withContext(coroutineDispatchers.io) { + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } } } @@ -1198,7 +1262,6 @@ internal class DefaultKeysBackupService @Inject constructor( Timber.v("backupKeys: Invalid configuration") backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) resetBackupAllGroupSessionsListeners() - return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt index deec8b1b3c..d699950e4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/repository/WarnOnUnknownDeviceRepository.kt @@ -22,7 +22,7 @@ import javax.inject.Inject @SessionScope internal class WarnOnUnknownDeviceRepository @Inject constructor() { - // TODO: set it back to true by default. Need UI + // TODO set it back to true by default. Need UI // Warn the user if some new devices are detected while encrypting a message. private var warnOnUnknownDevices = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index dc50afe67d..1ded9c6c7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption @@ -57,7 +57,7 @@ import kotlin.experimental.and internal class DefaultSharedSecretStorageService @Inject constructor( @UserId private val userId: String, private val accountDataService: SessionAccountDataService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val secretShareManager: SecretShareManager, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { @@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return IntegrityResult.Success(keyInfo.content.passphrase != null) } - override fun requestSecret(name: String, myOtherDeviceId: String) { - outgoingGossipingRequestManager.sendSecretShareRequest( - name, - mapOf(userId to listOf(myOtherDeviceId)) - ) + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { + secretShareManager.requestSecretTo(myOtherDeviceId, name) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 8bedb78808..d720777ae1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -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.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.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper @@ -82,6 +81,15 @@ internal interface IMXCryptoStore { */ 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 + /** * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. * @@ -125,18 +133,6 @@ internal interface IMXCryptoStore { */ fun getDeviceTrackingStatuses(): Map - /** - * @return the pending IncomingRoomKeyRequest requests - */ - fun getPendingIncomingRoomKeyRequests(): List - - fun getPendingIncomingGossipingRequests(): List - - fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) - - fun storeIncomingGossipingRequests(requests: List) -// fun getPendingIncomingSecretShareRequests(): List - /** * Indicate if the store contains data for the passed account. * @@ -377,7 +373,9 @@ internal interface IMXCryptoStore { * @param requestBody the request body * @return an OutgoingRoomKeyRequest instance or null */ - fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List /** * Look for an existing outgoing room key request, and if none is found, add a new one. @@ -385,39 +383,59 @@ internal interface IMXCryptoStore { * @param request the request * @return either the same instance as passed in, or the existing one. */ - fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest + fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) + fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) + fun updateOutgoingRoomKeyReply( + roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) - fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? + fun deleteOutgoingRoomKeyRequest(requestId: String) + fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) - fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event)) + fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String + ) - fun saveGossipingEvents(events: List) + fun saveWithheldAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String + ) - fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { - updateGossipingRequestState( - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - state = state - ) - } + fun saveForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) - fun updateGossipingRequestState(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) - - /** - * Search an IncomingRoomKeyRequest - * - * @param userId the user id - * @param deviceId the device id - * @param requestId the request id - * @return an IncomingRoomKeyRequest if it exists, else null - */ - fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? - - fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + fun saveIncomingForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) fun addNewSessionListener(listener: NewSessionListener) @@ -477,17 +495,15 @@ internal interface IMXCryptoStore { fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap // Dev tools - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> - fun getOutgoingSecretKeyRequests(): List - fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? - fun getIncomingRoomKeyRequests(): List - fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEvents(): List + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun logDbUsageInfo() + fun getOutgoingRoomKeyRequests(inStates: Set): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index a509315e7a..d5750a2e2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration import io.realm.Sort +import io.realm.kotlin.createObject import io.realm.kotlin.where import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -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.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -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.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper @@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity @@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.DeviceId @@ -106,6 +104,8 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject +private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) + @SessionScope internal class RealmCryptoStore @Inject constructor( @CryptoDatabase private val realmConfiguration: RealmConfiguration, @@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) - .findFirst() - ?.let { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) + .findAll() + .mapNotNull { CryptoMapper.mapToModel(it) } + .firstOrNull { + it.identityKey() == identityKey } } } @@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - senderKey = session.senderKey - putInboundGroupSession(session) - } + val existing = realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() - realm.insertOrUpdate(realmOlmInboundGroupSession) + if (existing != null) { + // we want to keep the existing backup status + existing.putInboundGroupSession(session) + } else { + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.senderKey + putInboundGroupSession(session) + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } } } @@ -879,18 +889,33 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { + doRealmTransaction(realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { + val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() val key = OlmInboundGroupSessionEntity.createPrimaryKey( - olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), + sessionIdentifier, olmInboundGroupSessionWrapper.senderKey ) - it.where() + val existing = realm.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() - ?.backedUp = true + + if (existing != null) { + existing.backedUp = true + } else { + // ... might be in cache but not yet persisted, create a record to persist backedup state + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = olmInboundGroupSessionWrapper.senderKey + putInboundGroupSession(olmInboundGroupSessionWrapper) + backedUp = true + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } catch (e: OlmException) { Timber.e(e, "OlmException") } @@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun enableKeyGossiping(enable: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping = enable + } + } + + override fun isKeyGossipingEnabled(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping + } ?: true + } + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { return doWithRealm(realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices @@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor( ?: defaultValue } - override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.roomId == requestBody.roomId && @@ -1024,41 +1062,37 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) - .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull() } - override fun getIncomingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List { + // TODO this annoying we have to load all return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + }.map { + it.toOutgoingKeyRequest() + }.filter { + it.requestBody?.algorithm == algorithm && + it.requestBody?.senderKey == senderKey } } - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + override fun getGossipingEventsTrail(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING) + realm.where().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } val dataSourceFactory = realmDataSourceFactory.map { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest - ?: IncomingRoomKeyRequest( - requestBody = null, - deviceId = "", - userId = "", - requestId = "", - state = GossipingRequestState.NONE, - localCreationTimestamp = 0 - ) + AuditTrailMapper.map(it) + // mm we can't map not null... + ?: createUnknownTrail() } return monarchy.findAllPagedWithChanges( realmDataSourceFactory, @@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor( ) } - override fun getGossipingEventsTrail(): LiveData> { + private fun createUnknownTrail() = AuditTrail( + clock.epochMillis(), + TrailType.Unknown, + IncomingKeyRequestInfo( + "", + "", + "", + "", + "", + "", + "", + ) + ) + + override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) + realm.where() + .equalTo(AuditTrailEntityFields.TYPE, type.name) + .sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } - val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } - val trail = monarchy.findAllPagedWithChanges( + val dataSourceFactory = realmDataSourceFactory.map { entity -> + (AuditTrailMapper.map(entity) + // mm we can't map not null... + ?: createUnknownTrail() + ).let { mapper.invoke(it) } + } + return monarchy.findAllPagedWithChanges( realmDataSourceFactory, LivePagedListBuilder( dataSourceFactory, @@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor( .build() ) ) - return trail } - override fun getGossipingEvents(): List { + override fun getGossipingEvents(): List { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - }.map { - it.toModel() + realm.where() + }.mapNotNull { + AuditTrailMapper.map(it) } } - override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, + recipients: Map>, + fromIndex: Int): OutgoingKeyRequest { // Insert the request and return the one passed in parameter - var request: OutgoingRoomKeyRequest? = null + lateinit var request: OutgoingKeyRequest doRealmTransaction(realmConfiguration) { realm -> - val existing = realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + val existing = realm.where() + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) .findAll() - .mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - }.firstOrNull { + .map { + it.toOutgoingKeyRequest() + }.also { + if (it.size > 1) { + // there should be one or zero but not more, worth warning + Timber.tag(loggerTag.value).w("There should not be more than one active key request per session") + } + } + .firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.sessionId == requestBody.sessionId && it.requestBody?.senderKey == requestBody.senderKey && @@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor( } if (existing == null) { - request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply { this.requestId = RequestIdHelper.createUniqueRequestId() this.setRecipients(recipients) - this.requestState = OutgoingGossipingRequestState.UNSENT - this.type = GossipRequestType.KEY - this.requestedInfoStr = requestBody.toJson() - }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + this.requestedIndex = fromIndex + this.requestState = OutgoingRoomKeyRequestState.UNSENT + this.setRequestBody(requestBody) + this.creationTimeStamp = clock.epochMillis() + }.toOutgoingKeyRequest() } else { request = existing } @@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? { - var request: OutgoingSecretRequest? = null - - // Insert the request and return the one passed in parameter + override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { doRealmTransaction(realmConfiguration) { realm -> - val existing = realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) - .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) - .findAll() - .mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingSecretRequest - }.firstOrNull() - if (existing == null) { - request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { - this.type = GossipRequestType.SECRET - setRecipients(recipients) - this.requestState = OutgoingGossipingRequestState.UNSENT - this.requestId = RequestIdHelper.createUniqueRequestId() - this.requestedInfoStr = secretName - }.toOutgoingGossipingRequest() as? OutgoingSecretRequest - } else { - request = existing - } - } - - return request - } - - override fun saveGossipingEvents(events: List) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - events.forEach { event -> - val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now - val entity = GossipingEventEntity( - type = event.type, - sender = event.senderId, - ageLocalTs = ageLocalTs, - content = ContentMapper.map(event.content) - ).apply { - sendState = SendState.SYNCED - decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) - decryptionErrorCode = event.mCryptoError?.name - } - realm.insertOrUpdate(entity) - } - } - } - -// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { -// val statesIndex = states.map { it.ordinal }.toTypedArray() -// return doRealmQueryAndCopy(realmConfiguration) { realm -> -// realm.where() -// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) -// .findAll() -// .filter {entity -> -// states.any { it == entity.requestState} -// } -// }.mapNotNull { -// ContentMapper.map(it.content)?.toModel() -// } -// ?.toOutgoingRoomKeyRequest() -// } -// -// override fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? { -// val statesIndex = states.map { it.ordinal }.toTypedArray() -// return doRealmQueryAndCopy(realmConfiguration) { -// it.where() -// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) -// .findFirst() -// } -// ?.toOutgoingSecretRequest() -// } - -// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { -// doRealmTransaction(realmConfiguration) { -// val obj = OutgoingRoomKeyRequestEntity().apply { -// requestId = request.requestId -// cancellationTxnId = request.cancellationTxnId -// state = request.state.ordinal -// putRecipients(request.recipients) -// putRequestBody(request.requestBody) -// } -// -// it.insertOrUpdate(obj) -// } -// } - -// override fun deleteOutgoingRoomKeyRequest(transactionId: String) { -// doRealmTransaction(realmConfiguration) { -// it.where() -// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) -// .findFirst() -// ?.deleteFromRealm() -// } -// } - -// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { -// if (incomingRoomKeyRequest == null) { -// return -// } -// -// doRealmTransaction(realmConfiguration) { -// // Delete any previous store request with the same parameters -// it.where() -// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) -// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) -// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) -// .findAll() -// .deleteAllFromRealm() -// -// // Then store it -// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { -// userId = incomingRoomKeyRequest.userId -// deviceId = incomingRoomKeyRequest.deviceId -// requestId = incomingRoomKeyRequest.requestId -// putRequestBody(incomingRoomKeyRequest.requestBody) -// } -// } -// } - -// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { -// doRealmTransaction(realmConfiguration) { -// it.where() -// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) -// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) -// .findAll() -// .filter { -// ContentMapper.map(it.content).toModel()?.let { -// -// } -// } -// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) -// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) -// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) -// // .findAll() -// // .deleteAllFromRealm() -// } -// } - - override fun updateGossipingRequestState(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) { - doRealmTransaction(realmConfiguration) { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, requestUserId) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, requestDeviceId) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, requestId) - .findAll().forEach { - it.requestState = state - } - } - } - - override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { - doRealmTransaction(realmConfiguration) { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) - .findAll().forEach { - it.requestState = state - } - } - } - - override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { - return doWithRealm(realmConfiguration) { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) - .findAll() - .mapNotNull { entity -> - entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest - } - .firstOrNull() - } - } - - override fun getPendingIncomingRoomKeyRequests(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) - .findAll() - .map { entity -> - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } - } - } - - override fun getPendingIncomingGossipingRequests(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) - .findAll() - .mapNotNull { entity -> - when (entity.type) { - GossipRequestType.KEY -> { - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } - GossipRequestType.SECRET -> { - IncomingSecretShareRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - secretName = entity.getRequestedSecretName(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestState = newState + if (newState == OutgoingRoomKeyRequestState.UNSENT) { + // clear the old replies + this.replies.deleteAllFromRealm() } } } } - override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { - doRealmTransactionAsync(realmConfiguration) { realm -> - - // After a clear cache, we might have a - - realm.createObject(IncomingGossipingRequestEntity::class.java).let { - it.otherDeviceId = request.deviceId - it.otherUserId = request.userId - it.requestId = request.requestId ?: "" - it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = ageLocalTS ?: clock.epochMillis() - if (request is IncomingSecretShareRequest) { - it.type = GossipRequestType.SECRET - it.requestedInfoStr = request.secretName - } else if (request is IncomingRoomKeyRequest) { - it.type = GossipRequestType.KEY - it.requestedInfoStr = request.requestBody?.toJson() - } - } - } - } - - override fun storeIncomingGossipingRequests(requests: List) { - doRealmTransactionAsync(realmConfiguration) { realm -> - requests.forEach { request -> - // After a clear cache, we might have a - realm.createObject(IncomingGossipingRequestEntity::class.java).let { - it.otherDeviceId = request.deviceId - it.otherUserId = request.userId - it.requestId = request.requestId ?: "" - it.requestState = GossipingRequestState.PENDING - it.localCreationTimestamp = request.localCreationTimestamp ?: clock.epochMillis() - if (request is IncomingSecretShareRequest) { - it.type = GossipRequestType.SECRET - it.requestedInfoStr = request.secretName - } else if (request is IncomingRoomKeyRequest) { - it.type = GossipRequestType.KEY - it.requestedInfoStr = request.requestBody?.toJson() + override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestedIndex = newIndex } + } + } + + override fun updateOutgoingRoomKeyReply(roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + .findAll().firstOrNull { entity -> + entity.toOutgoingKeyRequest().let { + it.requestBody?.senderKey == senderKey && + it.requestBody?.algorithm == algorithm + } + }?.apply { + event.senderId?.let { addReply(it, fromDevice, event) } + } + } + } + + override fun deleteOutgoingRoomKeyRequest(requestId: String) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.deleteOnCascade() + } + } + + override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) + .findAll() + // I delete like this because I want to cascade delete replies? + .onEach { it.deleteOnCascade() } + } + } + + override fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.IncomingKeyRequest.name + val info = IncomingKeyRequestInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = fromUser, + deviceId = fromDevice, + requestId = requestId + ) + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let { + this.contentJson = it } } } } -// override fun getPendingIncomingSecretShareRequests(): List { -// return doRealmQueryAndCopyList(realmConfiguration) { -// it.where() -// .findAll() -// }.map { -// it.toIncomingSecretShareRequest() -// } -// } + override fun saveWithheldAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.OutgoingKeyWithheld.name + val info = WithheldInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + code = code, + userId = userId, + deviceId = deviceId + ) + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } + + override fun saveForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false) + } + + override fun saveIncomingForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true) + } + + private fun saveForwardKeyTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?, + incoming: Boolean + ) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name + val info = ForwardInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } /* ========================================================================================== * Cross Signing @@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingSecretKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(inStates: Set): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .where(OutgoingKeyRequestEntity::class.java) + .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) } val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) + it.toOutgoingKeyRequest() } val trail = monarchy.findAllPagedWithChanges( realmDataSourceFactory, @@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor( val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 doRealmTransaction(realmConfiguration) { realm -> - // Only keep one week history - realm.where() - .lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs) + // Clean the old ones? + realm.where() + .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") } + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } .deleteAllFromRealm() - // Clean the cancelled ones? - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") } - .deleteAllFromRealm() + // Only keep one month history - // Only keep one week history - realm.where() - .lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs) + val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L + realm.where() + .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") } + .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } .deleteAllFromRealm() // Can we do something for WithHeldSessionEntity? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 32f24c5d2e..d621450ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - val schemaVersion = 15L + val schemaVersion = 16L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -70,5 +71,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 13) MigrateCryptoTo013(realm).perform() if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform() + if (oldVersion < 16) MigrateCryptoTo016(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt index b4c94a7fd1..b43030e343 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -17,19 +17,19 @@ package org.matrix.android.sdk.internal.crypto.store.db import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -51,9 +51,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti KeyInfoEntity::class, CrossSigningInfoEntity::class, TrustLevelEntity::class, - GossipingEventEntity::class, - IncomingGossipingRequestEntity::class, - OutgoingGossipingRequestEntity::class, + AuditTrailEntity::class, + OutgoingKeyRequestEntity::class, + KeyRequestReplyEntity::class, MyDeviceLastSeenInfoEntity::class, WithHeldSessionEntity::class, SharedSessionEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt index e1d7598767..8ec2932a8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto.store.db.migration import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { @@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) realm.schema.remove("IncomingRoomKeyRequestEntity") // Not need to migrate existing request, just start fresh? - realm.schema.create("GossipingEventEntity") - .addField(GossipingEventEntityFields.TYPE, String::class.java) - .addIndex(GossipingEventEntityFields.TYPE) - .addField(GossipingEventEntityFields.CONTENT, String::class.java) - .addField(GossipingEventEntityFields.SENDER, String::class.java) - .addIndex(GossipingEventEntityFields.SENDER) - .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) - .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) - .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) - .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) - .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + .addField("type", String::class.java) + .addIndex("type") + .addField("content", String::class.java) + .addField("sender", String::class.java) + .addIndex("sender") + .addField("decryptionResultJson", String::class.java) + .addField("decryptionErrorCode", String::class.java) + .addField("ageLocalTs", Long::class.java) + .setNullable("ageLocalTs", true) + .addField("sendStateStr", String::class.java) realm.schema.create("IncomingGossipingRequestEntity") - .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) - .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) - .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) - .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) - .addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) - .setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("otherUserId", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("otherDeviceId", String::class.java) + .addField("requestStateStr", String::class.java) + .addField("localCreationTimestamp", Long::class.java) + .setNullable("localCreationTimestamp", true) realm.schema.create("OutgoingGossipingRequestEntity") - .addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) - .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) - .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) - .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) - .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("recipientsData", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("requestStateStr", String::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt index 718b9787d2..0e221e78f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt @@ -42,7 +42,7 @@ internal class MigrateCryptoTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) val jsonSignatures = crossSigningKeysMapper.serializeSignatures(objectSignatures) it.setString(KeyInfoEntityFields.SIGNATURES, jsonSignatures) } - } catch (failure: Throwable) { + } catch (ignore: Throwable) { } // Migrate frozen classes diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt new file mode 100644 index 0000000000..5a14ebf85a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.remove("OutgoingGossipingRequestEntity") + realm.schema.remove("IncomingGossipingRequestEntity") + realm.schema.remove("GossipingEventEntity") + + // No need to migrate existing request, just start fresh + + val replySchema = realm.schema.create("KeyRequestReplyEntity") + .addField(KeyRequestReplyEntityFields.SENDER_ID, String::class.java) + .addField(KeyRequestReplyEntityFields.FROM_DEVICE, String::class.java) + .addField(KeyRequestReplyEntityFields.EVENT_JSON, String::class.java) + + realm.schema.create("OutgoingKeyRequestEntity") + .addField(OutgoingKeyRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_ID) + .addField(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID) + .addRealmListField(OutgoingKeyRequestEntityFields.REPLIES.`$`, replySchema) + .addField(OutgoingKeyRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingKeyRequestEntityFields.ROOM_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.ROOM_ID) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INDEX, Integer::class.java) + .addField(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, Long::class.java) + .setNullable(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, true) + + realm.schema.create("AuditTrailEntity") + .addField(AuditTrailEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(AuditTrailEntityFields.AGE_LOCAL_TS, true) + .addField(AuditTrailEntityFields.CONTENT_JSON, String::class.java) + .addField(AuditTrailEntityFields.TYPE, String::class.java) + .addIndex(AuditTrailEntityFields.TYPE) + + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, Boolean::class.java) + ?.transform { + // set the default value to true + it.setBoolean(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, true) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt similarity index 60% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt index 585bcdbbde..2e0e9c8c8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,15 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.crypto.algorithms +package org.matrix.android.sdk.internal.crypto.store.db.model -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import io.realm.RealmObject +import io.realm.annotations.Index -internal interface IMXWithHeldExtension { - fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) +internal open class AuditTrailEntity( + var ageLocalTs: Long? = null, + @Index var type: String? = null, + var contentJson: String? = null +) : RealmObject() { + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt new file mode 100644 index 0000000000..80ae4a8d0d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.UnknownInfo +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal object AuditTrailMapper { + + fun map(entity: AuditTrailEntity): AuditTrail? { + val contentJson = entity.contentJson ?: return null + return when (entity.type) { + TrailType.OutgoingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyForward, + info = info + ) + } + TrailType.OutgoingKeyWithheld.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyWithheld, + info = info + ) + } + TrailType.IncomingKeyRequest.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyRequest, + info = info + ) + } + TrailType.IncomingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyForward, + info = info + ) + } + else -> { + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.Unknown, + info = UnknownInfo + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 35ae86db8b..63ed0e537e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity( var deviceSyncToken: String? = null, // Settings for blacklisting unverified devices. var globalBlacklistUnverifiedDevices: Boolean = false, + // setting to enable or disable key gossiping + var globalEnableKeyGossiping: Boolean = true, // The keys backup version currently used. Null means no backup. var backupVersion: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt deleted file mode 100644 index a024e092b7..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonDataException -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -import timber.log.Timber - -/** - * Keep track of gossiping event received in toDevice messages - * (room key request, or sss secret sharing, as well as cancellations) - * - */ -internal open class GossipingEventEntity(@Index var type: String? = "", - var content: String? = null, - @Index var sender: String? = null, - var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null, - var ageLocalTs: Long? = null) : RealmObject() { - - private var sendStateStr: String = SendState.UNKNOWN.name - - var sendState: SendState - get() { - return SendState.valueOf(sendStateStr) - } - set(value) { - sendStateStr = value.name - } - - companion object - - fun setDecryptionResult(result: MXEventDecryptionResult) { - val decryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) - decryptionResultJson = adapter.toJson(decryptionResult) - decryptionErrorCode = null - } - - fun toModel(): Event { - return Event( - type = this.type ?: "", - content = ContentMapper.map(this.content), - senderId = this.sender - ).also { - it.ageLocalTs = this.ageLocalTs - it.sendState = this.sendState - this.decryptionResultJson?.let { json -> - try { - it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) - } catch (t: JsonDataException) { - Timber.e(t, "Failed to parse decryption result") - } - } - // TODO get the full crypto error object - it.mCryptoError = this.decryptionErrorCode?.let { errorCode -> - MXCryptoError.ErrorType.valueOf(errorCode) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt deleted file mode 100644 index f05c8853c8..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -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.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon - -internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "", - @Index var typeStr: String? = null, - var otherUserId: String? = null, - var requestedInfoStr: String? = null, - var otherDeviceId: String? = null, - var localCreationTimestamp: Long? = null -) : RealmObject() { - - fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { - requestedInfoStr - } else null - - fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { - RoomKeyRequestBody.fromJson(requestedInfoStr) - } else null - - var type: GossipRequestType - get() { - return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = GossipingRequestState.NONE.name - - var requestState: GossipingRequestState - get() { - return tryOrNull { GossipingRequestState.valueOf(requestStateStr) } - ?: GossipingRequestState.NONE - } - set(value) { - requestStateStr = value.name - } - - companion object - - fun toIncomingGossipingRequest(): IncomingShareRequestCommon { - return when (type) { - GossipRequestType.KEY -> { - IncomingRoomKeyRequest( - requestBody = getRequestedKeyInfo(), - deviceId = otherDeviceId, - userId = otherUserId, - requestId = requestId, - state = requestState, - localCreationTimestamp = localCreationTimestamp - ) - } - GossipRequestType.SECRET -> { - IncomingSecretShareRequest( - secretName = getRequestedSecretName(), - deviceId = otherDeviceId, - userId = otherUserId, - requestId = requestId, - localCreationTimestamp = localCreationTimestamp - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt new file mode 100644 index 0000000000..0c7cf79e78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class KeyRequestReplyEntity( + var senderId: String? = null, + var fromDevice: String? = null, + var eventJson: String? = null +) : RealmObject() { + companion object + + fun getEvent(): Event? { + return eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt deleted file mode 100644 index 0e1278967e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -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.GossipRequestType -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest -import org.matrix.android.sdk.internal.di.MoshiProvider - -internal open class OutgoingGossipingRequestEntity( - @Index var requestId: String? = null, - var recipientsData: String? = null, - var requestedInfoStr: String? = null, - @Index var typeStr: String? = null -) : RealmObject() { - - fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) { - requestedInfoStr - } else null - - fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) { - RoomKeyRequestBody.fromJson(requestedInfoStr) - } else null - - var type: GossipRequestType - get() { - return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name - - var requestState: OutgoingGossipingRequestState - get() { - return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) } - ?: OutgoingGossipingRequestState.UNSENT - } - set(value) { - requestStateStr = value.name - } - - companion object { - - private val recipientsDataMapper: JsonAdapter>> = - MoshiProvider - .providesMoshi() - .adapter>>( - Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) - ) - } - - fun toOutgoingGossipingRequest(): OutgoingGossipingRequest { - return when (type) { - GossipRequestType.KEY -> { - OutgoingRoomKeyRequest( - requestBody = getRequestedKeyInfo(), - recipients = getRecipients().orEmpty(), - requestId = requestId ?: "", - state = requestState - ) - } - GossipRequestType.SECRET -> { - OutgoingSecretRequest( - secretName = getRequestedSecretName(), - recipients = getRecipients().orEmpty(), - requestId = requestId ?: "", - state = requestState - ) - } - } - } - - private fun getRecipients(): Map>? { - return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } - } - - fun setRecipients(recipients: Map>) { - this.recipientsData = recipientsDataMapper.toJson(recipients) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt new file mode 100644 index 0000000000..7a8ba18809 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.Index +import org.matrix.android.sdk.api.extensions.tryOrNull +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.RequestReply +import org.matrix.android.sdk.api.session.crypto.RequestResult +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.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class OutgoingKeyRequestEntity( + @Index var requestId: String? = null, + var requestedIndex: Int? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + var creationTimeStamp: Long? = null, + // de-normalization for better query (if not have to query all and parse json) + @Index var roomId: String? = null, + @Index var megolmSessionId: String? = null, + + var replies: RealmList = RealmList() +) : RealmObject() { + + @Index private var requestStateStr: String = OutgoingRoomKeyRequestState.UNSENT.name + + companion object { + + private val recipientsDataMapper: JsonAdapter>> = + MoshiProvider + .providesMoshi() + .adapter( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr) + + fun setRequestBody(body: RoomKeyRequestBody) { + requestedInfoStr = body.toJson() + roomId = body.roomId + megolmSessionId = body.sessionId + } + + var requestState: OutgoingRoomKeyRequestState + get() { + return tryOrNull { OutgoingRoomKeyRequestState.valueOf(requestStateStr) } + ?: OutgoingRoomKeyRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + private fun getRecipients(): Map>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } + + fun addReply(userId: String, fromDevice: String?, event: Event) { + val newReply = KeyRequestReplyEntity( + senderId = userId, + fromDevice = fromDevice, + eventJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(event) + ) + replies.add(newReply) + } + + fun toOutgoingKeyRequest(): OutgoingKeyRequest { + return OutgoingKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + fromIndex = requestedIndex ?: 0, + state = requestState, + results = replies.mapNotNull { entity -> + val userId = entity.senderId ?: return@mapNotNull null + val result = entity.eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + }?.let { event -> + eventToResult(event) + } ?: return@mapNotNull null + RequestReply( + userId = userId, + fromDevice = entity.fromDevice, + result = result + ) + } + ) + } + + private fun eventToResult(event: Event): RequestResult? { + return when (event.getClearType()) { + EventType.ROOM_KEY_WITHHELD -> { + event.content.toModel()?.code?.let { + RequestResult.Failure(it) + } + } + EventType.FORWARDED_ROOM_KEY -> { + RequestResult.Success((event.content?.get("chain_index") as? Number)?.toInt() ?: 0) + } + else -> null + } + } +} + +internal fun OutgoingKeyRequestEntity.deleteOnCascade() { + replies.deleteAllFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 1e395796a9..394c618968 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import dagger.Lazy import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult @@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task { internal class DefaultEncryptEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, - private val cryptoService: CryptoService + private val cryptoService: Lazy ) : EncryptEventTask { override suspend fun execute(params: EncryptEventTask.Params): Event { // don't want to wait for any query @@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor( // try { // let it throws awaitCallback { - cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) }.let { result -> val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> @@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() ) } else { null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 14674ef258..50efe51bc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor( params.event.roomId ?.takeIf { params.encrypt } ?.let { roomId -> - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + tryOrNull { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } } val event = handleEncryption(params) @@ -66,6 +69,7 @@ internal class DefaultSendEventTask @Inject constructor( } } catch (e: Throwable) { // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED) + Timber.w(e, "Unable to send the Event") throw e } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 44aa6d9db1..7c52c62d92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) val response = executeRequest(globalErrorReceiver) { roomAPI.send( - localId, + txId = localId, roomId = event.roomId ?: "", content = event.content, eventType = event.type ?: "" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt index e03e353cb1..18d8b26558 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSignaturesTask.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -34,20 +33,15 @@ internal class DefaultUploadSignaturesTask @Inject constructor( ) : UploadSignaturesTask { override suspend fun execute(params: UploadSignaturesTask.Params) { - try { - val response = executeRequest( - globalErrorReceiver, - canRetry = true, - maxRetriesCount = 10 - ) { - cryptoApi.uploadSignatures(params.signatures) - } - if (response.failures?.isNotEmpty() == true) { - throw Throwable(response.failures.toString()) - } - return - } catch (f: Failure) { - throw f + val response = executeRequest( + globalErrorReceiver, + canRetry = true, + maxRetriesCount = 10 + ) { + cryptoApi.uploadSignatures(params.signatures) + } + if (response.failures?.isNotEmpty() == true) { + throw Throwable(response.failures.toString()) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 8ecb68560b..a9d5cf1191 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( override val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -47,8 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserID, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 4bdf414295..ddb8ed232b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId: String?, cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -44,8 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index d62ca5503d..cd503d6ed8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel @@ -95,8 +95,8 @@ internal class DefaultVerificationService @Inject constructor( @UserId private val userId: String, @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -551,8 +551,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, @@ -771,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor( return } + val roomId = event.roomId + if (roomId == null) { + Timber.e("## SAS Verification missing roomId for event") + // TODO cancel? + return + } + handleReadyReceived(event.senderId, readyReq) { - verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + verificationTransportRoomMessageFactory.createTransport(roomId, it) } } @@ -814,21 +821,15 @@ internal class DefaultVerificationService @Inject constructor( getExistingTransaction(userId, doneReq.transactionId) ?: getOldTransaction(userId, doneReq.transactionId) ?.let { vt -> - val otherDeviceId = vt.otherDeviceId + val otherDeviceId = vt.otherDeviceId ?: return@let if (!crossSigningService.canCrossSign()) { - outgoingGossipingRequestManager.sendSecretShareRequest( - MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) - outgoingGossipingRequestManager.sendSecretShareRequest( - SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) - outgoingGossipingRequestManager.sendSecretShareRequest( - USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) + cryptoCoroutineScope.launch { + secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) + } } - outgoingGossipingRequestManager.sendSecretShareRequest( - KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) } } } @@ -922,8 +923,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = senderId, otherDeviceId = readyReq.fromDevice, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, @@ -1124,8 +1125,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, @@ -1188,6 +1189,7 @@ internal class DefaultVerificationService @Inject constructor( } .distinct() + requestsForUser.add(verificationRequest) transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> // We need to update with the syncedID updatePendingRequest( @@ -1199,7 +1201,6 @@ internal class DefaultVerificationService @Inject constructor( ) } - requestsForUser.add(verificationRequest) dispatchRequestAdded(verificationRequest) return verificationRequest @@ -1323,8 +1324,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, @@ -1465,8 +1466,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = otherUserId, otherDeviceId = otherDeviceId, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt index 692ddd0148..1837b22445 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ 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.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import timber.log.Timber @@ -31,8 +31,8 @@ import timber.log.Timber internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction( } if (otherUserId == userId) { - incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!) // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 00069ba122..4e82a6f406 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.extensions.toUnsignedInt @@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction( open val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -52,8 +52,8 @@ internal abstract class SASDefaultVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt deleted file mode 100644 index 0a175ae3ca..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.verification - -import android.content.Context -import androidx.work.Data -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker -import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -/** - * Possible previous worker: None - * Possible next worker : None - */ -internal class SendVerificationMessageWorker(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 eventId: String, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask - @Inject lateinit var localEchoRepository: LocalEchoRepository - @Inject lateinit var cancelSendTracker: CancelSendTracker - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found") - val localEventId = localEvent.eventId ?: "" - val roomId = localEvent.roomId ?: "" - - if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) { - return Result.success() - .also { - cancelSendTracker.markCancelled(localEventId, roomId) - Timber.e("## SendEvent: Event sending has been cancelled $localEventId") - } - } - - return try { - val resultEventId = sendVerificationMessageTask.execute( - SendVerificationMessageTask.Params( - event = localEvent - ) - ) - - Result.success(Data.Builder().putString(localEventId, resultEventId).build()) - } catch (throwable: Throwable) { - if (throwable.shouldBeRetried()) { - Result.retry() - } else { - 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/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index ec4e1aa65c..303b6c5839 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -15,13 +15,9 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import io.realm.Realm -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent @@ -29,22 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( - private val eventDecryptor: EventDecryptor, - private val clock: Clock, private val verificationService: DefaultVerificationService, @UserId private val userId: String, - @DeviceId private val deviceId: String? -) : EventInsertLiveProcessor { + @DeviceId private val deviceId: String?, + private val clock: Clock, +) { private val transactionsHandledByOtherDevice = ArrayList() @@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor( EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { - if (insertType != EventInsertType.INCREMENTAL_SYNC) { - return false - } - return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) + fun shouldProcess(eventType: String): Boolean { + return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + suspend fun process(event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}") // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. - if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms") } - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = eventDecryptor.decryptEvent(event, "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) - } - } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") // Relates to is not encrypted @@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor( if (event.senderId == userId) { // If it's send from me, we need to keep track of Requests or Start // done from another device of mine - if (EventType.MESSAGE == event.getClearType()) { val msgType = event.getClearContent().toModel()?.msgType if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { @@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor( transactionsHandledByOtherDevice.remove(it) verificationService.onRoomRequestHandledByOtherDevice(event) } + } else if (EventType.ENCRYPTED == event.getClearType()) { + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) } Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index 325a6f0ba2..e32828af23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -15,14 +15,8 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import androidx.lifecycle.Observer -import androidx.work.BackoffPolicy -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.Operation -import androidx.work.WorkInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest @@ -45,27 +39,28 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit +import java.util.concurrent.Executors internal class VerificationTransportRoomMessage( - private val workManagerProvider: WorkManagerProvider, - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, private val userId: String, private val userDeviceId: String?, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val tx: DefaultVerificationTransaction?, - private val coroutineScope: CoroutineScope, + cryptoCoroutineScope: CoroutineScope, private val clock: Clock, ) : VerificationTransport { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: VerificationTxState, @@ -79,70 +74,22 @@ internal class VerificationTransportRoomMessage( content = verificationInfo.toEventContent()!! ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - val enqueueInfo = enqueueSendWork(workerParams) - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - -// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { -// override fun onSuccess(result: Operation.State.SUCCESS?) { -// if (onDone != null) { -// onDone() -// } else { -// tx?.state = nextState -// } -// } -// -// override fun onFailure(t: Throwable) { -// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") -// tx?.cancel(onErrorReason) -// } -// }, listenerExecutor) - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.firstOrNull { it.id == enqueueInfo.second } - ?.let { wInfo -> - when (wInfo.state) { - WorkInfo.State.FAILED -> { - tx?.cancel(onErrorReason) - workLiveData.removeObserver(this) - } - WorkInfo.State.SUCCEEDED -> { - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") - tx?.cancel(onErrorReason) - } else { - if (onDone != null) { - onDone() - } else { - tx?.state = nextState - } - } - workLiveData.removeObserver(this) - } - else -> { - // nop - } - } - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } catch (failure: Throwable) { + tx?.cancel(onErrorReason) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun sendVerificationRequest(supportedMethods: List, @@ -173,60 +120,24 @@ internal class VerificationTransportRoomMessage( val content = info.toContent() val event = createEventAndLocalEcho( - localId, - EventType.MESSAGE, - roomId, - content + localId = localId, + type = EventType.MESSAGE, + roomId = roomId, + content = content ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } - ?.firstOrNull { it.id == workRequest.id } - ?.let { wInfo -> - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - callback(null, null) - } else { - val eventId = wInfo.outputData.getString(localId) - if (eventId != null) { - callback(eventId, validInfo) - } else { - callback(null, null) - } - } - workLiveData.removeObserver(this) - } + verificationSenderScope.launch { + val params = SendVerificationMessageTask.Params(event) + sequencer.post { + try { + val eventId = sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + callback(eventId, validInfo) + } catch (failure: Throwable) { + callback(null, null) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { @@ -236,13 +147,17 @@ internal class VerificationTransportRoomMessage( roomId = roomId, content = MessageVerificationCancelContent.create(transactionId, code).toContent() ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - enqueueSendWork(workerParams) + + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to cancel verification transaction") + } + } + } } override fun done(transactionId: String, @@ -258,47 +173,21 @@ internal class VerificationTransportRoomMessage( ) ).toContent() ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - val enqueueInfo = enqueueSendWork(workerParams) - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } - ?.firstOrNull { it.id == enqueueInfo.second } - ?.let { _ -> - onDone?.invoke() - workLiveData.removeObserver(this) - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to complete (done) verification") + // should we call onDone? + } finally { + onDone?.invoke() + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } - private fun enqueueSendWork(workerParams: Data): Pair { - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - return workManagerProvider.workManager - .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() to workRequest.id - } - - private fun uniqueQueueName() = "${roomId}_VerificationWork" - override fun createAccept(tid: String, keyAgreementProtocol: String, hash: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt index b1b7ad7a98..345948e608 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt @@ -16,39 +16,35 @@ package org.matrix.android.sdk.internal.crypto.verification +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportRoomMessageFactory @Inject constructor( - private val workManagerProvider: WorkManagerProvider, - @SessionId - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, @UserId private val userId: String, @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, - private val taskExecutor: TaskExecutor, + private val cryptoCoroutineScope: CoroutineScope, private val clock: Clock, ) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { return VerificationTransportRoomMessage( - workManagerProvider, - sessionId, - userId, - deviceId, - roomId, - localEchoEventFactory, - tx, - taskExecutor.executorScope, - clock + sendVerificationMessageTask = sendVerificationMessageTask, + userId = userId, + userDeviceId = deviceId, + roomId = roomId, + localEchoEventFactory = localEchoEventFactory, + tx = tx, + cryptoCoroutineScope = cryptoCoroutineScope, + clock = clock, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 1793cd123b..b1ad4d87b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction( override val otherUserId: String, override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -48,8 +48,8 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 6efa347d3a..8b54978279 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -33,7 +33,7 @@ internal interface CheckNumberType { companion object { val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { @Nullable - override fun create(type: Type, annotations: Set?, moshi: Moshi): JsonAdapter<*>? { + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type !== Any::class.java) { return null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt index d8bdc5fc2b..a09e817be5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/CertUtil.kt @@ -250,7 +250,7 @@ internal object CertUtil { builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions) val list = ArrayList() list.add(builder.build()) - // TODO: we should display a warning if user enter an http url + // TODO we should display a warning if user enter an http url if (hsConfig.allowHttpExtension || hsConfig.homeServerUriBase.toString().startsWith("http://")) { list.add(ConnectionSpec.CLEARTEXT) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 78f1c84f3d..2264c3270a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -88,7 +88,7 @@ internal class DefaultFileService @Inject constructor( Timber.v("## FileService downloadFile $url") - // TODO: Remove use of `synchronized` in suspend function. + // TODO Remove use of `synchronized` in suspend function. val existingDownload = synchronized(ongoing) { val existing = ongoing[url] if (existing != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index fb9b2da6c1..050480e6c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -21,12 +21,8 @@ import dagger.Component import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker import org.matrix.android.sdk.internal.crypto.CryptoModule -import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker -import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.federation.FederationModule import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker @@ -133,14 +129,6 @@ internal interface SessionComponent { fun inject(worker: AddPusherWorker) - fun inject(worker: SendVerificationMessageWorker) - - fun inject(worker: SendGossipRequestWorker) - - fun inject(worker: CancelGossipRequestWorker) - - fun inject(worker: SendGossipWorker) - fun inject(worker: UpdateTrustWorker) @Component.Factory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 0aae9f3105..7ceb89e892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask -import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory @@ -318,10 +317,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor - @Binds - @IntoSet - abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor - @Binds @IntoSet abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt index d53ddb7371..1313fcaa62 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/GetTurnServerTask.kt @@ -22,16 +22,16 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal abstract class GetTurnServerTask : Task { - object Params -} +internal abstract class GetTurnServerTask : Task -internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: VoipApi, - private val globalErrorReceiver: GlobalErrorReceiver) : GetTurnServerTask() { +internal class DefaultGetTurnServerTask @Inject constructor( + private val voipApi: VoipApi, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetTurnServerTask() { - override suspend fun execute(params: Params): TurnServerResponse { + override suspend fun execute(params: Unit): TurnServerResponse { return executeRequest(globalErrorReceiver) { - voipAPI.getTurnServer() + voipApi.getTurnServer() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt index 8e2ac5e17e..126a581fa4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt @@ -39,7 +39,7 @@ internal class TurnServerDataSource @Inject constructor(private val turnServerTa } suspend fun getTurnServer(): TurnServerResponse { - return cachedTurnServerResponse.data ?: turnServerTask.execute(GetTurnServerTask.Params).also { + return cachedTurnServerResponse.data ?: turnServerTask.execute(Unit).also { cachedTurnServerResponse.data = it } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt index 8f9b29ed0a..87d07167ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt @@ -29,6 +29,6 @@ internal data class GroupSummaryRoomsSection( @Json(name = "rooms") val rooms: List = emptyList() - // @TODO: Check the meaning and the usage of these categories. This dictionary is empty FTM. + // TODO Check the meaning and the usage of these categories. This dictionary is empty FTM. // public Map categories; ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt index 799aa8a5b1..63608c582a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt @@ -30,6 +30,6 @@ internal data class GroupSummaryUsersSection( @Json(name = "users") val users: List = emptyList() - // @TODO: Check the meaning and the usage of these roles. This dictionary is empty FTM. + // TODO Check the meaning and the usage of these roles. This dictionary is empty FTM. // public Map roles; ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationBody.kt index 6998257263..465d296e94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationBody.kt @@ -16,14 +16,16 @@ package org.matrix.android.sdk.internal.session.identity.model +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SignInvitationBody( - /**The Matrix user ID of the user accepting the invitation.*/ + /** The Matrix user ID of the user accepting the invitation.*/ val mxid: String, - /**The token from the call to store- invite..*/ + /** The token from the call to store- invite..*/ val token: String, /** The private key, encoded as Unpadded base64. */ - val private_key: String + @Json(name = "private_key") + val privateKey: String ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index 6306f3c6fe..4c631c2182 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure @@ -53,6 +54,7 @@ internal class DefaultJoinRoomTask @Inject constructor( private val readMarkersTask: SetReadMarkersTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val coroutineDispatcher: MatrixCoroutineDispatchers, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index e5c7a75cb6..60231892e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -136,7 +136,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private if (joinRules != null) { val body = if (joinRules == RoomJoinRules.RESTRICTED) { RoomJoinRulesContent( - _joinRules = RoomJoinRules.RESTRICTED.value, + joinRulesStr = RoomJoinRules.RESTRICTED.value, allowList = allowList ).toContent() } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt index 1f2ec09367..683dd30b80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt @@ -23,8 +23,11 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.util.JsonDict +/** + * Serializable object + */ @JsonClass(generateAdapter = true) -internal data class SerializablePowerLevelsContent( +internal data class SafePowerLevelContent( @Json(name = "ban") val ban: Int?, @Json(name = "kick") val kick: Int?, @Json(name = "invite") val invite: Int?, @@ -41,7 +44,7 @@ internal data class SerializablePowerLevelsContent( internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict { return toModel() ?.let { content -> - SerializablePowerLevelsContent( + SafePowerLevelContent( ban = content.ban, kick = content.kick, invite = content.invite, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 18fbe73c09..fad21c0918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer @@ -59,6 +60,7 @@ internal class DefaultTimeline( private val settings: TimelineSettings, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val clock: Clock, + stateEventDataSource: StateEventDataSource, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, @@ -106,7 +108,9 @@ internal class DefaultTimeline( onEventsUpdated = this::sendSignalToPostSnapshot, onEventsDeleted = this::onEventsDeleted, onLimitedTimeline = this::onLimitedTimeline, - onNewTimelineEvents = this::onNewTimelineEvents + onNewTimelineEvents = this::onNewTimelineEvents, + stateEventDataSource = stateEventDataSource, + matrixCoroutineDispatchers = coroutineDispatchers, ) private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) @@ -291,7 +295,6 @@ internal class DefaultTimeline( } } - @Suppress("EXPERIMENTAL_API_USAGE") private fun listenToPostSnapshotSignals() { postSnapshotSignalFlow .sample(150) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 6d63b24cf5..53c0253876 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.time.Clock @@ -53,6 +54,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val timelineEventDataSource: TimelineEventDataSource, private val clock: Clock, + private val stateEventDataSource: StateEventDataSource, ) : TimelineService { @AssistedFactory @@ -78,7 +80,8 @@ internal class DefaultTimelineService @AssistedInject constructor( getEventTask = contextOfEventTask, threadsAwarenessHandler = threadsAwarenessHandler, lightweightSettingsStorage = lightweightSettingsStorage, - clock = clock + clock = clock, + stateEventDataSource = stateEventDataSource, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt new file mode 100644 index 0000000000..b2692bf805 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource + +/** + * Helper to observe and query the live room state. + */ +internal class LiveRoomStateListener( + roomId: String, + stateEventDataSource: StateEventDataSource, + private val mainDispatcher: CoroutineDispatcher, +) { + private val roomStateObserver = Observer> { stateEvents -> + stateEvents.map { event -> + val memberContent = event.getFixedRoomMemberContent() ?: return@map + val stateKey = event.stateKey ?: return@map + liveRoomState[stateKey] = memberContent + } + } + private val stateEventsLiveData: LiveData> by lazy { + stateEventDataSource.getStateEventsLive( + roomId = roomId, + eventTypes = setOf(EventType.STATE_ROOM_MEMBER), + stateKey = QueryStringValue.NoCondition, + ) + } + + private val liveRoomState = mutableMapOf() + + suspend fun start() = withContext(mainDispatcher) { + stateEventsLiveData.observeForever(roomStateObserver) + } + + suspend fun stop() = withContext(mainDispatcher) { + if (stateEventsLiveData.hasActiveObservers()) { + stateEventsLiveData.removeObserver(roomStateObserver) + } + } + + fun getLiveState(stateKey: String): RoomMemberContent? = liveRoomState[stateKey] +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index bcf202962c..1e5c993dfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -23,6 +23,7 @@ import io.realm.RealmConfiguration import io.realm.RealmResults import io.realm.kotlin.createObject import kotlinx.coroutines.CompletableDeferred +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError @@ -41,6 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -100,7 +102,9 @@ internal class LoadTimelineStrategy constructor( val onEventsUpdated: (Boolean) -> Unit, val onEventsDeleted: () -> Unit, val onLimitedTimeline: () -> Unit, - val onNewTimelineEvents: (List) -> Unit + val onNewTimelineEvents: (List) -> Unit, + val stateEventDataSource: StateEventDataSource, + val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, ) private var getContextLatch: CompletableDeferred? = null @@ -165,7 +169,13 @@ internal class LoadTimelineStrategy constructor( onEventsUpdated = dependencies.onEventsUpdated ) - fun onStart() { + private val liveRoomStateListener = LiveRoomStateListener( + roomId, + dependencies.stateEventDataSource, + dependencies.matrixCoroutineDispatchers.main + ) + + suspend fun onStart() { dependencies.eventDecryptor.start() dependencies.timelineInput.listeners.add(timelineInputListener) val realm = dependencies.realm.get() @@ -174,9 +184,13 @@ internal class LoadTimelineStrategy constructor( it.addChangeListener(chunkEntityListener) timelineChunk = it.createTimelineChunk() } + + if (dependencies.timelineSettings.useLiveSenderInfo) { + liveRoomStateListener.start() + } } - fun onStop() { + suspend fun onStop() { dependencies.eventDecryptor.destroy() dependencies.timelineInput.listeners.remove(timelineInputListener) chunkEntity?.removeChangeListener(chunkEntityListener) @@ -188,6 +202,9 @@ internal class LoadTimelineStrategy constructor( if (mode is Mode.Thread) { clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId) } + if (dependencies.timelineSettings.useLiveSenderInfo) { + liveRoomStateListener.stop() + } } suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { @@ -222,7 +239,22 @@ internal class LoadTimelineStrategy constructor( } fun buildSnapshot(): List { - return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty() + val events = buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty() + return if (dependencies.timelineSettings.useLiveSenderInfo) { + events.map(this::applyLiveRoomState) + } else { + events + } + } + + private fun applyLiveRoomState(event: TimelineEvent): TimelineEvent { + val updatedState = liveRoomStateListener.getLiveState(event.senderInfo.userId) + return if (updatedState != null) { + val updatedSenderInfo = event.senderInfo.copy(avatarUrl = updatedState.avatarUrl, displayName = updatedState.displayName) + event.copy(senderInfo = updatedSenderInfo) + } else { + event + } } private fun buildSendingEvents(): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 8541c478ba..2c6218443c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -136,6 +136,7 @@ internal class TimelineChunk( val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty() deepBuiltItems.addAll(prevEvents) } + return deepBuiltItems } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt index 6064381808..e78c170a32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt @@ -55,7 +55,7 @@ internal data class SearchRequestRoomEvents( * Requests the server return the current state for each room returned. */ @Json(name = "include_state") - val include_state: Boolean? = null + val includeState: Boolean? = null /** * Requests that the server partitions the result set based on the provided list of keys. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt index 42cd972e0c..ce41a4568c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.presence.model.PresenceEnum internal enum class SyncPresence(val value: String) { Offline("offline"), Online("online"), + Busy("busy"), Unavailable("unavailable"); companion object { @@ -36,6 +37,7 @@ internal enum class SyncPresence(val value: String) { return when (presenceEnum) { PresenceEnum.ONLINE -> Online PresenceEnum.OFFLINE -> Offline + PresenceEnum.BUSY -> Busy PresenceEnum.UNAVAILABLE -> Unavailable } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 5437a015fd..c5d14afac0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -213,7 +213,7 @@ internal class RoomSyncHandler @Inject constructor( val isInitialSync = insertType == EventInsertType.INITIAL_SYNC val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed) - ?._roomSyncEphemeral + ?.roomSyncEphemeral ?.events ?.takeIf { it.isNotEmpty() } ?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC, aggregator) } @@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor( val roomMemberContentsByUser = HashMap() val optimizedThreadSummaryMap = hashMapOf() - for (event in eventList) { + for (rawEvent in eventList) { + // It's annoying roomId is not there, but lot of code rely on it. + // And had to do it now as copy would delete all decryption results.. + val event = rawEvent.copy(roomId = roomId) if (event.eventId == null || event.senderId == null || event.type == null) { continue } @@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor( } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event) + cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo event.unsignedData?.transactionId?.also { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index 59c4dd671e..df9dcfb903 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -68,7 +68,7 @@ internal class DefaultSessionAccountDataService @Inject constructor( val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content) awaitCallback { callback -> updateUserAccountDataTask.configureWith(params) { - this.retryCount = 5 // TODO: Need to refactor retrying out into a helper method. + this.retryCount = 5 // TODO Need to refactor retrying out into a helper method. this.callback = callback } .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt index 9f42688f7d..82cfb570e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt @@ -151,7 +151,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh override fun sendError(message: String, eventData: JsonDict) { Timber.e("## sendError() : eventData $eventData failed $message") - // TODO: JS has an additional optional parameter: nestedError + // TODO JS has an additional optional parameter: nestedError val params = HashMap>() val subMap = HashMap() subMap["message"] = message diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MathUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BestChunkSize.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MathUtils.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BestChunkSize.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index 82ff9a321f..a6b398f6f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -43,7 +43,7 @@ internal interface GetWellknownTask : Task AddPusherWorker(appContext, workerParameters, sessionManager) - CancelGossipRequestWorker::class.java.name -> - CancelGossipRequestWorker(appContext, workerParameters, sessionManager) GetGroupDataWorker::class.java.name -> GetGroupDataWorker(appContext, workerParameters, sessionManager) MultipleEventSendingDispatcherWorker::class.java.name -> @@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage RedactEventWorker(appContext, workerParameters, sessionManager) SendEventWorker::class.java.name -> SendEventWorker(appContext, workerParameters, sessionManager) - SendGossipRequestWorker::class.java.name -> - SendGossipRequestWorker(appContext, workerParameters, sessionManager) - SendGossipWorker::class.java.name -> - SendGossipWorker(appContext, workerParameters, sessionManager) - SendVerificationMessageWorker::class.java.name -> - SendVerificationMessageWorker(appContext, workerParameters, sessionManager) SyncWorker::class.java.name -> SyncWorker(appContext, workerParameters, sessionManager) UpdateTrustWorker::class.java.name -> diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt index 088e160950..e1438f4741 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt @@ -62,12 +62,12 @@ class VersionsKtTest { Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false - Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true - Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true - Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe false + Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe false Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false - Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true + Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt index 31fd86fe65..32b1d44fb9 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt @@ -81,7 +81,7 @@ class DefaultAddPusherTaskTest { } @Test - fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows error`() { + fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() { val realmResult = PusherEntity() monarchy.givenWhereReturns(result = realmResult) pushersAPI.givenSetPusherErrors(SocketException()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt index 149b964fd2..0cfd9d97a8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.task +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay @@ -31,8 +33,8 @@ class CoroutineSequencersTest : MatrixTest { private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_run_sequential() { val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() @@ -60,8 +62,8 @@ class CoroutineSequencersTest : MatrixTest { assertEquals(results[2], "#3") } + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_run_parallel() { val sequencer1 = SemaphoreCoroutineSequencer() val sequencer2 = SemaphoreCoroutineSequencer() @@ -87,8 +89,8 @@ class CoroutineSequencersTest : MatrixTest { assertEquals(3, results.size) } + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test - @Suppress("EXPERIMENTAL_API_USAGE") fun sequencer_should_jump_to_next_when_current_job_canceled() { val sequencer = SemaphoreCoroutineSequencer() val results = ArrayList() diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 393e942b2a..7362ff2d10 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -60,7 +60,7 @@ private short final short ### Line length is limited to 160 chars. Please split long lines -[^─]{161} +#[^─]{161} ### "DO NOT COMMIT" has been committed DO NOT COMMIT diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml new file mode 100644 index 0000000000..fc9f42e4f5 --- /dev/null +++ b/tools/detekt/detekt.yml @@ -0,0 +1,98 @@ +# Default rules: https://github.com/detekt/detekt/blob/main/detekt-core/src/main/resources/default-detekt-config.yml + +style: + MaxLineLength: + # Default is 120 + maxLineLength: 160 + MagicNumber: + active: false + ReturnCount: + active: false + UnnecessaryAbstractClass: + active: false + FunctionOnlyReturningConstant: + active: false + UnusedPrivateMember: + # TODO Enable it + active: false + ThrowsCount: + active: false + LoopWithTooManyJumpStatements: + active: false + SerialVersionUIDInSerializableClass: + active: false + ProtectedMemberInFinalClass: + active: false + +empty-blocks: + EmptyFunctionBlock: + active: false + EmptySecondaryConstructor: + active: false + +potential-bugs: + ImplicitDefaultLocale: + active: false + +exceptions: + TooGenericExceptionCaught: + active: false + SwallowedException: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + TooGenericExceptionThrown: + active: false + +complexity: + TooManyFunctions: + active: false + LongMethod: + active: false + LongParameterList: + active: false + ComplexMethod: + active: false + NestedBlockDepth: + active: false + ComplexCondition: + active: false + LargeClass: + active: false + +naming: + VariableNaming: + # TODO Enable it + active: false + TopLevelPropertyNaming: + # TODO Enable it + active: false + +performance: + SpreadOperator: + active: false + +# Note: all rules for `comments` are disabled by default, but I put them here to be aware of their existence +comments: + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: true + EndOfSentenceFormat: + # TODO Enable it + active: false + OutdatedDocumentation: + # TODO Enable it + active: false + UndocumentedPublicClass: + active: false + UndocumentedPublicFunction: + active: false + UndocumentedPublicProperty: + active: false diff --git a/tools/hs_diag.py b/tools/hs_diag.py index 50f117bc8e..8651321fa3 100755 --- a/tools/hs_diag.py +++ b/tools/hs_diag.py @@ -58,6 +58,9 @@ items = [ # Need token , ["Capability", baseUrl + "_matrix/client/r0/capabilities", True] # Need token , ["Media config", baseUrl + "_matrix/media/r0/config", True] # Need token , ["Turn", baseUrl + "_matrix/client/r0/voip/turnServer", True] + + # Only for Synapse + , ["Synapse version", baseUrl + "_synapse/admin/v1/server_version", True] ] for item in items: diff --git a/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl index a5c097065e..de63de6c06 100644 --- a/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl @@ -8,7 +8,7 @@ import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity -//TODO: add this activity to manifest +//TODO add this activity to manifest class ${activityClass} : VectorBaseActivity(), ToolbarConfigurable { companion object { diff --git a/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl index 38f8132d24..0f01b347c0 100644 --- a/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl @@ -18,7 +18,7 @@ import javax.inject.Inject data class ${fragmentArgsClass}() : Parcelable -//TODO: add this fragment into FragmentModule +//TODO add this fragment into FragmentModule class ${fragmentClass} @Inject constructor( private val viewModelFactory: ${viewModelClass}.Factory ) : VectorBaseFragment(), ${viewModelClass}.Factory by viewModelFactory { diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index 0a4da4c98e..b2cd21c3de 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -38,7 +38,8 @@ false - + true + false diff --git a/vector/build.gradle b/vector/build.gradle index de8e731a0f..c4b1f3fa4e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -297,14 +297,14 @@ android { kotlinOptions { jvmTarget = "11" freeCompilerArgs += [ - "-Xopt-in=kotlin.RequiresOptIn", + "-opt-in=kotlin.RequiresOptIn", // Fixes false positive "This is an internal Mavericks API. It is not intended for external use." // of MvRx `by viewModel()` calls. Maybe due to the inlining of code... This is a temporary fix... - "-Xopt-in=com.airbnb.mvrx.InternalMavericksApi", + "-opt-in=com.airbnb.mvrx.InternalMavericksApi", // Opt in for kotlinx.coroutines.FlowPreview too - "-Xopt-in=kotlinx.coroutines.FlowPreview", + "-opt-in=kotlinx.coroutines.FlowPreview", // Opt in for kotlinx.coroutines.ExperimentalCoroutinesApi too - "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ] } @@ -343,11 +343,11 @@ dependencies { implementation libs.androidx.constraintLayout implementation "androidx.sharetarget:sharetarget:1.1.0" implementation libs.androidx.core - implementation "androidx.media:media:1.4.3" + implementation "androidx.media:media:1.6.0" implementation "androidx.transition:transition:1.4.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" - implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.9.0" + implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi kapt libs.squareup.moshiKotlin @@ -413,10 +413,10 @@ dependencies { implementation 'androidx.browser:browser:1.4.0' // Passphrase strength helper - implementation 'com.nulab-inc:zxcvbn:1.5.2' + implementation 'com.nulab-inc:zxcvbn:1.7.0' - // To convert voice message on old platforms - implementation 'com.arthenica:ffmpeg-kit-audio:4.5.LTS' + // To convert voice message on old platforms. Always keep the LTS suffix! + implementation 'com.arthenica:ffmpeg-kit-audio:4.5.1.LTS' // Alerter implementation 'com.github.tapadoo:alerter:7.2.4' @@ -463,7 +463,7 @@ dependencies { // OSS License, gplay flavor only gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' - implementation "androidx.emoji2:emoji2:1.0.1" + implementation "androidx.emoji2:emoji2:1.1.0" // WebRTC // org.webrtc:google-webrtc is for development purposes only diff --git a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt index 47e1e43be3..6ac3226674 100644 --- a/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt +++ b/vector/src/androidTest/java/im/vector/app/VerificationTestBase.kt @@ -19,6 +19,7 @@ package im.vector.app import android.net.Uri import androidx.lifecycle.Observer import im.vector.app.ui.robot.OnboardingRobot +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -107,7 +108,7 @@ abstract class VerificationTestBase { return result!! } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) private fun syncSession(session: Session) { val lock = CountDownLatch(1) diff --git a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt index a5154c7483..57bdf721a2 100644 --- a/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt +++ b/vector/src/fdroid/java/im/vector/app/fdroid/features/settings/troubleshoot/TestBatteryOptimization.kt @@ -31,7 +31,7 @@ class TestBatteryOptimization @Inject constructor( ) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) { override fun perform(activityResultLauncher: ActivityResultLauncher) { - if (isIgnoringBatteryOptimizations(context)) { + if (context.isIgnoringBatteryOptimizations()) { description = stringProvider.getString(R.string.settings_troubleshoot_test_battery_success) status = TestStatus.SUCCESS quickFix = null diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 4883676f87..983850e3be 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -87,7 +87,7 @@ class ActiveSessionHolder @Inject constructor(private val activeSessionDataSourc ?: throw IllegalStateException("You should authenticate before using this") } - // TODO: Stop sync ? + // TODO Stop sync ? // fun switchToSession(sessionParams: SessionParams) { // val newActiveSession = authenticationService.getSession(sessionParams) // activeSession.set(newActiveSession) diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index 0db7e4e8ea..7e2e786b18 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -50,6 +50,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob @@ -133,7 +134,7 @@ object VectorStaticModule { @Provides fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { - // TODO: handle session injection better + // TODO handle session injection better return activeSessionHolder.getActiveSession() } @@ -173,7 +174,7 @@ object VectorStaticModule { return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) @Provides @NamedGlobalScope fun providesGlobalScope(): CoroutineScope { diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index f8b8b83178..a3ef1cf4e3 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -30,6 +30,11 @@ inline fun T.ooi(block: (T) -> Unit): T = also(block) */ fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() +/** + * Return empty CharSequence if the CharSequence is null + */ +fun CharSequence?.orEmpty() = this ?: "" + /** * Check if a CharSequence is a phone number */ diff --git a/vector/src/main/java/im/vector/app/core/intent/ExternalIntentAnalyser.kt b/vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt similarity index 100% rename from vector/src/main/java/im/vector/app/core/intent/ExternalIntentAnalyser.kt rename to vector/src/main/java/im/vector/app/core/intent/ExternalIntentData.kt diff --git a/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt b/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt index 22fc1758f1..2dd6f058d7 100755 --- a/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt +++ b/vector/src/main/java/im/vector/app/core/preference/VectorPreference.kt @@ -33,6 +33,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import im.vector.app.R import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber /** @@ -152,16 +153,12 @@ open class VectorPreference : Preference { */ private fun addClickListeners(view: View) { view.setOnLongClickListener { - if (null != onPreferenceLongClickListener) { - onPreferenceLongClickListener!!.onPreferenceLongClick(this@VectorPreference) - } else false + onPreferenceLongClickListener?.onPreferenceLongClick(this@VectorPreference).orFalse() } view.setOnClickListener { // call only the click listener - if (onPreferenceClickListener != null) { - onPreferenceClickListener.onPreferenceClick(this@VectorPreference) - } + onPreferenceClickListener?.onPreferenceClick(this@VectorPreference) } } } diff --git a/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt b/vector/src/main/java/im/vector/app/core/resources/Resource.kt similarity index 100% rename from vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt rename to vector/src/main/java/im/vector/app/core/resources/Resource.kt diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index 3aa1964d8d..d39dcbe318 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -52,4 +52,8 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: fun areThreadMessagesEnabled(): Boolean { return vectorPreferences.areThreadMessagesEnabled() } + + fun showLiveSenderInfo(): Boolean { + return vectorPreferences.showLiveSenderInfo() + } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt index 82675e8c11..0024f706b6 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/PresenceStateImageView.kt @@ -42,13 +42,17 @@ class PresenceStateImageView @JvmOverloads constructor( contentDescription = context.getString(R.string.a11y_presence_online) } PresenceEnum.UNAVAILABLE -> { - setImageResource(R.drawable.ic_presence_offline) + setImageResource(R.drawable.ic_presence_away) contentDescription = context.getString(R.string.a11y_presence_unavailable) } PresenceEnum.OFFLINE -> { setImageResource(R.drawable.ic_presence_offline) contentDescription = context.getString(R.string.a11y_presence_offline) } + PresenceEnum.BUSY -> { + setImageResource(R.drawable.ic_presence_busy) + contentDescription = context.getString(R.string.a11y_presence_busy) + } null -> Unit } } diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt index 1fa2b8151a..f8ff12ddb2 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemUtils.kt @@ -43,21 +43,20 @@ import im.vector.app.features.notifications.NotificationUtils * This user option appears on Android M but Android O enforces its usage and kills apps not * authorised by the user to run in background. * - * @param context the context * @return true if battery optimisations are ignored */ -fun isIgnoringBatteryOptimizations(context: Context): Boolean { +fun Context.isIgnoringBatteryOptimizations(): Boolean { // no issue before Android M, battery optimisations did not exist return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || - context.getSystemService()?.isIgnoringBatteryOptimizations(context.packageName) == true + getSystemService()?.isIgnoringBatteryOptimizations(packageName) == true } -fun isAirplaneModeOn(context: Context): Boolean { - return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +fun Context.isAirplaneModeOn(): Boolean { + return Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 } -fun isAnimationDisabled(context: Context): Boolean { - return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f +fun Context.isAnimationEnabled(): Boolean { + return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f } /** diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 9d54475e8c..42693a53f9 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -44,5 +44,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingCombinedRegisterEnabled() = false override fun isLiveLocationEnabled(): Boolean = false - override fun isScreenSharingEnabled(): Boolean = false + override fun isScreenSharingEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt index dea499edde..ae60664b6b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/UserProperties.kt @@ -27,23 +27,23 @@ data class UserProperties( /** * Whether the user has the favourites space enabled */ - val WebMetaSpaceFavouritesEnabled: Boolean? = null, + val webMetaSpaceFavouritesEnabled: Boolean? = null, /** * Whether the user has the home space set to all rooms */ - val WebMetaSpaceHomeAllRooms: Boolean? = null, + val webMetaSpaceHomeAllRooms: Boolean? = null, /** * Whether the user has the home space enabled */ - val WebMetaSpaceHomeEnabled: Boolean? = null, + val webMetaSpaceHomeEnabled: Boolean? = null, /** * Whether the user has the other rooms space enabled */ - val WebMetaSpaceOrphansEnabled: Boolean? = null, + val webMetaSpaceOrphansEnabled: Boolean? = null, /** * Whether the user has the people space enabled */ - val WebMetaSpacePeopleEnabled: Boolean? = null, + val webMetaSpacePeopleEnabled: Boolean? = null, /** * The selected messaging use case during the onboarding flow. */ @@ -82,11 +82,11 @@ data class UserProperties( fun getProperties(): Map? { return mutableMapOf().apply { - WebMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } - WebMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) } - WebMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } - WebMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } - WebMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } + webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) } + webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) } + webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) } + webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) } + webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } numFavouriteRooms?.let { put("numFavouriteRooms", it) } numSpaces?.let { put("numSpaces", it) } diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index b3fc36e5bc..f0158fc4d6 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -89,6 +89,8 @@ class CallControlsView @JvmOverloads constructor( views.videoToggleIcon.setImageResource(R.drawable.ic_video_off) views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera) } + views.videoToggleIcon.isEnabled = !state.isSharingScreen + views.videoToggleIcon.alpha = if (state.isSharingScreen) 0.5f else 1f when (callState) { is CallState.LocalRinging -> { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index ea9adcde85..a904658e9c 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -24,6 +24,7 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.res.Configuration import android.graphics.Color +import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import android.os.Build import android.os.Bundle @@ -32,6 +33,7 @@ import android.util.Rational import android.view.MenuItem import android.view.View import android.view.WindowManager +import androidx.activity.result.ActivityResult import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -76,6 +78,7 @@ import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.webrtc.EglBase import org.webrtc.RendererCommon +import org.webrtc.ScreenCapturerAndroid import timber.log.Timber import javax.inject.Inject @@ -161,6 +164,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } } + + // Bind to service in case of user killed the app while there is an ongoing call + bindToScreenCaptureService() } override fun onNewIntent(intent: Intent?) { @@ -636,18 +642,40 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private val screenSharingPermissionActivityResultLauncher = registerStartForActivityResult { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { - callViewModel.handle(VectorCallViewActions.StartScreenSharing) - // We need to start a foreground service with a sticky notification during screen sharing if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ContextCompat.startForegroundService( - this, - Intent(this, ScreenCaptureService::class.java) - ) - screenCaptureServiceConnection.bind() + // We need to start a foreground service with a sticky notification during screen sharing + startScreenSharingService(activityResult) + } else { + startScreenSharing(activityResult) } } } + private fun startScreenSharing(activityResult: ActivityResult) { + val videoCapturer = ScreenCapturerAndroid(activityResult.data, object : MediaProjection.Callback() { + override fun onStop() { + Timber.i("User revoked the screen capturing permission") + } + }) + callViewModel.handle(VectorCallViewActions.StartScreenSharing(videoCapturer)) + } + + private fun startScreenSharingService(activityResult: ActivityResult) { + ContextCompat.startForegroundService( + this, + Intent(this, ScreenCaptureService::class.java) + ) + bindToScreenCaptureService(activityResult) + } + + private fun bindToScreenCaptureService(activityResult: ActivityResult? = null) { + screenCaptureServiceConnection.bind(object : ScreenCaptureServiceConnection.Callback { + override fun onServiceConnected() { + activityResult?.let { startScreenSharing(it) } + } + }) + } + private fun handleShowScreenSharingPermissionDialog() { getSystemService()?.let { navigator.openScreenSharingPermissionDialog(it.createScreenCaptureIntent(), screenSharingPermissionActivityResultLauncher) diff --git a/vector/src/main/java/im/vector/app/features/call/CallSessionDependencies.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallService.kt similarity index 100% rename from vector/src/main/java/im/vector/app/features/call/CallSessionDependencies.kt rename to vector/src/main/java/im/vector/app/features/call/VectorCallService.kt diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index c84f733b9a..cec118f296 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.transfer.CallTransferResult +import org.webrtc.VideoCapturer sealed class VectorCallViewActions : VectorViewModelAction { object EndCall : VectorCallViewActions() @@ -41,5 +42,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions() object TransferCall : VectorCallViewActions() object ToggleScreenSharing : VectorCallViewActions() - object StartScreenSharing : VectorCallViewActions() + data class StartScreenSharing(val videoCapturer: VideoCapturer) : VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 55a0219bfe..e2cedbe1b0 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -145,9 +145,10 @@ class VectorCallViewModel @AssistedInject constructor( override fun onCallEnded(callId: String) { withState { state -> if (state.otherKnownCallInfo?.callId == callId) { - setState { copy(otherKnownCallInfo = null) } + setState { copy(otherKnownCallInfo = null, isSharingScreen = false) } } } + _viewEvents.post(VectorCallViewEvents.StopScreenSharingService) } override fun onCurrentCallChange(call: WebRtcCall?) { @@ -156,9 +157,10 @@ class VectorCallViewModel @AssistedInject constructor( } } - override fun onAudioDevicesChange() { - val currentSoundDevice = callManager.audioManager.selectedDevice ?: return - if (currentSoundDevice == CallAudioManager.Device.Phone) { + override fun onAudioDevicesChange() = withState { state -> + val currentSoundDevice = callManager.audioManager.selectedDevice ?: return@withState + val webRtcCall = callManager.getCallById(state.callId) + if (webRtcCall != null && shouldActivateProximitySensor(webRtcCall)) { proximityManager.start() } else { proximityManager.stop() @@ -205,7 +207,7 @@ class VectorCallViewModel @AssistedInject constructor( callManager.addListener(callManagerListener) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice - if (currentSoundDevice == CallAudioManager.Device.Phone) { + if (shouldActivateProximitySensor(webRtcCall)) { proximityManager.start() } setState { @@ -224,13 +226,18 @@ class VectorCallViewModel @AssistedInject constructor( formattedDuration = webRtcCall.formattedDuration(), isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(), - transferee = computeTransfereeState(webRtcCall.mxCall) + transferee = computeTransfereeState(webRtcCall.mxCall), + isSharingScreen = webRtcCall.isSharingScreen() ) } updateOtherKnownCall(webRtcCall) } } + private fun shouldActivateProximitySensor(webRtcCall: WebRtcCall): Boolean { + return callManager.audioManager.selectedDevice == CallAudioManager.Device.Phone && !webRtcCall.isSharingScreen() + } + private fun WebRtcCall.extractCallInfo(): VectorCallViewState.CallInfo { val assertedIdentity = this.remoteAssertedIdentity val matrixItem = if (assertedIdentity != null) { @@ -349,7 +356,8 @@ class VectorCallViewModel @AssistedInject constructor( handleToggleScreenSharing(state.isSharingScreen) } is VectorCallViewActions.StartScreenSharing -> { - call?.startSharingScreen() + call?.startSharingScreen(action.videoCapturer) + proximityManager.stop() setState { copy(isSharingScreen = true) } @@ -366,6 +374,9 @@ class VectorCallViewModel @AssistedInject constructor( _viewEvents.post( VectorCallViewEvents.StopScreenSharingService ) + if (callManager.audioManager.selectedDevice == CallAudioManager.Device.Phone) { + proximityManager.start() + } } else { _viewEvents.post( VectorCallViewEvents.ShowScreenSharingPermissionDialog diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt index 922e9676a8..aa7c7f450a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt @@ -27,11 +27,20 @@ class ScreenCaptureServiceConnection @Inject constructor( private val context: Context ) : ServiceConnection { + interface Callback { + fun onServiceConnected() + } + private var isBound = false private var screenCaptureService: ScreenCaptureService? = null + private var callback: Callback? = null - fun bind() { - if (!isBound) { + fun bind(callback: Callback) { + this.callback = callback + + if (isBound) { + callback.onServiceConnected() + } else { Intent(context, ScreenCaptureService::class.java).also { intent -> context.bindService(intent, this, 0) } @@ -45,10 +54,12 @@ class ScreenCaptureServiceConnection @Inject constructor( override fun onServiceConnected(className: ComponentName, binder: IBinder) { screenCaptureService = (binder as ScreenCaptureService.LocalBinder).getService() isBound = true + callback?.onServiceConnected() } override fun onServiceDisconnected(className: ComponentName) { isBound = false screenCaptureService = null + callback = null } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index f0db3e199f..5a100edcf2 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -80,10 +80,12 @@ import org.webrtc.MediaConstraints import org.webrtc.MediaStream import org.webrtc.PeerConnection import org.webrtc.PeerConnectionFactory +import org.webrtc.RtpSender import org.webrtc.RtpTransceiver import org.webrtc.SessionDescription import org.webrtc.SurfaceTextureHelper import org.webrtc.SurfaceViewRenderer +import org.webrtc.VideoCapturer import org.webrtc.VideoSource import org.webrtc.VideoTrack import timber.log.Timber @@ -95,6 +97,7 @@ import kotlin.coroutines.CoroutineContext private const val STREAM_ID = "userMedia" private const val AUDIO_TRACK_ID = "${STREAM_ID}a0" private const val VIDEO_TRACK_ID = "${STREAM_ID}v0" +private const val SCREEN_TRACK_ID = "${STREAM_ID}s0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() private const val INVITE_TIMEOUT_IN_MS = 60_000L @@ -153,13 +156,16 @@ class WebRtcCall( private var makingOffer: Boolean = false private var ignoreOffer: Boolean = false - private var videoCapturer: CameraVideoCapturer? = null + private var videoCapturer: VideoCapturer? = null private val availableCamera = ArrayList() private var cameraInUse: CameraProxy? = null private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null + private var videoSender: RtpSender? = null + private var screenSender: RtpSender? = null + private val timer = CountUpTimer(1000L).apply { tickListener = object : CountUpTimer.TickListener { override fun onTick(milliseconds: Long) { @@ -617,7 +623,7 @@ class WebRtcCall( val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource) Timber.tag(loggerTag.value).v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}") videoTrack.setEnabled(true) - peerConnection?.addTrack(videoTrack, listOf(STREAM_ID)) + videoSender = peerConnection?.addTrack(videoTrack, listOf(STREAM_ID)) localVideoSource = videoSource localVideoTrack = videoTrack } @@ -718,7 +724,7 @@ class WebRtcCall( Timber.tag(loggerTag.value).v("switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return@launch - videoCapturer?.switchCamera( + (videoCapturer as? CameraVideoCapturer)?.switchCamera( object : CameraVideoCapturer.CameraSwitchHandler { // Invoked on success. |isFrontCamera| is true if the new camera is front facing. override fun onCameraSwitchDone(isFrontCamera: Boolean) { @@ -766,12 +772,60 @@ class WebRtcCall( return currentCaptureFormat } - fun startSharingScreen() { - // TODO. Will be handled within the next PR. + fun startSharingScreen(videoCapturer: VideoCapturer) { + val factory = peerConnectionFactoryProvider.get() ?: return + + this.videoCapturer = videoCapturer + + val localMediaStream = factory.createLocalMediaStream(STREAM_ID) + val videoSource = factory.createVideoSource(videoCapturer.isScreencast) + + startCapturingScreen(videoCapturer, videoSource) + + removeLocalSurfaceRenderers() + + showScreenLocally(factory, videoSource, localMediaStream) + + videoSender?.let { removeStream(it) } + + screenSender = peerConnection?.addTrack(localVideoTrack, listOf(STREAM_ID)) } fun stopSharingScreen() { - // TODO. Will be handled within the next PR. + localVideoTrack?.setEnabled(false) + screenSender?.let { removeStream(it) } + if (mxCall.isVideoCall) { + peerConnectionFactoryProvider.get()?.let { configureVideoTrack(it) } + } + updateMuteStatus() + sessionScope?.launch(dispatcher) { attachViewRenderersInternal() } + } + + private fun removeStream(sender: RtpSender) { + peerConnection?.removeTrack(sender) + } + + private fun showScreenLocally(factory: PeerConnectionFactory, videoSource: VideoSource?, localMediaStream: MediaStream?) { + localVideoTrack = factory.createVideoTrack(SCREEN_TRACK_ID, videoSource).apply { setEnabled(true) } + localMediaStream?.addTrack(localVideoTrack) + localSurfaceRenderers.forEach { it.get()?.let { localVideoTrack?.addSink(it) } } + } + + private fun removeLocalSurfaceRenderers() { + localSurfaceRenderers.forEach { it.get()?.let { localVideoTrack?.removeSink(it) } } + } + + private fun startCapturingScreen(videoCapturer: VideoCapturer, videoSource: VideoSource) { + val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext) + videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) + videoCapturer.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) + } + + /** + * Returns true if the user is sharing the screen, false otherwise. + */ + fun isSharingScreen(): Boolean { + return localVideoTrack?.enabled().orFalse() && localVideoTrack?.id() == SCREEN_TRACK_ID } private suspend fun release() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt index a4f6587be4..a32cd7caa7 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -128,7 +128,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { } private fun launch4SActivity() { - SharedSecureStorageActivity.newIntent( + SharedSecureStorageActivity.newReadIntent( context = this, keyId = null, // default key requestedSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME), diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt index 776c7bb521..0d19ae630b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeyBackupSettingsAction.kt @@ -22,4 +22,8 @@ sealed class KeyBackupSettingsAction : VectorViewModelAction { object Init : KeyBackupSettingsAction() object GetKeyBackupTrust : KeyBackupSettingsAction() object DeleteKeyBackup : KeyBackupSettingsAction() + object SetUpKeyBackup : KeyBackupSettingsAction() + data class StoreIn4SSuccess(val recoveryKey: String, val alias: String) : KeyBackupSettingsAction() + object StoreIn4SReset : KeyBackupSettingsAction() + object StoreIn4SFailure : KeyBackupSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt index 13df109dd5..e58746193b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.crypto.keysbackup.settings +import android.app.Activity import android.content.Context import android.content.Intent import com.airbnb.mvrx.Fail @@ -23,9 +24,13 @@ import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.WaitingViewData +import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.app.features.crypto.quads.SharedSecureStorageActivity +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @AndroidEntryPoint class KeysBackupManageActivity : SimpleFragmentActivity() { @@ -41,6 +46,21 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { private val viewModel: KeysBackupSettingsViewModel by viewModel() + private val secretStartForActivityResult = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + val result = activityResult.data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT) + val reset = activityResult.data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false + if (result != null) { + viewModel.handle(KeyBackupSettingsAction.StoreIn4SSuccess(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)) + } else if (reset) { + // all have been reset so a new backup would have been created + viewModel.handle(KeyBackupSettingsAction.StoreIn4SReset) + } + } else { + viewModel.handle(KeyBackupSettingsAction.StoreIn4SFailure) + } + } + override fun initUiAndData() { super.initUiAndData() if (supportFragmentManager.fragments.isEmpty()) { @@ -69,6 +89,22 @@ class KeysBackupManageActivity : SimpleFragmentActivity() { } } } + + viewModel.observeViewEvents { + when (it) { + KeysBackupViewEvents.OpenLegacyCreateBackup -> { + startActivity(KeysBackupSetupActivity.intent(this, false)) + } + is KeysBackupViewEvents.RequestStore4SSecret -> { + secretStartForActivityResult.launch( + SharedSecureStorageActivity.newWriteIntent( + context = this, + writeSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME to it.recoveryKey) + ) + ) + } + } + } } override fun onBackPressed() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt index 4d3ec9a820..edc44fa796 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt @@ -28,7 +28,6 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentKeysBackupSettingsBinding import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity -import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import javax.inject.Inject class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) : @@ -58,9 +57,7 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti } override fun didSelectSetupMessageRecovery() { - context?.let { - startActivity(KeysBackupSetupActivity.intent(it, false)) - } + viewModel.handle(KeyBackupSettingsAction.SetUpKeyBackup) } override fun didSelectRestoreMessageRecovery() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index d281360678..d13b97a9ba 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -29,9 +29,11 @@ import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.genericItem import im.vector.app.features.settings.VectorPreferences import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature import java.util.UUID import javax.inject.Inject @@ -191,69 +193,105 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor( } } is Success -> { - keysVersionTrust().signatures.forEach { - genericItem { - id(UUID.randomUUID().toString()) - title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence()) - - val isDeviceKnown = it.device != null - val isDeviceVerified = it.device?.isVerified ?: false - val isSignatureValid = it.valid - val deviceId: String = it.deviceId ?: "" - - if (!isDeviceKnown) { - description( - host.stringProvider - .getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId) - .toEpoxyCharSequence() - ) - endIconResourceId(R.drawable.e2e_warning) - } else { - if (isSignatureValid) { - if (host.session.sessionParams.deviceId == it.deviceId) { + keysVersionTrust() + .signatures + .filterIsInstance() + .forEach { + val isUserVerified = it.cryptoCrossSigningKey?.trustLevel?.isVerified().orFalse() + val isSignatureValid = it.valid + val userId: String = it.cryptoCrossSigningKey?.userId ?: "" + if (userId == session.sessionParams.userId && isSignatureValid && isUserVerified) { + genericItem { + id(UUID.randomUUID().toString()) + title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence()) description( host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_this_device) + .getString(R.string.keys_backup_settings_signature_from_this_user) .toEpoxyCharSequence() ) endIconResourceId(R.drawable.e2e_verified) - } else { - if (isDeviceVerified) { - description( - host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId) - .toEpoxyCharSequence() - ) - endIconResourceId(R.drawable.e2e_verified) - } else { - description( - host.stringProvider - .getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId) - .toEpoxyCharSequence() - ) - endIconResourceId(R.drawable.e2e_warning) - } - } - } else { - // Invalid signature - endIconResourceId(R.drawable.e2e_warning) - if (isDeviceVerified) { - description( - host.stringProvider - .getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId) - .toEpoxyCharSequence() - ) - } else { - description( - host.stringProvider - .getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId) - .toEpoxyCharSequence() - ) } } } - } - } // end for each + + keysVersionTrust() + .signatures + .filterIsInstance() + .forEach { + genericItem { + id(UUID.randomUUID().toString()) + title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence()) + + val isDeviceKnown = it.device != null + val isDeviceVerified = it.device?.isVerified ?: false + val isSignatureValid = it.valid + val deviceId: String = it.deviceId ?: "" + + if (!isDeviceKnown) { + description( + host.stringProvider + .getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId) + .toEpoxyCharSequence() + ) + endIconResourceId(R.drawable.e2e_warning) + } else { + if (isSignatureValid) { + if (host.session.sessionParams.deviceId == it.deviceId) { + description( + host.stringProvider + .getString(R.string.keys_backup_settings_valid_signature_from_this_device) + .toEpoxyCharSequence() + ) + endIconResourceId(R.drawable.e2e_verified) + } else { + if (isDeviceVerified) { + description( + host.stringProvider + .getString( + R.string.keys_backup_settings_valid_signature_from_verified_device, + deviceId + ) + .toEpoxyCharSequence() + ) + endIconResourceId(R.drawable.e2e_verified) + } else { + description( + host.stringProvider + .getString( + R.string.keys_backup_settings_valid_signature_from_unverified_device, + deviceId + ) + .toEpoxyCharSequence() + ) + endIconResourceId(R.drawable.e2e_warning) + } + } + } else { + // Invalid signature + endIconResourceId(R.drawable.e2e_warning) + if (isDeviceVerified) { + description( + host.stringProvider + .getString( + R.string.keys_backup_settings_invalid_signature_from_verified_device, + deviceId + ) + .toEpoxyCharSequence() + ) + } else { + description( + host.stringProvider + .getString( + R.string.keys_backup_settings_invalid_signature_from_unverified_device, + deviceId + ) + .toEpoxyCharSequence() + ) + } + } + } + } + } // end for each } is Fail -> { errorWithRetryItem { diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index ca6edf0941..b47b84af71 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -25,8 +25,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.session.Session @@ -34,10 +34,16 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion +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.util.awaitCallback +import org.matrix.android.sdk.api.util.toBase64NoPadding +import timber.log.Timber class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState, - session: Session -) : VectorViewModel(initialState), + private val session: Session +) : VectorViewModel(initialState), KeysBackupStateListener { @AssistedFactory @@ -49,6 +55,8 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService() + var pendingBackupCreationInfo: MegolmBackupCreationInfo? = null + init { setState { this.copy( @@ -62,9 +70,18 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS override fun handle(action: KeyBackupSettingsAction) { when (action) { - KeyBackupSettingsAction.Init -> init() - KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust() - KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup() + KeyBackupSettingsAction.Init -> init() + KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust() + KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup() + KeyBackupSettingsAction.SetUpKeyBackup -> viewModelScope.launch { + setUpKeyBackup() + } + KeyBackupSettingsAction.StoreIn4SReset, + KeyBackupSettingsAction.StoreIn4SFailure -> { + pendingBackupCreationInfo = null + // nothing to do just stay on fragment + } + is KeyBackupSettingsAction.StoreIn4SSuccess -> viewModelScope.launch { completeBackupCreation() } } } @@ -120,6 +137,41 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS getKeysBackupTrust() } + suspend fun setUpKeyBackup() { + // We need to check if 4S is enabled first. + // If it is we need to use it, generate a random key + // for the backup and store it in the 4S + if (session.sharedSecretStorageService().isRecoverySetup()) { + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + pendingBackupCreationInfo = creationInfo + val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding() + _viewEvents.post(KeysBackupViewEvents.RequestStore4SSecret(recoveryKey!!)) + } else { + // No 4S so we can open legacy flow + _viewEvents.post(KeysBackupViewEvents.OpenLegacyCreateBackup) + } + } + + suspend fun completeBackupCreation() { + val info = pendingBackupCreationInfo ?: return + try { + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(info, it) + } + // Save it for gossiping + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") + session.cryptoService().keysBackupService().saveBackupRecoveryKey(info.recoveryKey, version = version.version) + } catch (failure: Throwable) { + // XXX mm... failed we should remove what we put in 4S, as it was not created? + + // for now just stay on the screen, user can retry, there is no api to delete account data + } finally { + pendingBackupCreationInfo = null + } + } + private fun deleteCurrentBackup() { val keysBackupService = keysBackupService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupViewEvents.kt similarity index 62% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt rename to vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupViewEvents.kt index d9a6f4fcba..b39a516772 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupViewEvents.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,11 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.model +package im.vector.app.features.crypto.keysbackup.settings -enum class GossipingRequestState { - NONE, - PENDING, - REJECTED, - ACCEPTING, - ACCEPTED, - FAILED_TO_ACCEPTED, +import im.vector.app.core.platform.VectorViewEvents - // USER_REJECTED, - UNABLE_TO_PROCESS, - CANCELLED_BY_REQUESTER, - RE_REQUESTED +sealed class KeysBackupViewEvents : VectorViewEvents { + object OpenLegacyCreateBackup : KeysBackupViewEvents() + data class RequestStore4SSecret(val recoveryKey: String) : KeysBackupViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt index 0fbb18e63c..034c667aac 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt @@ -22,16 +22,17 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.PopupAlertManager +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener 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.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.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction @@ -60,6 +61,9 @@ class KeyRequestHandler @Inject constructor( var session: Session? = null + // This functionality is disabled in element for now. As it could be prone to social attacks + var enablePromptingForRequest = false + fun start(session: Session) { this.session = session session.cryptoService().verificationService().addListener(this) @@ -72,10 +76,9 @@ class KeyRequestHandler @Inject constructor( session = null } - override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean { + override fun onSecretShareRequest(request: SecretShareRequest): Boolean { // By default Element will not prompt if the SDK has decided that the request should not be fulfilled Timber.v("## onSecretShareRequest() : Ignoring $request") - request.ignore?.run() return true } @@ -85,6 +88,8 @@ class KeyRequestHandler @Inject constructor( * @param request the key request. */ override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + if (!enablePromptingForRequest) return + val userId = request.userId val deviceId = request.deviceId val requestId = request.requestId @@ -195,15 +200,14 @@ class KeyRequestHandler @Inject constructor( } private fun denyAllRequests(mappingKey: String) { - alertsToRequests[mappingKey]?.forEach { - it.ignore?.run() - } alertsToRequests.remove(mappingKey) } private fun shareAllSessions(mappingKey: String) { alertsToRequests[mappingKey]?.forEach { - it.share?.run() + session?.coroutineScope?.launch { + session?.cryptoService()?.manuallyAcceptRoomKeyRequest(it) + } } alertsToRequests.remove(mappingKey) } @@ -213,7 +217,7 @@ class KeyRequestHandler @Inject constructor( * * @param request the cancellation request. */ - override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + override fun onRequestCancelled(request: IncomingRoomKeyRequest) { // see if we can find the request in the queue val userId = request.userId val deviceId = request.deviceId diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt index 8ca1dec6d7..40ad2859fe 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt @@ -48,8 +48,9 @@ class SharedSecureStorageActivity : @Parcelize data class Args( val keyId: String?, - val requestedSecrets: List, - val resultKeyStoreAlias: String + val requestedSecrets: List = emptyList(), + val resultKeyStoreAlias: String, + val writeSecrets: List> = emptyList(), ) : Parcelable private val viewModel: SharedSecureStorageViewModel by viewModel() @@ -148,18 +149,36 @@ class SharedSecureStorageActivity : const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET" const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity" - fun newIntent(context: Context, - keyId: String? = null, - requestedSecrets: List, - resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent { + fun newReadIntent(context: Context, + keyId: String? = null, + requestedSecrets: List, + resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent { require(requestedSecrets.isNotEmpty()) return Intent(context, SharedSecureStorageActivity::class.java).also { it.putExtra( - Mavericks.KEY_ARG, Args( - keyId, - requestedSecrets, - resultKeyStoreAlias + Mavericks.KEY_ARG, + Args( + keyId = keyId, + requestedSecrets = requestedSecrets, + resultKeyStoreAlias = resultKeyStoreAlias + ) ) + } + } + + fun newWriteIntent(context: Context, + keyId: String? = null, + writeSecrets: List>, + resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent { + require(writeSecrets.isNotEmpty()) + return Intent(context, SharedSecureStorageActivity::class.java).also { + it.putExtra( + Mavericks.KEY_ARG, + Args( + keyId = keyId, + writeSecrets = writeSecrets, + resultKeyStoreAlias = resultKeyStoreAlias + ) ) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 3fafda54a3..e045ac020d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -39,13 +39,20 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.IntegrityResult +import org.matrix.android.sdk.api.session.securestorage.KeyInfo import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.util.toBase64NoPadding import org.matrix.android.sdk.flow.flow import timber.log.Timber import java.io.ByteArrayOutputStream +sealed class RequestType { + data class ReadSecrets(val secretsName: List) : RequestType() + data class WriteSecrets(val secretsNameValue: List>) : RequestType() +} + data class SharedSecureStorageViewState( val ready: Boolean = false, val hasPassphrase: Boolean = true, @@ -55,13 +62,17 @@ data class SharedSecureStorageViewState( val showResetAllAction: Boolean = false, val userId: String = "", val keyId: String?, - val requestedSecrets: List, + val requestType: RequestType, val resultKeyStoreAlias: String ) : MavericksState { constructor(args: SharedSecureStorageActivity.Args) : this( keyId = args.keyId, - requestedSecrets = args.requestedSecrets, + requestType = if (args.writeSecrets.isNotEmpty()) { + RequestType.WriteSecrets(args.writeSecrets) + } else { + RequestType.ReadSecrets(args.requestedSecrets) + }, resultKeyStoreAlias = args.resultKeyStoreAlias ) @@ -87,14 +98,17 @@ class SharedSecureStorageViewModel @AssistedInject constructor( setState { copy(userId = session.myUserId) } - val integrityResult = session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestedSecrets, initialState.keyId) - if (integrityResult !is IntegrityResult.Success) { - _viewEvents.post( - SharedSecureStorageViewEvent.Error( - stringProvider.getString(R.string.enter_secret_storage_invalid), - true - ) - ) + if (initialState.requestType is RequestType.ReadSecrets) { + val integrityResult = + session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestType.secretsName, initialState.keyId) + if (integrityResult !is IntegrityResult.Success) { + _viewEvents.post( + SharedSecureStorageViewEvent.Error( + stringProvider.getString(R.string.enter_secret_storage_invalid), + true + ) + ) + } } val keyResult = initialState.keyId?.let { session.sharedSecretStorageService().getKey(it) } ?: session.sharedSecretStorageService().getDefaultKey() @@ -226,20 +240,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor( _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) } } - withContext(Dispatchers.IO) { - initialState.requestedSecrets.forEach { - if (session.accountDataService().getUserAccountDataEvent(it) != null) { - val res = session.sharedSecretStorageService().getSecret( - name = it, - keyId = keyInfo.id, - secretKey = keySpec - ) - decryptedSecretMap[it] = res - } else { - Timber.w("## Cannot find secret $it in SSSS, skip") - } - } + performRequest(keyInfo, keySpec, decryptedSecretMap) } }.fold({ setState { copy(checkingSSSSAction = Success(Unit)) } @@ -258,6 +260,37 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } } + private suspend fun performRequest(keyInfo: KeyInfo, keySpec: RawBytesKeySpec, decryptedSecretMap: HashMap) { + when (val requestType = initialState.requestType) { + is RequestType.ReadSecrets -> { + requestType.secretsName.forEach { + if (session.accountDataService().getUserAccountDataEvent(it) != null) { + val res = session.sharedSecretStorageService().getSecret( + name = it, + keyId = keyInfo.id, + secretKey = keySpec + ) + decryptedSecretMap[it] = res + } else { + Timber.w("## Cannot find secret $it in SSSS, skip") + } + } + } + is RequestType.WriteSecrets -> { + requestType.secretsNameValue.forEach { + val (name, value) = it + + session.sharedSecretStorageService().storeSecret( + name = name, + secretBase64 = value, + keys = listOf(SharedSecretStorageService.KeyRef(keyInfo.id, keySpec)) + ) + decryptedSecretMap[name] = value + } + } + } + } + private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) { _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading) val decryptedSecretMap = HashMap() @@ -302,17 +335,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor( ) withContext(Dispatchers.IO) { - initialState.requestedSecrets.forEach { - if (session.accountDataService().getUserAccountDataEvent(it) != null) { - val res = session.sharedSecretStorageService().getSecret( - name = it, - keyId = keyInfo.id, - secretKey = keySpec - ) - decryptedSecretMap[it] = res - } else { - Timber.w("## Cannot find secret $it in SSSS, skip") - } + withContext(Dispatchers.IO) { + performRequest(keyInfo, keySpec, decryptedSecretMap) } } }.fold({ diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt index bc756974ad..e620500d70 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -20,7 +20,6 @@ import im.vector.app.R import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor( // save for gossiping keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) - // while we are there let's restore, but do not block - session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - version, - recoveryKey, - null, - null, - null, - NoOpMatrixCallback() - ) + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources return Result.Success } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 9c6c22b6ca..dc79136cad 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -95,14 +95,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment dismiss() is VerificationBottomSheetViewEvents.AccessSecretStore -> { - secretStartForActivityResult.launch( - SharedSecureStorageActivity.newIntent( - requireContext(), - null, // use default key - listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), - SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS - ) - ) + secretStartForActivityResult.launch(SharedSecureStorageActivity.newReadIntent( + requireContext(), + null, // use default key + listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), + SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS + )) } is VerificationBottomSheetViewEvents.ModalError -> { MaterialAlertDialogBuilder(requireContext()) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 44694404bc..b65ce4b30b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest @@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun tentativeRestoreBackup(res: Map?) { + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources + // Just check that the key is valid and store it, the backup will be used megolm session per + // megolm session when an UISI is encountered + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { @@ -434,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( session.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() ?: return@launch - awaitCallback { - session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - version, - computeRecoveryKey(secret.fromBase64()), - null, - null, - null, - it - ) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + val isValid = awaitCallback { + session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it) + } + if (isValid) { + session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version) } - awaitCallback { session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index da53d7a820..85a440a263 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -200,43 +200,13 @@ class HomeActivity : when (sharedAction) { is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { - views.drawerLayout.closeDrawer(GravityCompat.START) - - // Temporary - // When switching from space to group or group to space, we need to reload the fragment - // To be removed when dropping legacy groups - if (sharedAction.clearFragment) { - replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) - } else { - // nop - } - // we might want to delay that to avoid having the drawer animation lagging - // would be probably better to let the drawer do that? in the on closed callback? - } - is HomeActivitySharedAction.OpenSpacePreview -> { - startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) - } - is HomeActivitySharedAction.AddSpace -> { - createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) - } - is HomeActivitySharedAction.ShowSpaceSettings -> { - // open bottom sheet - SpaceSettingsMenuBottomSheet - .newInstance(sharedAction.spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener { - override fun onShareSpaceSelected(spaceId: String) { - ShareSpaceBottomSheet.show(supportFragmentManager, spaceId) - } - }) - .show(supportFragmentManager, "SPACE_SETTINGS") - } - is HomeActivitySharedAction.OpenSpaceInvite -> { - SpaceInviteBottomSheet.newInstance(sharedAction.spaceId) - .show(supportFragmentManager, "SPACE_INVITE") - } - HomeActivitySharedAction.SendSpaceFeedBack -> { - bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) - } + is HomeActivitySharedAction.OpenGroup -> openGroup(sharedAction.shouldClearFragment) + is HomeActivitySharedAction.OpenSpacePreview -> startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) + is HomeActivitySharedAction.AddSpace -> createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this)) + is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId) + is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId) + HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) + HomeActivitySharedAction.CloseGroup -> closeGroup() } } .launchIn(lifecycleScope) @@ -273,6 +243,37 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } + private fun openGroup(shouldClearFragment: Boolean) { + views.drawerLayout.closeDrawer(GravityCompat.START) + + // When switching from space to group or group to space, we need to reload the fragment + if (shouldClearFragment) { + replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) + } else { + // do nothing + } + } + + private fun showSpaceSettings(spaceId: String) { + // open bottom sheet + SpaceSettingsMenuBottomSheet + .newInstance(spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener { + override fun onShareSpaceSelected(spaceId: String) { + ShareSpaceBottomSheet.show(supportFragmentManager, spaceId) + } + }) + .show(supportFragmentManager, "SPACE_SETTINGS") + } + + private fun openSpaceInvite(spaceId: String) { + SpaceInviteBottomSheet.newInstance(spaceId) + .show(supportFragmentManager, "SPACE_INVITE") + } + + private fun closeGroup() { + views.drawerLayout.openDrawer(GravityCompat.START) + } + private fun handleShowAnalyticsOptIn() { navigator.openAnalyticsOptIn(this) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index 6047a1e55e..cebde7fe0a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -24,7 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() - data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction() + data class OpenGroup(val shouldClearFragment: Boolean) : HomeActivitySharedAction() + object CloseGroup : HomeActivitySharedAction() object AddSpace : HomeActivitySharedAction() data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index e4cb47f44c..dae15a652f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -38,6 +38,7 @@ import im.vector.app.R import im.vector.app.RoomGroupingMethod import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -76,7 +77,8 @@ class HomeDetailFragment @Inject constructor( private val appStateHandler: AppStateHandler ) : VectorBaseFragment(), KeysBackupBanner.Delegate, - CurrentCallsView.Callback { + CurrentCallsView.Callback, + OnBackPressed { private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() @@ -186,6 +188,10 @@ class HomeDetailFragment @Inject constructor( updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab) } + views.groupToolbarNavigateUp.setOnClickListener { + navigateUpOneSpace() + } + viewModel.observeViewEvents { viewEvent -> when (viewEvent) { HomeDetailViewEvents.CallStarted -> handleCallStarted() @@ -196,7 +202,6 @@ class HomeDetailFragment @Inject constructor( unknownDeviceDetectorSharedViewModel.onEach { state -> state.unknownSessions.invoke()?.let { unknownDevices -> -// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}") if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { val uid = "review_login" alertManager.cancelAlert(uid) @@ -270,6 +275,14 @@ class HomeDetailFragment @Inject constructor( views.dimViewBottom.isVisible = false } + private fun navigateUpOneSpace() { + val parentId = getCurrentSpace()?.flattenParentIds?.lastOrNull() + appStateHandler.setCurrentSpace(parentId) + sharedActionViewModel.post(HomeActivitySharedAction.CloseGroup) + } + + private fun getCurrentSpace() = (appStateHandler.getCurrentRoomGroupingMethod() as? RoomGroupingMethod.BySpace)?.spaceSummary + private fun handleCallStarted() { dismissLoadingDialog() val fragmentTag = HomeTab.DialPad.toFragmentTag() @@ -283,20 +296,16 @@ class HomeDetailFragment @Inject constructor( override fun onResume() { super.onResume() - // update notification tab if needed updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab()) callManager.checkForProtocolsSupportIfNeeded() + refreshSpaceState() + } - // Current space/group is not live so at least refresh toolbar on resume - appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod -> - when (roomGroupingMethod) { - is RoomGroupingMethod.ByLegacyGroup -> { - onGroupChange(roomGroupingMethod.groupSummary) - } - is RoomGroupingMethod.BySpace -> { - onSpaceChange(roomGroupingMethod.spaceSummary) - } - } + private fun refreshSpaceState() { + when (val roomGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) { + is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary) + is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary) + else -> Unit } } @@ -340,12 +349,12 @@ class HomeDetailFragment @Inject constructor( viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary) contentAction = Runnable { - (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { + (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity -> // mark as ignored to avoid showing it again unknownDeviceDetectorSharedViewModel.handle( UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId }) ) - it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS) + activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS) } } dismissedAction = Runnable { @@ -425,11 +434,11 @@ class HomeDetailFragment @Inject constructor( withState(viewModel) { when (it.roomGroupingMethod) { is RoomGroupingMethod.ByLegacyGroup -> { - // nothing do far + // do nothing } is RoomGroupingMethod.BySpace -> { - it.roomGroupingMethod.spaceSummary?.let { - sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(it.roomId)) + it.roomGroupingMethod.spaceSummary?.let { spaceSummary -> + sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId)) } } } @@ -449,17 +458,6 @@ class HomeDetailFragment @Inject constructor( viewModel.handle(HomeDetailAction.SwitchTab(tab)) true } - -// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView - -// bottomNavigationView.getOrCreateBadge() -// menuView.forEachIndexed { index, view -> -// val itemView = view as BottomNavigationItemView -// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false) -// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView) -// itemView.addView(badgeLayout) -// unreadCounterBadgeViews.add(index, unreadCounterBadgeView) -// } } private fun updateUIForTab(tab: HomeTab) { @@ -538,7 +536,6 @@ class HomeDetailFragment @Inject constructor( } override fun invalidate() = withState(viewModel) { -// Timber.v(it.toString()) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) @@ -598,4 +595,11 @@ class HomeDetailFragment @Inject constructor( } return this } + + override fun onBackPressed(toolbarButton: Boolean) = if (getCurrentSpace() != null) { + navigateUpOneSpace() + true + } else { + false + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt index be5f9c0bb4..65d18105fe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt @@ -53,7 +53,7 @@ class AutoCompleter @AssistedInject constructor( @Assisted val isInThreadTimeline: Boolean, private val avatarRenderer: AvatarRenderer, private val commandAutocompletePolicy: CommandAutocompletePolicy, - AutocompleteCommandPresenterFactory: AutocompleteCommandPresenter.Factory, + autocompleteCommandPresenterFactory: AutocompleteCommandPresenter.Factory, private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter, @@ -68,7 +68,7 @@ class AutoCompleter @AssistedInject constructor( } private val autocompleteCommandPresenter: AutocompleteCommandPresenter by lazy { - AutocompleteCommandPresenterFactory.create(isInThreadTimeline) + autocompleteCommandPresenterFactory.create(isInThreadTimeline) } private var editText: EditText? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 099f3779ee..de1d512c75 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -105,6 +105,7 @@ import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createUIHandler +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.onPermissionDeniedSnackbar @@ -586,6 +587,10 @@ class TimelineFragment @Inject constructor( } private fun handleChatEffect(chatEffect: ChatEffect) { + if (!requireContext().isAnimationEnabled()) { + Timber.d("Do not perform chat effect, animations are disabled.") + return + } when (chatEffect) { ChatEffect.CONFETTI -> { views.viewKonfetti.isVisible = true @@ -666,11 +671,13 @@ class TimelineFragment @Inject constructor( ).apply { directListener = { granted -> if (granted) { - timelineViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed( - widget = it.widget, - userJustAccepted = true, - grantedEvents = it.grantedEvents - )) + timelineViewModel.handle( + RoomDetailAction.EnsureNativeWidgetAllowed( + widget = it.widget, + userJustAccepted = true, + grantedEvents = it.grantedEvents + ) + ) } } } @@ -791,25 +798,29 @@ class TimelineFragment @Inject constructor( override fun onSendVoiceMessage() { messageComposerViewModel.handle( - MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId())) + MessageComposerAction.EndRecordingVoiceMessage(isCancelled = false, rootThreadEventId = getRootThreadEventId()) + ) updateRecordingUiState(RecordingUiState.Idle) } override fun onDeleteVoiceMessage() { messageComposerViewModel.handle( - MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId())) + MessageComposerAction.EndRecordingVoiceMessage(isCancelled = true, rootThreadEventId = getRootThreadEventId()) + ) updateRecordingUiState(RecordingUiState.Idle) } override fun onRecordingLimitReached() { messageComposerViewModel.handle( - MessageComposerAction.PauseRecordingVoiceMessage) + MessageComposerAction.PauseRecordingVoiceMessage + ) updateRecordingUiState(RecordingUiState.Draft) } override fun onRecordingWaveformClicked() { messageComposerViewModel.handle( - MessageComposerAction.PauseRecordingVoiceMessage) + MessageComposerAction.PauseRecordingVoiceMessage + ) updateRecordingUiState(RecordingUiState.Draft) } @@ -827,7 +838,8 @@ class TimelineFragment @Inject constructor( private fun updateRecordingUiState(state: RecordingUiState) { messageComposerViewModel.handle( - MessageComposerAction.OnVoiceRecordingUiStateChanged(state)) + MessageComposerAction.OnVoiceRecordingUiStateChanged(state) + ) } } } @@ -1527,9 +1539,11 @@ class TimelineFragment @Inject constructor( attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) attachmentTypeSelector.setAttachmentVisibility( AttachmentTypeSelectorView.Type.LOCATION, - vectorPreferences.isLocationSharingEnabled()) + vectorPreferences.isLocationSharingEnabled() + ) attachmentTypeSelector.setAttachmentVisibility( - AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine()) + AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() + ) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -2292,12 +2306,18 @@ class TimelineFragment @Inject constructor( handleCancelSend(action) } is EventSharedAction.ReportContentSpam -> { - timelineViewModel.handle(RoomDetailAction.ReportContent( - action.eventId, action.senderId, "This message is spam", spam = true)) + timelineViewModel.handle( + RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is spam", spam = true + ) + ) } is EventSharedAction.ReportContentInappropriate -> { - timelineViewModel.handle(RoomDetailAction.ReportContent( - action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) + timelineViewModel.handle( + RoomDetailAction.ReportContent( + action.eventId, action.senderId, "This message is inappropriate", inappropriate = true + ) + ) } is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) @@ -2443,7 +2463,8 @@ class TimelineFragment @Inject constructor( displayName = timelineViewModel.getRoomSummary()?.displayName, avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - rootThreadEventId = rootThreadEventId) + rootThreadEventId = rootThreadEventId + ) navigator.openThread(it, roomThreadDetailArgs) } } @@ -2479,7 +2500,8 @@ class TimelineFragment @Inject constructor( roomId = timelineArgs.roomId, displayName = timelineViewModel.getRoomSummary()?.displayName, roomEncryptionTrustLevel = timelineViewModel.getRoomSummary()?.roomEncryptionTrustLevel, - avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl) + avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl + ) navigator.openThreadList(it, roomThreadDetailArgs) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index b898aaf114..4350ad6a7d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -225,7 +225,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { when (state) { is AudioMessagePlaybackTracker.Listener.State.Recording -> { - voiceMessageViews.renderRecordingWaveform(state.amplitudeList.toTypedArray()) + voiceMessageViews.renderRecordingWaveform(state.amplitudeList.toList()) } is AudioMessagePlaybackTracker.Listener.State.Playing -> { voiceMessageViews.renderPlaying(state) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt index 0256064af2..0a093221a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageViews.kt @@ -345,10 +345,10 @@ class VoiceMessageViews( } } - fun renderRecordingWaveform(amplitudeList: Array) { + fun renderRecordingWaveform(amplitudeList: List) { views.voicePlaybackWaveform.doOnLayout { waveFormView -> val waveformColor = ThemeUtils.getColor(waveFormView.context, R.attr.vctr_content_quaternary) - amplitudeList.iterator().forEach { + amplitudeList.forEach { (waveFormView as AudioWaveformView).add(AudioWaveformView.FFT(it.toFloat(), waveformColor)) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt index 8b7dcc9c72..18bdc2fdaa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt @@ -26,7 +26,8 @@ class TimelineSettingsFactory @Inject constructor(private val userPreferencesPro return TimelineSettings( initialSize = 30, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts(), - rootThreadEventId = rootThreadEventId + rootThreadEventId = rootThreadEventId, + useLiveSenderInfo = userPreferencesProvider.showLiveSenderInfo() ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index a29016e883..f083c70100 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -95,6 +95,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo || isTileTypeMessage(nextDisplayableEvent) || + nextDisplayableEvent.isRootThread() || event.isRootThread() || nextDisplayableEvent.isEdition() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt index 45b6e5b8cd..76f95d75e8 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginCaptchaFragment.kt @@ -157,12 +157,14 @@ class LoginCaptchaFragment @Inject constructor( } } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { @Suppress("DEPRECATION") super.onReceivedError(view, errorCode, description, failingUrl) onError(description) } + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (url?.startsWith("js:") == true) { var json = url.substring(3) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index b55e393339..d5a98695e1 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -141,6 +141,7 @@ class LoginWebFragment @Inject constructor( .show() } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) @@ -193,6 +194,7 @@ class LoginWebFragment @Inject constructor( * @param url * @return */ + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt index f729584f51..04422069af 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginCaptchaFragment2.kt @@ -152,12 +152,14 @@ class LoginCaptchaFragment2 @Inject constructor( } } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { @Suppress("DEPRECATION") super.onReceivedError(view, errorCode, description, failingUrl) onError(description) } + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (url?.startsWith("js:") == true) { var json = url.substring(3) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt index 789cd459ac..81a87d7e12 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginWebFragment2.kt @@ -142,6 +142,7 @@ class LoginWebFragment2 @Inject constructor( .show() } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) @@ -194,6 +195,7 @@ class LoginWebFragment2 @Inject constructor( * @param url * @return */ + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 80f5f47b3b..b9d9261abe 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -215,7 +215,7 @@ class NotifiableEventResolver @Inject constructor( keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ) - } catch (e: MXCryptoError) { + } catch (ignore: MXCryptoError) { } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt index 9f63ff3e22..bbbffc7656 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt @@ -309,9 +309,9 @@ class Login2Variant( activity.finish() } - private fun updateWithState(LoginViewState2: LoginViewState2) { + private fun updateWithState(loginViewState2: LoginViewState2) { // Loading - setIsLoading(LoginViewState2.isLoading) + setIsLoading(loginViewState2.isLoading) } // Hack for AccountCreatedFragment diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt index 558c26fe56..c4099effa7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/CaptchaWebview.kt @@ -103,12 +103,14 @@ class CaptchaWebview @Inject constructor( } } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { @Suppress("DEPRECATION") super.onReceivedError(view, errorCode, description, failingUrl) onError(description) } + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (url?.startsWith("js:") == true) { val javascriptResponse = parseJsonFromUrl(url) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt index 49db52da67..b8114b5d94 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt @@ -24,6 +24,7 @@ import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.animations.play import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.databinding.FragmentFtueAccountCreatedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents @@ -57,7 +58,7 @@ class FtueAuthAccountCreatedFragment @Inject constructor( views.personalizeButtonGroup.isVisible = canPersonalize views.takeMeHomeButtonGroup.isVisible = !canPersonalize - if (!hasPlayedConfetti && !canPersonalize) { + if (!hasPlayedConfetti && !canPersonalize && requireContext().isAnimationEnabled()) { hasPlayedConfetti = true views.viewKonfetti.isVisible = true views.viewKonfetti.play() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt index 6b47b9830c..074f58864e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt @@ -22,6 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import im.vector.app.core.animations.play +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.databinding.FragmentFtuePersonalizationCompleteBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents @@ -43,7 +44,7 @@ class FtueAuthPersonalizationCompleteFragment @Inject constructor() : AbstractFt private fun setupViews() { views.personalizationCompleteCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } - if (!hasPlayedConfetti) { + if (!hasPlayedConfetti && requireContext().isAnimationEnabled()) { hasPlayedConfetti = true views.viewKonfetti.isVisible = true views.viewKonfetti.play() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index 2fe9a82fbd..63b3bc0f71 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -288,8 +288,8 @@ class FtueAuthVariant( .show() } - private fun onServerSelectionDone(OnboardingViewEvents: OnboardingViewEvents.OnServerSelectionDone) { - when (OnboardingViewEvents.serverType) { + private fun onServerSelectionDone(onboardingViewEvents: OnboardingViewEvents.OnServerSelectionDone) { + when (onboardingViewEvents.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.EMS, ServerType.Other -> activity.addFragmentToBackstack( @@ -301,9 +301,9 @@ class FtueAuthVariant( } } - private fun onSignModeSelected(OnboardingViewEvents: OnboardingViewEvents.OnSignModeSelected) = withState(onboardingViewModel) { state -> + private fun onSignModeSelected(onboardingViewEvents: OnboardingViewEvents.OnSignModeSelected) = withState(onboardingViewModel) { state -> // state.signMode could not be ready yet. So use value from the ViewEvent - when (OnboardingViewEvents.signMode) { + when (onboardingViewEvents.signMode) { SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.SignUp -> Unit // This case is processed in handleOnboardingViewEvents SignMode.SignIn -> handleSignInSelected(state) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt index 26b9a38d83..aa30d11134 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWebFragment.kt @@ -138,6 +138,7 @@ class FtueAuthWebFragment @Inject constructor( .show() } + @Deprecated("Deprecated in Java") override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) @@ -190,6 +191,7 @@ class FtueAuthWebFragment @Inject constructor( * @param url * @return */ + @Deprecated("Deprecated in Java") override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean { if (url == null) return super.shouldOverrideUrlLoading(view, url as String?) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt index f848a3174e..e41ea320ac 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber @@ -59,7 +60,7 @@ class PinLocker @Inject constructor( return liveState } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) private fun computeState() { GlobalScope.launch { val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 91292e42e5..5d16fabbfd 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -25,7 +25,7 @@ import com.tapadoo.alerter.Alerter import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.time.Clock -import im.vector.app.core.utils.isAnimationDisabled +import im.vector.app.core.utils.isAnimationEnabled import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.pin.PinActivity import im.vector.app.features.signout.hard.SignedOutActivity @@ -218,7 +218,7 @@ class PopupAlertManager @Inject constructor( if (!alert.isLight) { clearLightStatusBar() } - val noAnimation = !animate || isAnimationDisabled(activity) + val noAnimation = !(animate && activity.isAnimationEnabled()) alert.weakCurrentActivity = WeakReference(activity) val alerter = Alerter.create(activity, alert.layoutRes) diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt index b23f2f171d..7af7db27cf 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt @@ -29,7 +29,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @AndroidEntryPoint -class QrCodeScannerActivity() : VectorBaseActivity() { +class QrCodeScannerActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt index 2f8e013f46..b687fa6a4d 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt @@ -19,6 +19,7 @@ package im.vector.app.features.rageshake import android.content.Context import android.util.Log import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -88,7 +89,7 @@ class VectorFileLogger @Inject constructor( } } - @Suppress("EXPERIMENTAL_API_USAGE") + @OptIn(DelicateCoroutinesApi::class) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { fileHandler ?: return GlobalScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt index bd2f0b67bd..a587468722 100644 --- a/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorUncaughtExceptionHandler.kt @@ -61,6 +61,7 @@ class VectorUncaughtExceptionHandler @Inject constructor( * @param throwable the throwable * @return the exception description */ + @Suppress("PrintStackTrace") override fun uncaughtException(thread: Thread, throwable: Throwable) { Timber.v("Uncaught exception: $throwable") preferences.edit(commit = true) { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index 7062a5d02d..f47b5b2411 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -44,8 +44,8 @@ import javax.inject.Inject /** * - * TODO: Loading indicator while getting emoji data source? - * TODO: Finish Refactor to vector base activity + * TODO Loading indicator while getting emoji data source? + * TODO Finish Refactor to vector base activity */ @AndroidEntryPoint class EmojiReactionPickerActivity : VectorBaseActivity(), diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 5c0d2f36f5..af399bdb31 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -41,9 +41,9 @@ import kotlin.math.abs /** * - * TODO: Configure Span using available width and emoji size - * TODO: Performances - * TODO: Scroll to section - Find a way to snap section to the top + * TODO Configure Span using available width and emoji size + * TODO Performances + * TODO Scroll to section - Find a way to snap section to the top */ class EmojiRecyclerAdapter @Inject constructor() : RecyclerView.Adapter() { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index caf17e09d6..1994de396f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -297,7 +297,7 @@ class CreateRoomViewModel @AssistedInject constructor( } } - // TODO: Should this be non-cancellable? + // TODO Should this be non-cancellable? viewModelScope.launch { runCatching { session.roomService().createRoom(createRoomParams) }.fold( { roomId -> diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 545e9f7190..adc720b0a5 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -182,6 +182,15 @@ class RoomMemberProfileController @Inject constructor( // More buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + if (!state.isMine) { + buildProfileAction( + id = "direct", + editable = false, + title = stringProvider.getString(R.string.room_member_open_or_create_dm), + action = { callback?.onOpenDmClicked() } + ) + } + buildProfileAction( id = "overrideColor", editable = false, @@ -194,13 +203,6 @@ class RoomMemberProfileController @Inject constructor( if (!state.isMine) { val membership = state.asyncMembership() ?: return - buildProfileAction( - id = "direct", - editable = false, - title = stringProvider.getString(R.string.room_member_open_or_create_dm), - action = { callback?.onOpenDmClicked() } - ) - if (!state.isSpace && state.hasReadReceipt) { buildProfileAction( id = "read_receipt", diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index ca42a07d50..15eac2a4ca 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -211,6 +211,9 @@ class VectorPreferences @Inject constructor( const val SETTINGS_LABS_ENABLE_THREAD_MESSAGES = "SETTINGS_LABS_ENABLE_THREAD_MESSAGES_FINAL" const val SETTINGS_THREAD_MESSAGES_SYNCED = "SETTINGS_THREAD_MESSAGES_SYNCED" + // This key will be used to enable user for displaying live user info or not. + const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" + // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 @@ -1039,9 +1042,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE, true) } - /** - * Indicates whether or not thread messages are enabled - */ fun areThreadMessagesEnabled(): Boolean { return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_THREAD_MESSAGES, getDefault(R.bool.settings_labs_thread_messages_default)) } @@ -1091,4 +1091,8 @@ class VectorPreferences @Inject constructor( .putBoolean(SETTINGS_THREAD_MESSAGES_SYNCED, shouldMigrate) .apply() } + + fun showLiveSenderInfo(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default)) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index 3a999f299f..5844467a1f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import androidx.preference.Preference import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.copyToClipboard @@ -72,7 +73,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( } it.setOnPreferenceClickListener { pref -> - copyToClipboard(requireContext(), pref.summary) + copyToClipboard(requireContext(), pref.summary.orEmpty()) true } } @@ -82,7 +83,7 @@ class VectorSettingsHelpAboutFragment @Inject constructor( it.summary = Matrix.getSdkVersion() it.setOnPreferenceClickListener { pref -> - copyToClipboard(requireContext(), pref.summary) + copyToClipboard(requireContext(), pref.summary.orEmpty()) true } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index bd4f556461..622f1f3f97 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -151,7 +151,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( findPreference("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!! } - override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView { + override fun onCreateRecyclerView(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): RecyclerView { return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also { // Insert animation are really annoying the first time the list is shown // due to the way preference fragment is done, it's not trivial to disable it for first appearance only.. diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index 83740c5018..ec4ef26001 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding -import org.billcarsonfr.jsonviewer.JSonViewerDialog -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import javax.inject.Inject class GossipingEventsPaperTrailFragment @Inject constructor( @@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor( super.onDestroyView() } - override fun didTap(event: Event) { - if (event.isEncrypted()) { - event.toClearContentStringWithIndent() - } else { - event.toContentStringWithIndent() - }?.let { - JSonViewerDialog.newInstance( - it, - -1, - createJSonViewerStyleProvider(colorProvider) - ).show(childFragmentManager, "JSON_VIEWER") - } + override fun didTap(event: AuditTrail) { +// if (event.isEncrypted()) { +// event.toClearContentStringWithIndent() +// } else { +// event.toContentStringWithIndent() +// }?.let { +// JSonViewerDialog.newInstance( +// it, +// -1, +// createJSonViewerStyleProvider(colorProvider) +// ).show(childFragmentManager, "JSON_VIEWER") +// } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index 30c2757eff..e7f09b09e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail data class GossipingEventsPaperTrailState( - val events: Async> = Uninitialized + val events: Async> = Uninitialized ) : MavericksState class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt index 30c2efc3ce..b8d82699e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt @@ -17,62 +17,43 @@ package im.vector.app.features.settings.devtools import im.vector.app.core.resources.DateProvider -import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -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.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.threeten.bp.format.DateTimeFormatter class GossipingEventsSerializer { private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") - fun serialize(eventList: List): String { + fun serialize(eventList: List): String { return buildString { - eventList.forEach { - val clearType = it.getClearType() - append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ") - when (clearType) { - EventType.ROOM_KEY_REQUEST -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("sessionId: ${content.body?.sessionId} ") - } - append("requestedBy: ${content?.requestingDeviceId}") + eventList.forEach { trail -> + val type = trail.type + val info = trail.info + append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ") + append("sessionId: ${info.sessionId} ") + when (type) { + TrailType.IncomingKeyRequest -> { + append("from:${info.userId}|${info.deviceId} - ") } - EventType.FORWARDED_ROOM_KEY -> { - val encryptedContent = it.content.toModel() - val content = it.getClearContent().toModel() - - append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}") - span("\nFrom Device (sender key):") { - textStyle = "bold" + TrailType.OutgoingKeyForward -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") } } - EventType.ROOM_KEY -> { - val content = it.getClearContent() - append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}") - } - EventType.SEND_SECRET -> { - val content = it.getClearContent().toModel() - append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}") - } - EventType.REQUEST_SECRET -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("secretName:${content.secretName} ") + TrailType.OutgoingKeyWithheld -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? WithheldInfo)?.let { + append("code: ${it.code} ") } - append("requestedBy:${content?.requestingDeviceId}") } - EventType.ENCRYPTED -> { - append("Failed to Decrypt") + TrailType.IncomingKeyForward -> { + append("from:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") + } } else -> { append("??") diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt index dd016c2bf5..6a45968916 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController -import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider @@ -26,137 +25,75 @@ import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -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.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import javax.inject.Inject class GossipingTrailPagedEpoxyController @Inject constructor( private val vectorDateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider -) : PagedListEpoxyController( +) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { interface InteractionListener { - fun didTap(event: Event) + fun didTap(event: AuditTrail) } var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> { val host = this val event = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { id(event.hashCode()) itemClickAction { host.interactionListener?.didTap(event) } - title( - if (event.isEncrypted()) { - "${event.getClearType()} [encrypted]" - } else { - event.type - }?.toEpoxyCharSequence() - ) + title(event.type.name.toEpoxyCharSequence()) description( span { +host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - span("\nfrom: ") { + span("\n${host.senderFromTo(event.type)}: ") { textStyle = "bold" } - +"${event.senderId}" + +"${event.info.userId}|${event.info.deviceId}" + span("\nroomId: ") { + textStyle = "bold" + } + +event.info.roomId + span("\nsessionId: ") { + textStyle = "bold" + } + +event.info.sessionId apply { - if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsessionId:") { + when (event.type) { + TrailType.OutgoingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { textStyle = "bold" } - +" ${content.body?.sessionId}" + +"${fInfo.chainIndex}" } - span("\nrequestedBy: ") { - textStyle = "bold" - } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - val encryptedContent = event.content.toModel() - val content = event.getClearContent().toModel() - if (event.mxDecryptionResult == null) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.sessionId}" - span("\nFrom Device (sender key):") { - textStyle = "bold" - } - +" ${encryptedContent?.senderKey}" - } else if (event.getClearType() == EventType.ROOM_KEY) { - // it's a bit of a fake event for trail reasons - val content = event.getClearContent() - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.get("session_id")}" - span("\nroomId:") { - textStyle = "bold" - } - +" ${content?.get("room_id")}" - span("\nTo :") { - textStyle = "bold" - } - +" ${content?.get("_dest") ?: "me"}" - } else if (event.getClearType() == EventType.SEND_SECRET) { - val content = event.getClearContent().toModel() - - span("\nrequestId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\nFrom Device:") { - textStyle = "bold" - } - +" ${event.mxDecryptionResult?.payload?.get("sender_device")}" - } else if (event.getClearType() == EventType.REQUEST_SECRET) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsecretName:") { + TrailType.OutgoingKeyWithheld -> { + val fInfo = event.info as WithheldInfo + span("\ncode: ") { textStyle = "bold" } - +" ${content.secretName}" + +"${fInfo.code}" } - span("\nrequestedBy: ") { - textStyle = "bold" + TrailType.IncomingKeyRequest -> { + // no additional info } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.ENCRYPTED) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) + TrailType.IncomingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { + textStyle = "bold" + } + +"${fInfo.chainIndex}" + } + TrailType.Unknown -> { } } } @@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor( ) } } + + private fun senderFromTo(type: TrailType): String { + return when (type) { + TrailType.OutgoingKeyWithheld, + TrailType.OutgoingKeyForward -> "to" + TrailType.IncomingKeyRequest, + TrailType.IncomingKeyForward -> "from" + TrailType.Unknown -> "" + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt index 4653f04f2c..09dd198bee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } span("${roomKeyRequest.userId}") + +"\n" +host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) span("\nsessionId:") { textStyle = "bold" @@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } +"${roomKeyRequest.deviceId}" - span("\nstate: ") { - textStyle = "bold" - } - +roomKeyRequest.state.name }.toEpoxyCharSequence() ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index a8045c07e3..e0a130a1e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest data class KeyRequestListViewState( val incomingRequests: Async> = Uninitialized, - val outgoingRoomKeyRequests: Async> = Uninitialized + val outgoingRoomKeyRequests: Async> = Uninitialized ) : MavericksState class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt index b23bd77277..13ba4a69ac 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt @@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import javax.inject.Inject -class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( +class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { @@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> { val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index 4e75c7ff82..d7c18b9c53 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -204,7 +204,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( // Important, Battery optim white listing is needed in this mode; // Even if using foreground service with foreground notif, it stops to work // in doze mode for certain devices :/ - if (!isIgnoringBatteryOptimizations(requireContext())) { + if (!requireContext().isIgnoringBatteryOptimizations()) { requestDisablingBatteryOptimization(requireActivity(), batteryStartForActivityResult) } } @@ -352,8 +352,8 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( super.onDetach() } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { - return when (preference?.key) { + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY -> { updateEnabledForAccount(preference) true diff --git a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt index 01e933e446..27116093d2 100755 --- a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt @@ -54,7 +54,7 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute views.syncStateProgressBar.isVisible = newState is SyncState.Running && newState.afterPause if (newState == SyncState.NoNetwork) { - val isAirplaneModeOn = isAirplaneModeOn(context) + val isAirplaneModeOn = context.isAirplaneModeOn() views.syncStateNoNetwork.isVisible = isAirplaneModeOn.not() views.syncStateNoNetworkAirplane.isVisible = isAirplaneModeOn } else { diff --git a/vector/src/main/res/drawable/ic_arrow_back.xml b/vector/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000000..8452791cfd --- /dev/null +++ b/vector/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_presence_away.xml b/vector/src/main/res/drawable/ic_presence_away.xml new file mode 100644 index 0000000000..617fe735b6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_presence_away.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_presence_busy.xml b/vector/src/main/res/drawable/ic_presence_busy.xml new file mode 100644 index 0000000000..9155a6b4a5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_presence_busy.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_settings_root_flair.xml b/vector/src/main/res/drawable/ic_settings_root_flair.xml deleted file mode 100644 index b074ae478e..0000000000 --- a/vector/src/main/res/drawable/ic_settings_root_flair.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml index 5a60632e86..7e856ba896 100644 --- a/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml +++ b/vector/src/main/res/layout/fragment_ftue_server_selection_combined.xml @@ -151,6 +151,13 @@ app:layout_constraintHeight_percent="0.02" app:layout_constraintTop_toBottomOf="@id/chooseServerSubmit" /> + + + + \ No newline at end of file diff --git a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cfe52..6f3b755bf5 100644 --- a/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 56b6fc05d1..a1998d0393 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1054,7 +1054,7 @@ Play shutter sound - Flair + Flair 3 days @@ -1510,6 +1510,7 @@ Your keys are not being backed up from this session. Backup has a signature from unknown session with ID %s. + Backup has a valid signature from this user. Backup has a valid signature from this session. Backup has a valid signature from verified session %s. Backup has a valid signature from unverified session %s @@ -1776,6 +1777,7 @@ Send attachment Open the navigation drawer + Navigate up one space Open the create room menu Close the create room menu… Create a new direct conversation @@ -2178,7 +2180,7 @@ Leave "Leaving the room…" - Override nick color + Override display name color Admins Moderators @@ -2729,7 +2731,8 @@ Public room Online Offline - Unavailable + Busy + Away Dev Tools Explore Room State @@ -2852,6 +2855,8 @@ Your system will automatically send logs when an unable to decrypt error occurs Enable Thread Messages Note: app will be restarted + Show latest user info + Show the latest profile info (avatar and display name) for all the messages. %s invites you diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 5144f6fe1f..8b25a8c287 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -1,5 +1,6 @@ - + @@ -68,4 +69,4 @@ android:key="SETTINGS_LABS_RENDER_LOCATIONS_IN_TIMELINE" android:title="@string/labs_render_locations_in_timeline" /> - \ No newline at end of file + diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 6c8250a76a..7ff588ca76 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -88,6 +88,13 @@ android:title="@string/message_bubbles" app:isPreferenceVisible="@bool/settings_interface_bubble_visible" /> + + - \ No newline at end of file + diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 0c50083d53..02dc36bf39 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -8,12 +8,6 @@ app:fragment="im.vector.app.features.settings.VectorSettingsGeneralFragment" app:isPreferenceVisible="@bool/settings_root_general_visible" /> - - - \ No newline at end of file + diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index c26c73a9a7..59f6d4ea12 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -308,49 +308,55 @@ class OnboardingViewModelTest { } @Test - fun `given personalisation enabled and registration has started and has dummy step to do, when handling action, then ignores other steps and executes dummy`() = runTest { - fakeVectorFeatures.givenPersonalisationEnabled() - givenSuccessfulRegistrationForStartAndDummySteps(missingStages = listOf(Stage.Dummy(mandatory = true))) - val test = viewModel.test() + fun `given personalisation enabled and registration has started and has dummy step to do, when handling action, then ignores other steps and does dummy`() { + runTest { + fakeVectorFeatures.givenPersonalisationEnabled() + givenSuccessfulRegistrationForStartAndDummySteps(missingStages = listOf(Stage.Dummy(mandatory = true))) + val test = viewModel.test() - viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) + viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) - test - .assertStatesChanges( - initialState, - { copy(isLoading = true) }, - { copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) } - ) - .assertEvents(OnboardingViewEvents.OnAccountCreated) - .finish() + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false, personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) } + ) + .assertEvents(OnboardingViewEvents.OnAccountCreated) + .finish() + } } @Test - fun `given changing profile picture is supported, when updating display name, then updates upstream user display name and moves to choose profile picture`() = runTest { - viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))) - val test = viewModel.test() + fun `given changing profile avatar is supported, when updating display name, then updates upstream user display name and moves to choose profile avatar`() { + runTest { + viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))) + val test = viewModel.test() - viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) + viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) - test - .assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates()) - .assertEvents(OnboardingViewEvents.OnChooseProfilePicture) - .finish() - fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) + test + .assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates()) + .assertEvents(OnboardingViewEvents.OnChooseProfilePicture) + .finish() + fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) + } } @Test - fun `given changing profile picture is not supported, when updating display name, then updates upstream user display name and completes personalization`() = runTest { - viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))) - val test = viewModel.test() + fun `given changing profile avatar is not supported, when updating display name, then updates upstream user display name and completes personalization`() { + runTest { + viewModelWith(initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))) + val test = viewModel.test() - viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) + viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) - test - .assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates()) - .assertEvents(OnboardingViewEvents.OnPersonalizationComplete) - .finish() - fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) + test + .assertStatesChanges(initialState, expectedSuccessfulDisplayNameUpdateStates()) + .assertEvents(OnboardingViewEvents.OnPersonalizationComplete) + .finish() + fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) + } } @Test diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt index 4f349f8506..c79a0be542 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt @@ -34,7 +34,11 @@ class FakeSharedSecretStorageService : SharedSecretStorageService { TODO("Not yet implemented") } - override suspend fun generateKeyWithPassphrase(keyId: String, keyName: String, passphrase: String, keySigner: KeySigner, progressListener: ProgressListener?): SsssKeyCreationInfo { + override suspend fun generateKeyWithPassphrase(keyId: String, + keyName: String, + passphrase: String, + keySigner: KeySigner, + progressListener: ProgressListener?): SsssKeyCreationInfo { TODO("Not yet implemented") } @@ -66,7 +70,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService { override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) = integrityResult - override fun requestSecret(name: String, myOtherDeviceId: String) { + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { TODO("Not yet implemented") } }