Merge branch 'feature/eric/improve-back-navigation' into experiment/eric/space-switching-modal
# Conflicts: # vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt # vector/src/main/res/layout/fragment_home_detail.xml
This commit is contained in:
commit
60adecedd6
3
.github/ISSUE_TEMPLATE/release.yml
vendored
3
.github/ISSUE_TEMPLATE/release.yml
vendored
@ -23,7 +23,8 @@ body:
|
|||||||
|
|
||||||
### Do the release
|
### 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 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
|
- [ ] 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()`
|
- [ ] Run the integration test, and especially `UiAllScreensSanityTest.allScreensTest()`
|
||||||
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -10,6 +10,8 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
reviewers:
|
||||||
|
- "vector-im/element-android-reviewers"
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*github-script*"
|
- dependency-name: "*github-script*"
|
||||||
# Updates for Gradle dependencies used in the app
|
# Updates for Gradle dependencies used in the app
|
||||||
@ -19,6 +21,6 @@ updates:
|
|||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 200
|
open-pull-requests-limit: 200
|
||||||
reviewers:
|
reviewers:
|
||||||
- "bmarty"
|
- "vector-im/element-android-reviewers"
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: com.google.zxing:core
|
- dependency-name: com.google.zxing:core
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -67,4 +67,4 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
vector/build/outputs/apk/*/release/*.apk
|
vector/build/outputs/apk/*/release/*.apk
|
||||||
|
|
||||||
# TODO: add exodus checks
|
# TODO add exodus checks
|
||||||
|
22
.github/workflows/docs.yml
vendored
Normal file
22
.github/workflows/docs.yml
vendored
Normal file
@ -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
|
@ -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:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
branches: [ release/* ]
|
types: [closed]
|
||||||
schedule:
|
branches: [develop]
|
||||||
# At 20:00 every day UTC
|
|
||||||
- cron: '0 20 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
# Enrich gradle.properties for CI/CD
|
||||||
env:
|
env:
|
||||||
CI_GRADLE_ARG_PROPERTIES: >
|
CI_GRADLE_ARG_PROPERTIES: >
|
||||||
-Porg.gradle.jvmargs=-Xmx4g
|
-Porg.gradle.jvmargs=-Xmx4g
|
||||||
-Porg.gradle.parallel=false
|
-Porg.gradle.parallel=false
|
||||||
|
|
||||||
jobs:
|
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
|
# Run Android Tests
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Matrix SDK - Running Integration Tests
|
name: Matrix SDK - Running Integration Tests
|
||||||
|
needs: should-i-run
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
api-level: [ 28 ]
|
api-level: [ 28 ]
|
||||||
# No concurrency required, runs every time on a schedule.
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
@ -43,11 +58,12 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Start synapse server
|
- name: Start synapse server
|
||||||
uses: michaelkaye/setup-matrix-synapse@v1.0.1
|
uses: michaelkaye/setup-matrix-synapse@v1.0.3
|
||||||
with:
|
with:
|
||||||
uploadLogs: true
|
uploadLogs: true
|
||||||
httpPort: 8080
|
httpPort: 8080
|
||||||
disableRateLimiting: true
|
disableRateLimiting: true
|
||||||
|
public_baseurl: "http://10.0.2.2:8080/"
|
||||||
# package: org.matrix.android.sdk.session
|
# package: org.matrix.android.sdk.session
|
||||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
@ -209,6 +225,7 @@ jobs:
|
|||||||
|
|
||||||
ui-tests:
|
ui-tests:
|
||||||
name: UI Tests (Synapse)
|
name: UI Tests (Synapse)
|
||||||
|
needs: should-i-run
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -230,11 +247,12 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Start synapse server
|
- name: Start synapse server
|
||||||
uses: michaelkaye/setup-matrix-synapse@v1.0.1
|
uses: michaelkaye/setup-matrix-synapse@v1.0.3
|
||||||
with:
|
with:
|
||||||
uploadLogs: true
|
uploadLogs: true
|
||||||
httpPort: 8080
|
httpPort: 8080
|
||||||
disableRateLimiting: true
|
disableRateLimiting: true
|
||||||
|
public_baseurl: "http://10.0.2.2:8080/"
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
@ -266,7 +284,8 @@ jobs:
|
|||||||
|
|
||||||
codecov-units:
|
codecov-units:
|
||||||
name: Unit tests with code coverage
|
name: Unit tests with code coverage
|
||||||
runs-on: macos-latest
|
needs: should-i-run
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
@ -290,49 +309,21 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
|
||||||
|
|
||||||
sonarqube:
|
# Notify the channel about delayed failures
|
||||||
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:
|
notify:
|
||||||
name: Notify matrix
|
name: Notify matrix
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
|
- should-i-run
|
||||||
- integration-tests
|
- integration-tests
|
||||||
- ui-tests
|
- ui-tests
|
||||||
- sonarqube
|
- codecov-units
|
||||||
if: always() && github.event_name != 'workflow_dispatch'
|
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.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: michaelkaye/matrix-hookshot-action@v1.0.0
|
- uses: michaelkaye/matrix-hookshot-action@v1.0.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
|
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}}"
|
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: "{{#if '${{ github.event_name }}' == 'schedule' }}Nightly test run{{else}}Test run (on ${{ github.ref }}){{/if }}: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/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 }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
20
.github/workflows/quality.yml
vendored
20
.github/workflows/quality.yml
vendored
@ -147,3 +147,23 @@ jobs:
|
|||||||
name: release-lint-report-${{ matrix.target }}
|
name: release-lint-report-${{ matrix.target }}
|
||||||
path: |
|
path: |
|
||||||
vector/build/reports/*.*
|
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
|
||||||
|
81
.github/workflows/sonarqube.yml
vendored
Normal file
81
.github/workflows/sonarqube.yml
vendored
Normal file
@ -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 }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
@ -14,7 +14,7 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
|
|||||||
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
|
||||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
||||||
|
|
||||||
Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nighly test status: [](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
|
Nightly build: [](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) Nightly test status: [](https://github.com/vector-im/element-android/actions/workflows/nightly.yml)
|
||||||
|
|
||||||
|
|
||||||
# New Android SDK
|
# 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).
|
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.
|
We use [issue labels](https://github.com/vector-im/element-meta/wiki/Issue-labelling) to sort all incoming issues.
|
||||||
|
|
||||||
|
37
build.gradle
37
build.gradle
@ -5,10 +5,17 @@ buildscript {
|
|||||||
apply from: 'dependencies_groups.gradle'
|
apply from: 'dependencies_groups.gradle'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
// Do not use `google()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://maven.google.com'
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url "https://plugins.gradle.org/m2/"
|
url "https://plugins.gradle.org/m2/"
|
||||||
}
|
}
|
||||||
|
// Do not use `mavenCentral()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://repo1.maven.org/maven2'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -28,9 +35,11 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ktlint Plugin
|
|
||||||
plugins {
|
plugins {
|
||||||
|
// ktlint Plugin
|
||||||
id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
|
id "org.jlleitschuh.gradle.ktlint" version "10.3.0"
|
||||||
|
// Detekt
|
||||||
|
id "io.gitlab.arturbosch.detekt" version "1.20.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/jeremylong/DependencyCheck
|
// https://github.com/jeremylong/DependencyCheck
|
||||||
@ -45,9 +54,12 @@ dependencyCheck {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||||
|
apply plugin: "io.gitlab.arturbosch.detekt"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral {
|
// Do not use `mavenCentral()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://repo1.maven.org/maven2'
|
||||||
content {
|
content {
|
||||||
groups.mavenCentral.regex.each { includeGroupByRegex it }
|
groups.mavenCentral.regex.each { includeGroupByRegex it }
|
||||||
groups.mavenCentral.group.each { includeGroup it }
|
groups.mavenCentral.group.each { includeGroup it }
|
||||||
@ -70,14 +82,18 @@ allprojects {
|
|||||||
groups.jitsi.group.each { includeGroup it }
|
groups.jitsi.group.each { includeGroup it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
google {
|
// Do not use `google()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://maven.google.com'
|
||||||
content {
|
content {
|
||||||
groups.google.regex.each { includeGroupByRegex it }
|
groups.google.regex.each { includeGroupByRegex it }
|
||||||
groups.google.group.each { includeGroup it }
|
groups.google.group.each { includeGroup it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//noinspection JcenterRepositoryObsolete
|
//noinspection JcenterRepositoryObsolete
|
||||||
jcenter {
|
// Do not use `jcenter`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://jcenter.bintray.com'
|
||||||
content {
|
content {
|
||||||
groups.jcenter.regex.each { includeGroupByRegex it }
|
groups.jcenter.regex.each { includeGroupByRegex it }
|
||||||
groups.jcenter.group.each { includeGroup it }
|
groups.jcenter.group.each { includeGroup it }
|
||||||
@ -106,7 +122,7 @@ allprojects {
|
|||||||
// display the corresponding rule
|
// display the corresponding rule
|
||||||
verbose = true
|
verbose = true
|
||||||
disabledRules = [
|
disabledRules = [
|
||||||
// TODO: Re-enable these 4 rules after reformatting project
|
// TODO Re-enable these 4 rules after reformatting project
|
||||||
"indent",
|
"indent",
|
||||||
"experimental:argument-list-wrapping",
|
"experimental:argument-list-wrapping",
|
||||||
"max-line-length",
|
"max-line-length",
|
||||||
@ -127,6 +143,15 @@ allprojects {
|
|||||||
"experimental:kdoc-wrapping",
|
"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) {
|
task clean(type: Delete) {
|
||||||
|
1
changelog.d/5151.misc
Normal file
1
changelog.d/5151.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Improve threads rendering in the main timeline
|
1
changelog.d/5494.feature
Normal file
1
changelog.d/5494.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Use key backup before requesting keys + refactor & improvement of key request/forward
|
4
changelog.d/5559.sdk
Normal file
4
changelog.d/5559.sdk
Normal file
@ -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()
|
1
changelog.d/5825.bugfix
Normal file
1
changelog.d/5825.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Changed copy and list order in member profile screen.
|
1
changelog.d/5906.bugfix
Normal file
1
changelog.d/5906.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Desynchronized 4S | Megolm backup causing Unusable backup
|
1
changelog.d/5911.feature
Normal file
1
changelog.d/5911.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Screen sharing over WebRTC
|
1
changelog.d/5932.feature
Normal file
1
changelog.d/5932.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Allow using the latest user Avatar and name for all messages in the timeline
|
1
changelog.d/5936.feature
Normal file
1
changelog.d/5936.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Added themed launch icons for Android 13
|
1
changelog.d/5941.bugfix
Normal file
1
changelog.d/5941.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
If animations are disable on the System, chat effects and confetti will be disabled too
|
1
changelog.d/5965.sdk
Normal file
1
changelog.d/5965.sdk
Normal file
@ -0,0 +1 @@
|
|||||||
|
Including SSL/TLS error handing when doing WellKnown lookups without a custom HomeServerConnectionConfig
|
1
changelog.d/5973.doc
Normal file
1
changelog.d/5973.doc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Note public_baseurl requirement in integration tests documentation.
|
1
changelog.d/5997.misc
Normal file
1
changelog.d/5997.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Update check for server-side threads support to match spec.
|
1
changelog.d/6038.misc
Normal file
1
changelog.d/6038.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Setup detekt
|
1
changelog.d/6047.feature
Normal file
1
changelog.d/6047.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add presence indicator busy and away.
|
1
changelog.d/6073.feature
Normal file
1
changelog.d/6073.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Adds up navigation in spaces
|
@ -7,26 +7,26 @@ ext.versions = [
|
|||||||
'targetCompat' : JavaVersion.VERSION_11,
|
'targetCompat' : JavaVersion.VERSION_11,
|
||||||
]
|
]
|
||||||
|
|
||||||
def gradle = "7.0.4"
|
def gradle = "7.2.0"
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
def kotlin = "1.6.0"
|
def kotlin = "1.6.21"
|
||||||
def kotlinCoroutines = "1.6.0"
|
def kotlinCoroutines = "1.6.1"
|
||||||
def dagger = "2.40.5"
|
def dagger = "2.42"
|
||||||
def retrofit = "2.9.0"
|
def retrofit = "2.9.0"
|
||||||
def arrow = "0.8.2"
|
def arrow = "0.8.2"
|
||||||
def markwon = "4.6.2"
|
def markwon = "4.6.2"
|
||||||
def moshi = "1.13.0"
|
def moshi = "1.13.0"
|
||||||
def lifecycle = "2.4.0"
|
def lifecycle = "2.4.1"
|
||||||
def flowBinding = "1.2.0"
|
def flowBinding = "1.2.0"
|
||||||
def epoxy = "4.6.2"
|
def epoxy = "4.6.2"
|
||||||
def mavericks = "2.5.0"
|
def mavericks = "2.6.1"
|
||||||
def glide = "4.12.0"
|
def glide = "4.13.2"
|
||||||
def bigImageViewer = "1.8.1"
|
def bigImageViewer = "1.8.1"
|
||||||
def jjwt = "0.11.2"
|
def jjwt = "0.11.5"
|
||||||
def vanniktechEmoji = "0.8.0"
|
def vanniktechEmoji = "0.9.0"
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
def mockk = "1.12.1"
|
def mockk = "1.12.4"
|
||||||
def espresso = "3.4.0"
|
def espresso = "3.4.0"
|
||||||
def androidxTest = "1.4.0"
|
def androidxTest = "1.4.0"
|
||||||
def androidxOrchestrator = "1.4.1"
|
def androidxOrchestrator = "1.4.1"
|
||||||
@ -45,15 +45,15 @@ ext.libs = [
|
|||||||
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
|
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
|
||||||
],
|
],
|
||||||
androidx : [
|
androidx : [
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.4.0",
|
'appCompat' : "androidx.appcompat:appcompat:1.4.1",
|
||||||
'core' : "androidx.core:core-ktx:1.7.0",
|
'core' : "androidx.core:core-ktx:1.7.0",
|
||||||
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1",
|
||||||
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
|
||||||
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.0",
|
'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1",
|
||||||
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.2",
|
'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.3",
|
||||||
'work' : "androidx.work:work-runtime-ktx:2.7.1",
|
'work' : "androidx.work:work-runtime-ktx:2.7.1",
|
||||||
'autoFill' : "androidx.autofill:autofill:1.1.0",
|
'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",
|
'junit' : "androidx.test.ext:junit:1.1.3",
|
||||||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||||
@ -72,7 +72,7 @@ ext.libs = [
|
|||||||
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
|
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
|
||||||
],
|
],
|
||||||
google : [
|
google : [
|
||||||
'material' : "com.google.android.material:material:1.5.0"
|
'material' : "com.google.android.material:material:1.6.0"
|
||||||
],
|
],
|
||||||
dagger : [
|
dagger : [
|
||||||
'dagger' : "com.google.dagger:dagger:$dagger",
|
'dagger' : "com.google.dagger:dagger:$dagger",
|
||||||
|
@ -123,6 +123,7 @@ ext.groups = [
|
|||||||
'io.github.detekt.sarif4k',
|
'io.github.detekt.sarif4k',
|
||||||
'io.github.microutils',
|
'io.github.microutils',
|
||||||
'io.github.reactivecircus.flowbinding',
|
'io.github.reactivecircus.flowbinding',
|
||||||
|
'io.gitlab.arturbosch.detekt',
|
||||||
'io.grpc',
|
'io.grpc',
|
||||||
'io.jsonwebtoken',
|
'io.jsonwebtoken',
|
||||||
'io.kindedj',
|
'io.kindedj',
|
||||||
@ -195,6 +196,7 @@ ext.groups = [
|
|||||||
'org.testng',
|
'org.testng',
|
||||||
'org.threeten',
|
'org.threeten',
|
||||||
'org.webjars',
|
'org.webjars',
|
||||||
|
'org.yaml',
|
||||||
'ru.noties',
|
'ru.noties',
|
||||||
'xerces',
|
'xerces',
|
||||||
'xml-apis',
|
'xml-apis',
|
||||||
|
@ -43,14 +43,17 @@ virtualenv -p python3 env
|
|||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
pip install -e .
|
pip install -e .
|
||||||
demo/start.sh --no-rate-limit
|
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
|
```bash
|
||||||
pip install matrix-synapse
|
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.
|
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
|
## 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.
|
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
|
### virtualenv command fails
|
||||||
|
|
||||||
You can try using
|
You can try using
|
||||||
|
@ -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.
|
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
|
##### 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:
|
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:
|
||||||
|
@ -176,4 +176,4 @@ class SettingsAdvancedRobot {
|
|||||||
clickOn(R.string.settings_developer_mode_summary)
|
clickOn(R.string.settings_developer_mode_summary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -44,7 +44,7 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
freeCompilerArgs += [
|
freeCompilerArgs += [
|
||||||
"-Xopt-in=kotlin.RequiresOptIn"
|
"-opt-in=kotlin.RequiresOptIn"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,4 +52,4 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.androidx.appCompat
|
implementation libs.androidx.appCompat
|
||||||
implementation libs.jetbrains.coroutinesAndroid
|
implementation libs.jetbrains.coroutinesAndroid
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,14 @@ apply plugin: 'com.jakewharton.butterknife'
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
// Do not use `google()`, it prevents Dependabot from working properly
|
||||||
mavenCentral()
|
maven {
|
||||||
|
url 'https://maven.google.com'
|
||||||
|
}
|
||||||
|
// Do not use `mavenCentral()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://repo1.maven.org/maven2'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
|
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.lib.multipicker
|
package im.vector.lib.multipicker
|
||||||
|
|
||||||
class MultiPicker<T> {
|
class MultiPicker<T> private constructor() {
|
||||||
|
|
||||||
companion object Type {
|
companion object Type {
|
||||||
val IMAGE by lazy { MultiPicker<ImagePicker>() }
|
val IMAGE by lazy { MultiPicker<ImagePicker>() }
|
||||||
|
@ -128,6 +128,14 @@
|
|||||||
<color name="vctr_presence_indicator_online_light">@color/palette_element_green</color>
|
<color name="vctr_presence_indicator_online_light">@color/palette_element_green</color>
|
||||||
<color name="vctr_presence_indicator_online_dark">@color/palette_element_green</color>
|
<color name="vctr_presence_indicator_online_dark">@color/palette_element_green</color>
|
||||||
|
|
||||||
|
<attr name="vctr_presence_indicator_busy" format="color" />
|
||||||
|
<color name="vctr_presence_indicator_busy_light">@color/element_alert_light</color>
|
||||||
|
<color name="vctr_presence_indicator_busy_dark">@color/element_alert_dark</color>
|
||||||
|
|
||||||
|
<attr name="vctr_presence_indicator_away" format="color" />
|
||||||
|
<color name="vctr_presence_indicator_away_light">@color/palette_element_orange</color>
|
||||||
|
<color name="vctr_presence_indicator_away_dark">@color/palette_element_orange</color>
|
||||||
|
|
||||||
<!-- Location sharing colors -->
|
<!-- Location sharing colors -->
|
||||||
<attr name="vctr_live_location" format="color" />
|
<attr name="vctr_live_location" format="color" />
|
||||||
<color name="vctr_live_location_light">@color/palette_prune</color>
|
<color name="vctr_live_location_light">@color/palette_prune</color>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<color name="palette_melon">#FF812D</color>
|
<color name="palette_melon">#FF812D</color>
|
||||||
|
|
||||||
<color name="palette_element_green">#0DBD8B</color>
|
<color name="palette_element_green">#0DBD8B</color>
|
||||||
|
<color name="palette_element_orange">#D9B072</color>
|
||||||
<color name="palette_white">#FFFFFF</color>
|
<color name="palette_white">#FFFFFF</color>
|
||||||
<color name="palette_vermilion">#FF5B55</color>
|
<color name="palette_vermilion">#FF5B55</color>
|
||||||
<!-- (unused) -->
|
<!-- (unused) -->
|
||||||
|
@ -44,6 +44,8 @@
|
|||||||
<!-- Presence Indicator colors -->
|
<!-- Presence Indicator colors -->
|
||||||
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
|
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
|
||||||
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_dark</item>
|
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_dark</item>
|
||||||
|
<item name="vctr_presence_indicator_busy">@color/vctr_presence_indicator_busy_dark</item>
|
||||||
|
<item name="vctr_presence_indicator_away">@color/vctr_presence_indicator_away_dark</item>
|
||||||
|
|
||||||
<!-- Some aliases -->
|
<!-- Some aliases -->
|
||||||
<item name="vctr_header_background">?vctr_system</item>
|
<item name="vctr_header_background">?vctr_system</item>
|
||||||
|
@ -44,6 +44,8 @@
|
|||||||
<!-- Presence Indicator colors -->
|
<!-- Presence Indicator colors -->
|
||||||
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
|
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
|
||||||
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_light</item>
|
<item name="vctr_presence_indicator_online">@color/vctr_presence_indicator_online_light</item>
|
||||||
|
<item name="vctr_presence_indicator_busy">@color/vctr_presence_indicator_busy_light</item>
|
||||||
|
<item name="vctr_presence_indicator_away">@color/vctr_presence_indicator_away_light</item>
|
||||||
|
|
||||||
<!-- Some aliases -->
|
<!-- Some aliases -->
|
||||||
<item name="vctr_header_background">?vctr_system</item>
|
<item name="vctr_header_background">?vctr_system</item>
|
||||||
|
@ -7,7 +7,10 @@ apply plugin: "org.jetbrains.dokka"
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
// Do not use `mavenCentral()`, it prevents Dependabot from working properly
|
||||||
|
maven {
|
||||||
|
url 'https://repo1.maven.org/maven2'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "io.realm:realm-gradle-plugin:10.9.0"
|
classpath "io.realm:realm-gradle-plugin:10.9.0"
|
||||||
@ -98,6 +101,9 @@ android {
|
|||||||
freeCompilerArgs += [
|
freeCompilerArgs += [
|
||||||
// Disabled for now, there are too many errors. Could be handled in another dedicated PR
|
// Disabled for now, there are too many errors. Could be handled in another dedicated PR
|
||||||
// '-Xexplicit-api=strict', // or warning
|
// '-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
|
implementation libs.arrow.instances
|
||||||
|
|
||||||
// olm lib is now hosted in MavenCentral
|
// 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
|
// DI
|
||||||
implementation libs.dagger.dagger
|
implementation libs.dagger.dagger
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
# Module matrix-sdk-android
|
# Module matrix-sdk-android
|
||||||
|
|
||||||
|
<!-- Note: the line below will appear only when the documentation is generated from Element-Android project, and not when it's generated from the SDK project -->
|
||||||
|
**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!
|
## Welcome to the matrix-sdk-android documentation!
|
||||||
|
|
||||||
This pages list the complete API that this SDK is exposing to a client application.
|
This pages list the complete API that this SDK is exposing to a client application.
|
||||||
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.common
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import org.amshove.kluent.fail
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
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.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.MegolmBackupAuthData
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
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.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
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.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.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
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.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
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.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.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.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.awaitCallback
|
||||||
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@ -188,17 +203,49 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
// Alice sends a message
|
// 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
|
// Bob send 3 messages
|
||||||
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
|
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1).first().eventId.let { sentEventId ->
|
||||||
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
|
// ensure alice got it
|
||||||
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
|
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
|
// 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
|
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) {
|
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
|
||||||
assertEquals(EventType.ENCRYPTED, event.type)
|
assertEquals(EventType.ENCRYPTED, event.type)
|
||||||
assertNotNull(event.content)
|
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<MegolmBackupCreationInfo> {
|
||||||
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||||
|
}
|
||||||
|
val version = awaitCallback<KeysVersion> {
|
||||||
|
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) {
|
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
|
||||||
val requestID = UUID.randomUUID().toString()
|
|
||||||
val aliceVerificationService = alice.cryptoService().verificationService()
|
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||||
val bobVerificationService = bob.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(
|
aliceVerificationService.beginKeyVerificationInDMs(
|
||||||
VerificationMethod.SAS,
|
VerificationMethod.SAS,
|
||||||
requestID,
|
requestID!!,
|
||||||
roomId,
|
roomId,
|
||||||
bob.myUserId,
|
bob.myUserId,
|
||||||
bob.sessionParams.credentials.deviceId!!
|
bob.sessionParams.credentials.deviceId!!
|
||||||
@ -316,23 +450,9 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||||
|
|
||||||
// wait for alice to get the ready
|
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
|
||||||
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
|
|
||||||
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
}
|
}
|
||||||
@ -340,7 +460,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||||||
// wait for alice to get the ready
|
// wait for alice to get the ready
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
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}")
|
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||||
bobPovTx?.performAccept()
|
bobPovTx?.performAccept()
|
||||||
@ -392,4 +512,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||||||
|
|
||||||
return CryptoTestData(roomId, sessions)
|
return CryptoTestData(roomId, sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
||||||
|
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<MessageContent>()?.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureCannotDecrypt(sentEventIds: List<String>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters
|
|||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.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.KeysVersion
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
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.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.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.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
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.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
@RunWith(JUnit4::class)
|
@RunWith(JUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class E2eeSanityTests : InstrumentedTest {
|
class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
private val testHelper = CommonTestHelper(context())
|
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple test that create an e2ee room.
|
* Simple test that create an e2ee room.
|
||||||
* Some new members are added, and a message is sent.
|
* Some new members are added, and a message is sent.
|
||||||
@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testSendingE2EEMessages() {
|
fun testSendingE2EEMessages() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val e2eRoomID = cryptoTestData.roomId
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
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
|
// add some more users and invite them
|
||||||
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
||||||
.map {
|
.map {
|
||||||
testHelper.createAccount(it, SessionTestParams(true))
|
testHelper.createAccount(it, SessionTestParams(true)).also {
|
||||||
|
it.cryptoService().enableKeyGossiping(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v("#E2E TEST", "All accounts created")
|
Log.v("#E2E TEST", "All accounts created")
|
||||||
@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
|
|
||||||
// All user should accept invite
|
// All user should accept invite
|
||||||
otherAccounts.forEach { otherSession ->
|
otherAccounts.forEach { otherSession ->
|
||||||
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
|
||||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that alice see them as joined (not really necessary?)
|
// 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", "All users have joined the room")
|
||||||
Log.v("#E2E TEST", "Alice is sending the message")
|
Log.v("#E2E TEST", "Alice is sending the message")
|
||||||
|
|
||||||
val text = "This is my 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()
|
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
||||||
Assert.assertTrue("Message should be sent", sentEventId != null)
|
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||||
|
|
||||||
@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
otherAccounts.forEach { otherSession ->
|
otherAccounts.forEach { otherSession ->
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newAccount.forEach {
|
newAccount.forEach {
|
||||||
waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
|
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
|
||||||
|
|
||||||
// wait a bit
|
// wait a bit
|
||||||
testHelper.runBlockingTest {
|
testHelper.runBlockingTest {
|
||||||
@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
Log.v("#E2E TEST", "Alice sends a new message")
|
Log.v("#E2E TEST", "Alice sends a new message")
|
||||||
|
|
||||||
val secondMessage = "2 This is my 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
|
// new members should be able to decrypt it
|
||||||
newAccount.forEach { otherSession ->
|
newAccount.forEach { otherSession ->
|
||||||
@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
cryptoTestData.cleanUp(testHelper)
|
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
|
* Quick test for basic key backup
|
||||||
* 1. Create e2e between Alice and Bob
|
* 1. Create e2e between Alice and Bob
|
||||||
@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBasicBackupImport() {
|
fun testBasicBackupImport() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
val sentEventIds = mutableListOf<String>()
|
val sentEventIds = mutableListOf<String>()
|
||||||
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||||
messagesText.forEach { text ->
|
messagesText.forEach { text ->
|
||||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||||
sentEventIds.add(it)
|
sentEventIds.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we want more so let's discard the session
|
// 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
|
// 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
|
// Let's now import keys from backup
|
||||||
|
|
||||||
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
newBobSession.cryptoService().keysBackupService().let { kbs ->
|
||||||
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||||
keysBackupService.getVersion(version.version, it)
|
kbs.getVersion(version.version, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||||
keysBackupService.restoreKeyBackupWithPassword(
|
kbs.restoreKeyBackupWithPassword(
|
||||||
keyVersionResult!!,
|
keyVersionResult!!,
|
||||||
keyBackupPassword,
|
keyBackupPassword,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null, it
|
null,
|
||||||
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensure bob can now decrypt
|
// ensure bob can now decrypt
|
||||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
|
|
||||||
testHelper.signOutAndClose(newBobSession)
|
testHelper.signOutAndClose(newBobSession)
|
||||||
}
|
}
|
||||||
@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testSimpleGossip() {
|
fun testSimpleGossip() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
cryptoTestHelper.initializeCrossSigning(bobSession)
|
|
||||||
|
|
||||||
// let's send a few message to bob
|
// let's send a few message to bob
|
||||||
val sentEventIds = mutableListOf<String>()
|
val sentEventIds = mutableListOf<String>()
|
||||||
val messagesText = listOf("1. Hello", "2. Bob")
|
val messagesText = listOf("1. Hello", "2. Bob")
|
||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
messagesText.forEach { text ->
|
messagesText.forEach { text ->
|
||||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||||
sentEventIds.add(it)
|
sentEventIds.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure bob can decrypt
|
// Ensure bob can decrypt
|
||||||
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
|
ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
|
||||||
|
|
||||||
// Let's now add a new bob session
|
// Let's now add a new bob session
|
||||||
// Create a new session for bob
|
// Create a new session for bob
|
||||||
@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
// check that new bob can't currently decrypt
|
// check that new bob can't currently decrypt
|
||||||
Log.v("#E2E TEST", "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
|
// Try to request
|
||||||
sentEventIds.forEach { sentEventId ->
|
sentEventIds.forEach { sentEventId ->
|
||||||
@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait a bit
|
// wait a bit
|
||||||
testHelper.runBlockingTest {
|
// we need to wait a couple of syncs to let sharing occurs
|
||||||
delay(10_000)
|
// testHelper.waitFewSyncs(newBobSession, 6)
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
// 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<EncryptedEventContent>()!!.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
|
// Now mark new bob session as verified
|
||||||
|
|
||||||
@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a bit
|
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
testHelper.runBlockingTest {
|
|
||||||
delay(10_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
|
||||||
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
cryptoTestData.cleanUp(testHelper)
|
||||||
testHelper.signOutAndClose(newBobSession)
|
testHelper.signOutAndClose(newBobSession)
|
||||||
@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testForwardBetterKey() {
|
fun testForwardBetterKey() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
||||||
@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
|
|
||||||
|
|
||||||
// let's send a few message to bob
|
// let's send a few message to bob
|
||||||
var firstEventId: String
|
var firstEventId: String
|
||||||
val firstMessage = "1. Hello"
|
val firstMessage = "1. Hello"
|
||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
firstMessage.let { text ->
|
firstMessage.let { text ->
|
||||||
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure bob can decrypt
|
// Ensure bob can decrypt
|
||||||
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||||
|
|
||||||
// Let's add a new unverified session from bob
|
// Let's add a new unverified session from bob
|
||||||
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
// check that new bob can't currently decrypt
|
// check that new bob can't currently decrypt
|
||||||
Log.v("#E2E TEST", "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
|
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
||||||
var secondEventId: String
|
var secondEventId: String
|
||||||
@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
secondMessage.let { text ->
|
secondMessage.let { text ->
|
||||||
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
try {
|
try {
|
||||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
fail("Should not be able to decrypt event")
|
fail("Should not be able to decrypt event")
|
||||||
} catch (error: MXCryptoError) {
|
} catch (_: MXCryptoError) {
|
||||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
|
||||||
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
||||||
|
|
||||||
// now let new session request
|
// now let new session request
|
||||||
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||||
|
|
||||||
// wait a bit
|
// We need to wait for the key request to be sent out and then a reply to be received
|
||||||
testHelper.runBlockingTest {
|
|
||||||
delay(10_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// old session should have shared the key at earliest known index now
|
// old session should have shared the key at earliest known index now
|
||||||
// we should be able to decrypt both
|
// we should be able to decrypt both
|
||||||
testHelper.runBlockingTest {
|
testHelper.waitWithLatch {
|
||||||
try {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
val canDecryptFirst = try {
|
||||||
} catch (error: MXCryptoError) {
|
testHelper.runBlockingTest {
|
||||||
fail("Should be able to decrypt first event now $error")
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
}
|
}
|
||||||
}
|
true
|
||||||
testHelper.runBlockingTest {
|
} catch (error: MXCryptoError) {
|
||||||
try {
|
false
|
||||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
}
|
||||||
} catch (error: MXCryptoError) {
|
val canDecryptSecond = try {
|
||||||
fail("Should be able to decrypt event $error")
|
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)
|
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)
|
aliceRoomPOV.sendService().sendTextMessage(text)
|
||||||
var sentEventId: String? = null
|
var sentEventId: String? = null
|
||||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||||
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
||||||
timeline.start()
|
timeline.start()
|
||||||
|
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val decryptedMsg = timeline.getSnapshot()
|
val decryptedMsg = timeline.getSnapshot()
|
||||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||||
@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
return sentEventId
|
return sentEventId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, 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<Session>, e2eRoomID: String) {
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
otherAccounts.map {
|
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.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
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")
|
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||||
try {
|
try {
|
||||||
otherSession.roomService().joinRoom(e2eRoomID)
|
otherSession.roomService().joinRoom(e2eRoomID)
|
||||||
@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||||
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<MessageContent>()?.body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
sentEventIds.forEach { sentEventId ->
|
sentEventIds.forEach { sentEventId ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureCannotDecrypt(sentEventIds: List<String>, 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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters
|
|||||||
import org.matrix.android.sdk.InstrumentedTest
|
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.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
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.getTimelineEvent
|
||||||
@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
|
|||||||
// clear any outbound session
|
// clear any outbound session
|
||||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||||
|
|
||||||
val preShareCount = bobSession.cryptoService().getGossipingEvents().count {
|
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||||
it.senderId == aliceSession.myUserId &&
|
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
||||||
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
||||||
@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
|
|||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val newGossipCount = bobSession.cryptoService().getGossipingEvents().count {
|
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||||
it.senderId == aliceSession.myUserId &&
|
newKeysCount > preShareCount
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
newGossipCount > preShareCount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull {
|
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||||
it.senderId == aliceSession.myUserId &&
|
val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = latest?.getClearContent().toModel<RoomKeyContent>()
|
val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||||
assertNotNull("Bob should have received and decrypted a room key event from alice", content)
|
val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!!
|
||||||
assertEquals("Wrong room", e2eRoomID, content!!.roomId)
|
val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
||||||
val megolmSessionId = content.sessionId!!
|
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)
|
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
||||||
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
||||||
|
@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import junit.framework.TestCase.assertNotNull
|
import junit.framework.TestCase.assertNotNull
|
||||||
import junit.framework.TestCase.assertTrue
|
import junit.framework.TestCase.assertTrue
|
||||||
import junit.framework.TestCase.fail
|
import junit.framework.TestCase.fail
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||||
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.crosssigning.DeviceTrustLevel
|
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.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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
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.getTimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
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.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.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class KeyShareTests : InstrumentedTest {
|
class KeyShareTests : InstrumentedTest {
|
||||||
|
|
||||||
private val commonTestHelper = CommonTestHelper(context())
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_DoNotSelfShareIfNotTrusted() {
|
fun test_DoNotSelfShareIfNotTrusted() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
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
|
// Create an encrypted room and add a message
|
||||||
val roomId = commonTestHelper.runBlockingTest {
|
val roomId = commonTestHelper.runBlockingTest {
|
||||||
@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
assertNotNull(room)
|
assertNotNull(room)
|
||||||
Thread.sleep(4_000)
|
Thread.sleep(4_000)
|
||||||
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
|
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)
|
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||||
|
|
||||||
@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
|
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
|
||||||
|
|
||||||
// Try to request
|
// Try to request
|
||||||
|
aliceSession2.cryptoService().enableKeyGossiping(true)
|
||||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
||||||
@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
.filter { req ->
|
|
||||||
// filter out request that was known before
|
|
||||||
!outgoingRequestsBefore.any { req.requestId == it.requestId }
|
|
||||||
}
|
|
||||||
.let {
|
.let {
|
||||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||||
outGoingRequestId = outgoing?.requestId
|
outGoingRequestId = outgoing?.requestId
|
||||||
@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
// DEBUG LOGS
|
// DEBUG LOGS
|
||||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
// 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", "=========================")
|
Log.v("TEST", "=========================")
|
||||||
it.forEach { keyRequest ->
|
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||||
Log.v(
|
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||||
"TEST",
|
|
||||||
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Log.v("TEST", "=========================")
|
Log.v("TEST", "=========================")
|
||||||
}
|
}
|
||||||
|
|
||||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||||
incoming?.state == GossipingRequestState.REJECTED
|
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
|
// Re request
|
||||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTestHelper.signOutAndClose(aliceSession)
|
commonTestHelper.signOutAndClose(aliceSession)
|
||||||
commonTestHelper.signOutAndClose(aliceSession2)
|
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
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
fun test_reShareIfWasIntendedToBeShared() {
|
||||||
fun test_ShareSSSSSecret() {
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
commonTestHelper.doSync<Unit> {
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
aliceSession1.cryptoService().crossSigningService()
|
val aliceSession = testData.firstSession
|
||||||
.initializeCrossSigning(
|
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
val bobSession = testData.secondSession!!
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
|
||||||
user = aliceSession1.myUserId,
|
|
||||||
password = TestConstants.PASSWORD
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also bootstrap keybackup on first session
|
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||||
val creationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
|
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
|
||||||
}
|
|
||||||
val version = commonTestHelper.doSync<KeysVersion> {
|
|
||||||
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
|
||||||
}
|
|
||||||
// Save it for gossiping
|
|
||||||
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
|
||||||
|
|
||||||
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()
|
// Let's try to request any how.
|
||||||
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
// As it was share previously alice should accept to reshare
|
||||||
|
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||||
// force keys download
|
|
||||||
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
|
|
||||||
}
|
|
||||||
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(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
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
|
||||||
fun test_ImproperKeyShareBug() {
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
commonTestHelper.doSync<Unit> {
|
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
|
||||||
aliceSession.cryptoService().crossSigningService()
|
val aliceSession = testData.firstSession
|
||||||
.initializeCrossSigning(
|
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
// we wait for alice first session to be aware of that session?
|
||||||
user = aliceSession.myUserId,
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
password = TestConstants.PASSWORD,
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
session = flowResponse.session
|
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||||
)
|
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||||
)
|
newSession != null
|
||||||
}
|
}
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||||
|
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
|
||||||
// Create an encrypted room and send a couple of messages
|
// Let's try to request any how.
|
||||||
val roomId = commonTestHelper.runBlockingTest {
|
// As it was share previously alice should accept to reshare
|
||||||
aliceSession.roomService().createRoom(
|
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||||
CreateRoomParams().apply {
|
|
||||||
visibility = RoomDirectoryVisibility.PRIVATE
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
enableEncryption()
|
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))
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
commonTestHelper.doSync<Unit> {
|
val aliceSession = testData.firstSession
|
||||||
bobSession.cryptoService().crossSigningService()
|
val bobSession = testData.secondSession!!
|
||||||
.initializeCrossSigning(
|
val roomFromBob = bobSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
|
||||||
user = bobSession.myUserId,
|
|
||||||
password = TestConstants.PASSWORD,
|
|
||||||
session = flowResponse.session
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let alice invite bob
|
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
|
||||||
commonTestHelper.runBlockingTest {
|
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
roomAlicePov.membershipService().invite(bobSession.myUserId, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTestHelper.runBlockingTest {
|
// Let alice now add a new session
|
||||||
bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList())
|
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
|
||||||
}
|
aliceNewSession.cryptoService().enableKeyGossiping(false)
|
||||||
|
commonTestHelper.syncSession(aliceNewSession)
|
||||||
|
|
||||||
// we want to discard alice outbound session
|
// we wait bob first session to be aware of that session?
|
||||||
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
// and now resend a new message to reset index to 0
|
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||||
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
|
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||||
|
newSession != null
|
||||||
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
|
||||||
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
|
||||||
|
|
||||||
var dRes = tryOrNull {
|
|
||||||
commonTestHelper.runBlockingTest {
|
|
||||||
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<EncryptedEventContent>()!!.sessionId)
|
||||||
|
}
|
||||||
|
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.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 :/
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
dRes = tryOrNull {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
commonTestHelper.runBlockingTest {
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
|
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<MessageContent>()?.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<EncryptedEventContent>()!!.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<EncryptedEventContent>()!!.sessionId)
|
||||||
|
}
|
||||||
|
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
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.NoOpMatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
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.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.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
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.content.WithHeldCode
|
||||||
@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants
|
|||||||
@LargeTest
|
@LargeTest
|
||||||
class WithHeldTests : InstrumentedTest {
|
class WithHeldTests : InstrumentedTest {
|
||||||
|
|
||||||
private val testHelper = CommonTestHelper(context())
|
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldUnverifiedReason() {
|
fun test_WithHeldUnverifiedReason() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
// =============================
|
// =============================
|
||||||
@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
||||||
|
|
||||||
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// ACT
|
// ACT
|
||||||
// =============================
|
// =============================
|
||||||
@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
|
|
||||||
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
|
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
|
||||||
|
|
||||||
|
val megolmSessionId = eventBobPOV.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
// =============================
|
// =============================
|
||||||
// ASSERT
|
// ASSERT
|
||||||
// =============================
|
// =============================
|
||||||
@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
val technicalMessage = failure.technicalMessage
|
val technicalMessage = failure.technicalMessage
|
||||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
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
|
// enable back sending to unverified
|
||||||
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
||||||
|
|
||||||
@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
val technicalMessage = failure.technicalMessage
|
val technicalMessage = failure.technicalMessage
|
||||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
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)
|
testHelper.signOutAndClose(aliceSession)
|
||||||
@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldNoOlm() {
|
fun test_WithHeldNoOlm() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
val aliceSession = testData.firstSession
|
val aliceSession = testData.firstSession
|
||||||
val bobSession = testData.secondSession!!
|
val bobSession = testData.secondSession!!
|
||||||
@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldKeyRequest() {
|
fun test_WithHeldKeyRequest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
val aliceSession = testData.firstSession
|
val aliceSession = testData.firstSession
|
||||||
val bobSession = testData.secondSession!!
|
val bobSession = testData.secondSession!!
|
||||||
@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest {
|
|||||||
wc?.code == WithHeldCode.UNAUTHORISED
|
wc?.code == WithHeldCode.UNAUTHORISED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
testHelper.signOutAndClose(bobSecondSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull
|
|||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
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.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
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.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.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.MegolmBackupCreationInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
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.model.ImportRoomKeysResult
|
||||||
@ -54,18 +55,16 @@ import java.util.concurrent.CountDownLatch
|
|||||||
@LargeTest
|
@LargeTest
|
||||||
class KeysBackupTest : InstrumentedTest {
|
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
|
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
||||||
* - Check backup keys after having marked one as backed up
|
* - Check backup keys after having marked one as backed up
|
||||||
* - Reset keys backup markers
|
* - Reset keys backup markers
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun roomKeysTest_testBackupStore_ok() {
|
fun roomKeysTest_testBackupStore_ok() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
// From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
// From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
||||||
@ -104,6 +103,8 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun prepareKeysBackupVersionTest() {
|
fun prepareKeysBackupVersionTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
|
||||||
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
||||||
|
|
||||||
assertNotNull(bobSession.cryptoService().keysBackupService())
|
assertNotNull(bobSession.cryptoService().keysBackupService())
|
||||||
@ -132,7 +133,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun createKeysBackupVersionTest() {
|
fun createKeysBackupVersionTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
|
||||||
|
cryptoTestHelper.initializeCrossSigning(bobSession)
|
||||||
|
|
||||||
val keysBackup = bobSession.cryptoService().keysBackupService()
|
val keysBackup = bobSession.cryptoService().keysBackupService()
|
||||||
|
|
||||||
@ -147,13 +152,46 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertFalse(keysBackup.isEnabled)
|
assertFalse(keysBackup.isEnabled)
|
||||||
|
|
||||||
// Create the version
|
// Create the version
|
||||||
testHelper.doSync<KeysVersion> {
|
val version = testHelper.doSync<KeysVersion> {
|
||||||
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup must be enable now
|
// Backup must be enable now
|
||||||
assertTrue(keysBackup.isEnabled)
|
assertTrue(keysBackup.isEnabled)
|
||||||
|
|
||||||
|
// Check that it's signed with MSK
|
||||||
|
val versionResult = testHelper.doSync<KeysVersionResult?> {
|
||||||
|
keysBackup.getVersion(version.version, it)
|
||||||
|
}
|
||||||
|
val trust = testHelper.doSync<KeysBackupVersionTrust> {
|
||||||
|
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)
|
stateObserver.stopAndCheckStates(null)
|
||||||
testHelper.signOutAndClose(bobSession)
|
testHelper.signOutAndClose(bobSession)
|
||||||
}
|
}
|
||||||
@ -163,8 +201,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Check the backup completes
|
* - Check the backup completes
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun backupAfterCreateKeysBackupVersionTest() {
|
fun backupAfterCreateKeysBackupVersionTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
keysBackupTestHelper.waitForKeybackUpBatching()
|
keysBackupTestHelper.waitForKeybackUpBatching()
|
||||||
@ -204,8 +245,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* Check that backupAllGroupSessions() returns valid data
|
* Check that backupAllGroupSessions() returns valid data
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun backupAllGroupSessionsTest() {
|
fun backupAllGroupSessionsTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
|
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
|
||||||
@ -249,8 +293,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Compare the decrypted megolm key with the original one
|
* - Compare the decrypted megolm key with the original one
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testEncryptAndDecryptKeysBackupData() {
|
fun testEncryptAndDecryptKeysBackupData() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
|
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
|
||||||
@ -293,8 +340,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Restore must be successful
|
* - Restore must be successful
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun restoreKeysBackupTest() {
|
fun restoreKeysBackupTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
|
|
||||||
// - Restore the e2e backup from the homeserver
|
// - 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
|
* - It must be trusted and must have with 2 signatures now
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun trustKeyBackupVersionTest() {
|
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
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
@ -438,8 +491,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - It must be trusted and must have with 2 signatures now
|
* - It must be trusted and must have with 2 signatures now
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun trustKeyBackupVersionWithRecoveryKeyTest() {
|
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
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
@ -496,8 +552,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - The backup must still be untrusted and disabled
|
* - The backup must still be untrusted and disabled
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
|
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
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
@ -538,8 +597,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - It must be trusted and must have with 2 signatures now
|
* - It must be trusted and must have with 2 signatures now
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun trustKeyBackupVersionWithPasswordTest() {
|
fun trustKeyBackupVersionWithPasswordTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "Password"
|
val password = "Password"
|
||||||
|
|
||||||
// - Do an e2e backup to the homeserver with a 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
|
* - The backup must still be untrusted and disabled
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun trustKeyBackupVersionWithWrongPasswordTest() {
|
fun trustKeyBackupVersionWithWrongPasswordTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "Password"
|
val password = "Password"
|
||||||
val badPassword = "Bad Password"
|
val badPassword = "Bad Password"
|
||||||
|
|
||||||
@ -639,8 +704,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - It must fail
|
* - It must fail
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
|
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
|
|
||||||
// - Try to restore the e2e backup with a wrong recovery key
|
// - Try to restore the e2e backup with a wrong recovery key
|
||||||
@ -673,8 +741,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Restore must be successful
|
* - Restore must be successful
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testBackupWithPassword() {
|
fun testBackupWithPassword() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
|
||||||
@ -730,8 +801,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - It must fail
|
* - It must fail
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun restoreKeysBackupWithAWrongPasswordTest() {
|
fun restoreKeysBackupWithAWrongPasswordTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
val wrongPassword = "passw0rd"
|
val wrongPassword = "passw0rd"
|
||||||
|
|
||||||
@ -767,8 +841,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Restore must be successful
|
* - Restore must be successful
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
|
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
|
||||||
@ -797,8 +874,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - It must fail
|
* - It must fail
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
|
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
|
|
||||||
// - Try to restore the e2e backup with a password
|
// - Try to restore the e2e backup with a password
|
||||||
@ -829,8 +909,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* - Check the returned KeysVersionResult is trusted
|
* - Check the returned KeysVersionResult is trusted
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testIsKeysBackupTrusted() {
|
fun testIsKeysBackupTrusted() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -855,7 +938,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertTrue(keysBackupVersionTrust.usable)
|
assertTrue(keysBackupVersionTrust.usable)
|
||||||
assertEquals(1, keysBackupVersionTrust.signatures.size)
|
assertEquals(1, keysBackupVersionTrust.signatures.size)
|
||||||
|
|
||||||
val signature = keysBackupVersionTrust.signatures[0]
|
val signature = keysBackupVersionTrust.signatures[0] as KeysBackupVersionTrustSignature.DeviceSignature
|
||||||
assertTrue(signature.valid)
|
assertTrue(signature.valid)
|
||||||
assertNotNull(signature.device)
|
assertNotNull(signature.device)
|
||||||
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
|
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
|
||||||
@ -865,66 +948,6 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
cryptoTestData.cleanUp(testHelper)
|
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
|
* Check WrongBackUpVersion state
|
||||||
*
|
*
|
||||||
@ -935,6 +958,10 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBackupWhenAnotherBackupWasCreated() {
|
fun testBackupWhenAnotherBackupWasCreated() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -1005,8 +1032,11 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
* -> It must success
|
* -> It must success
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testBackupAfterVerifyingADevice() {
|
fun testBackupAfterVerifyingADevice() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -1039,6 +1069,8 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
// - Try to backup all in aliceSession2, it must fail
|
// - Try to backup all in aliceSession2, it must fail
|
||||||
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
|
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
|
||||||
|
|
||||||
|
assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
|
||||||
|
|
||||||
val stateObserver2 = StateObserver(keysBackup2)
|
val stateObserver2 = StateObserver(keysBackup2)
|
||||||
|
|
||||||
var isSuccessful = false
|
var isSuccessful = false
|
||||||
@ -1056,8 +1088,8 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertFalse(isSuccessful)
|
assertFalse(isSuccessful)
|
||||||
|
|
||||||
// Backup state must be NotTrusted
|
// Backup state must be NotTrusted
|
||||||
assertEquals(KeysBackupState.NotTrusted, keysBackup2.state)
|
assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.state)
|
||||||
assertFalse(keysBackup2.isEnabled)
|
assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
|
||||||
|
|
||||||
// - Validate the old device from the new one
|
// - Validate the old device from the new one
|
||||||
aliceSession2.cryptoService().setDeviceVerification(
|
aliceSession2.cryptoService().setDeviceVerification(
|
||||||
@ -1103,6 +1135,10 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun deleteKeysBackupTest() {
|
fun deleteKeysBackupTest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
|
@ -106,14 +106,14 @@ internal class KeysBackupTestHelper(
|
|||||||
|
|
||||||
Assert.assertNotNull(megolmBackupCreationInfo)
|
Assert.assertNotNull(megolmBackupCreationInfo)
|
||||||
|
|
||||||
Assert.assertFalse(keysBackup.isEnabled)
|
Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled)
|
||||||
|
|
||||||
// Create the version
|
// Create the version
|
||||||
val keysVersion = testHelper.doSync<KeysVersion> {
|
val keysVersion = testHelper.doSync<KeysVersion> {
|
||||||
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.assertNotNull(keysVersion.version)
|
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
|
||||||
|
|
||||||
// Backup must be enable now
|
// Backup must be enable now
|
||||||
Assert.assertTrue(keysBackup.isEnabled)
|
Assert.assertTrue(keysBackup.isEnabled)
|
||||||
|
@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.amshove.kluent.shouldBe
|
import org.amshove.kluent.shouldBe
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
@ -40,7 +39,6 @@ import kotlin.coroutines.resume
|
|||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@Ignore("This test is flaky ; see issue #5449")
|
|
||||||
class VerificationTest : InstrumentedTest {
|
class VerificationTest : InstrumentedTest {
|
||||||
|
|
||||||
data class ExpectedResult(
|
data class ExpectedResult(
|
||||||
|
@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
|
|||||||
* If set to false, the request will be forwarded to the application layer; in this
|
* If set to false, the request will be forwarded to the application layer; in this
|
||||||
* case the application can decide to prompt the user.
|
* 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,
|
||||||
)
|
)
|
||||||
|
@ -22,15 +22,15 @@ package org.matrix.android.sdk.api.logger
|
|||||||
* val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
|
* val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
|
||||||
* Timber.tag(loggerTag.value).v("My log message")
|
* 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 SYNC : LoggerTag("SYNC")
|
||||||
object VOIP : LoggerTag("VOIP")
|
object VOIP : LoggerTag("VOIP")
|
||||||
object CRYPTO : LoggerTag("CRYPTO")
|
object CRYPTO : LoggerTag("CRYPTO")
|
||||||
|
|
||||||
val value: String = if (parentTag == null) {
|
val value: String = if (parentTag == null) {
|
||||||
_value
|
name
|
||||||
} else {
|
} else {
|
||||||
"${parentTag.value}/$_value"
|
"${parentTag.value}/$name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
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.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.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
import org.matrix.android.sdk.api.session.crypto.model.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.MXEncryptEventContentResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
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.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.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
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.Event
|
||||||
@ -76,6 +75,15 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
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 setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||||
|
|
||||||
fun getDeviceTrackingStatus(userId: String): Int
|
fun getDeviceTrackingStatus(userId: String): Int
|
||||||
@ -94,8 +102,6 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun reRequestRoomKeyForEvent(event: Event)
|
fun reRequestRoomKeyForEvent(event: Event)
|
||||||
|
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
|
||||||
|
|
||||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
@ -142,14 +148,20 @@ interface CryptoService {
|
|||||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||||
|
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||||
|
|
||||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
/**
|
||||||
fun getGossipingEvents(): List<Event>
|
* 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<PagedList<AuditTrail>>
|
||||||
|
fun getGossipingEvents(): List<AuditTrail>
|
||||||
|
|
||||||
// For testing shared session
|
// For testing shared session
|
||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||||
|
@ -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<String, List<String>>,
|
||||||
|
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<RequestReply>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Used only for log.
|
||||||
|
*
|
||||||
|
* @return the room id.
|
||||||
|
*/
|
||||||
|
val roomId = requestBody?.roomId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used only for log.
|
||||||
|
*
|
||||||
|
* @return the session id
|
||||||
|
*/
|
||||||
|
val sessionId = requestBody?.sessionId
|
||||||
|
}
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,14 +14,20 @@
|
|||||||
* limitations under the License.
|
* 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,
|
UNSENT,
|
||||||
SENDING,
|
|
||||||
SENT,
|
SENT,
|
||||||
CANCELLING,
|
SENT_THEN_CANCELED,
|
||||||
CANCELLED,
|
CANCELLATION_PENDING,
|
||||||
FAILED_TO_SEND,
|
CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||||
FAILED_TO_CANCEL
|
|
||||||
|
companion object {
|
||||||
|
fun pendingStates() = setOf(
|
||||||
|
UNSENT,
|
||||||
|
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||||
|
CANCELLATION_PENDING
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -16,25 +16,35 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
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
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A signature in a `KeysBackupVersionTrust` object.
|
* A signature in a `KeysBackupVersionTrust` object.
|
||||||
*/
|
*/
|
||||||
data class KeysBackupVersionTrustSignature(
|
|
||||||
/**
|
|
||||||
* The id of the device that signed the backup version.
|
|
||||||
*/
|
|
||||||
val deviceId: String?,
|
|
||||||
|
|
||||||
/**
|
sealed class KeysBackupVersionTrustSignature {
|
||||||
* The device that signed the backup version.
|
|
||||||
* Can be null if the device is not known.
|
|
||||||
*/
|
|
||||||
val device: CryptoDeviceInfo?,
|
|
||||||
|
|
||||||
/**
|
data class DeviceSignature(
|
||||||
* Flag to indicate the signature from this device is valid.
|
/**
|
||||||
*/
|
* The id of the device that signed the backup version.
|
||||||
val valid: Boolean,
|
*/
|
||||||
)
|
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()
|
||||||
|
}
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.keyshare
|
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.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
|
* Room keys events listener
|
||||||
@ -35,12 +34,12 @@ interface GossipingRequestListener {
|
|||||||
* Returns the secret value to be shared
|
* Returns the secret value to be shared
|
||||||
* @return true if is handled
|
* @return true if is handled
|
||||||
*/
|
*/
|
||||||
fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean
|
fun onSecretShareRequest(request: SecretShareRequest): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A room key request cancellation has been received.
|
* A room key request cancellation has been received.
|
||||||
*
|
*
|
||||||
* @param request the cancellation request
|
* @param request the cancellation request
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
fun onRequestCancelled(request: IncomingRoomKeyRequest)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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<ShareRequestCancellation>()
|
|
||||||
?.let {
|
|
||||||
IncomingRequestCancellation(
|
|
||||||
userId = event.senderId,
|
|
||||||
deviceId = it.requestingDeviceId,
|
|
||||||
requestId = it.requestId,
|
|
||||||
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,9 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.model
|
package org.matrix.android.sdk.api.session.crypto.model
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||||
@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest(
|
|||||||
/**
|
/**
|
||||||
* The user id
|
* The user id
|
||||||
*/
|
*/
|
||||||
override val userId: String? = null,
|
val userId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The device id
|
* The device id
|
||||||
*/
|
*/
|
||||||
override val deviceId: String? = null,
|
val deviceId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request id
|
* The request id
|
||||||
*/
|
*/
|
||||||
override val requestId: String? = null,
|
val requestId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request body
|
* The request body
|
||||||
*/
|
*/
|
||||||
val requestBody: RoomKeyRequestBody? = null,
|
val requestBody: RoomKeyRequestBody? = null,
|
||||||
|
|
||||||
val state: GossipingRequestState = GossipingRequestState.NONE,
|
val localCreationTimestamp: Long?
|
||||||
|
) {
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Factory
|
* Factory
|
||||||
@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest(
|
|||||||
* @param event the event
|
* @param event the event
|
||||||
* @param currentTimeMillis the current time in milliseconds
|
* @param currentTimeMillis the current time in milliseconds
|
||||||
*/
|
*/
|
||||||
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? {
|
fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
|
||||||
return event.getClearContent()
|
return trail
|
||||||
.toModel<RoomKeyShareRequest>()
|
.takeIf { it.type == TrailType.IncomingKeyRequest }
|
||||||
|
?.let {
|
||||||
|
it.info as? IncomingKeyRequestInfo
|
||||||
|
}
|
||||||
?.let {
|
?.let {
|
||||||
IncomingRoomKeyRequest(
|
IncomingRoomKeyRequest(
|
||||||
userId = event.senderId,
|
userId = it.userId,
|
||||||
deviceId = it.requestingDeviceId,
|
deviceId = it.deviceId,
|
||||||
requestId = it.requestId,
|
requestId = it.requestId,
|
||||||
requestBody = it.body ?: RoomKeyRequestBody(),
|
requestBody = RoomKeyRequestBody(
|
||||||
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<SecretShareRequest>()
|
|
||||||
?.let {
|
|
||||||
IncomingSecretShareRequest(
|
|
||||||
userId = event.senderId,
|
|
||||||
deviceId = it.requestingDeviceId,
|
|
||||||
requestId = it.requestId,
|
|
||||||
secretName = it.secretName,
|
|
||||||
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<String, List<String>>,
|
|
||||||
// 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
|
|
||||||
}
|
|
@ -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.
|
* 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?
|
val code: WithHeldCode?
|
||||||
|
@ -28,7 +28,10 @@ enum class PresenceEnum(val value: String) {
|
|||||||
OFFLINE("offline"),
|
OFFLINE("offline"),
|
||||||
|
|
||||||
@Json(name = "unavailable")
|
@Json(name = "unavailable")
|
||||||
UNAVAILABLE("unavailable");
|
UNAVAILABLE("unavailable"),
|
||||||
|
|
||||||
|
@Json(name = "org.matrix.msc3026.busy")
|
||||||
|
BUSY("busy");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(s: String): PresenceEnum? = values().find { it.value == s }
|
fun from(s: String): PresenceEnum? = values().find { it.value == s }
|
||||||
|
@ -27,13 +27,13 @@ import timber.log.Timber
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RoomGuestAccessContent(
|
data class RoomGuestAccessContent(
|
||||||
// Required. Whether guests can join the room. One of: ["can_join", "forbidden"]
|
// 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
|
"can_join" -> GuestAccess.CanJoin
|
||||||
"forbidden" -> GuestAccess.Forbidden
|
"forbidden" -> GuestAccess.Forbidden
|
||||||
else -> {
|
else -> {
|
||||||
Timber.w("Invalid value for GuestAccess: `$_guestAccess`")
|
Timber.w("Invalid value for GuestAccess: `$guestAccessStr`")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,15 +22,15 @@ import timber.log.Timber
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RoomHistoryVisibilityContent(
|
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
|
"world_readable" -> RoomHistoryVisibility.WORLD_READABLE
|
||||||
"shared" -> RoomHistoryVisibility.SHARED
|
"shared" -> RoomHistoryVisibility.SHARED
|
||||||
"invited" -> RoomHistoryVisibility.INVITED
|
"invited" -> RoomHistoryVisibility.INVITED
|
||||||
"joined" -> RoomHistoryVisibility.JOINED
|
"joined" -> RoomHistoryVisibility.JOINED
|
||||||
else -> {
|
else -> {
|
||||||
Timber.w("Invalid value for RoomHistoryVisibility: `$_historyVisibility`")
|
Timber.w("Invalid value for RoomHistoryVisibility: `$historyVisibilityStr`")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RoomJoinRulesContent(
|
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),
|
* If the allow key is an empty list (or not a list at all),
|
||||||
* then no users are allowed to join without an invite.
|
* then no users are allowed to join without an invite.
|
||||||
@ -35,14 +35,14 @@ data class RoomJoinRulesContent(
|
|||||||
*/
|
*/
|
||||||
@Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null
|
@Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null
|
||||||
) {
|
) {
|
||||||
val joinRules: RoomJoinRules? = when (_joinRules) {
|
val joinRules: RoomJoinRules? = when (joinRulesStr) {
|
||||||
"public" -> RoomJoinRules.PUBLIC
|
"public" -> RoomJoinRules.PUBLIC
|
||||||
"invite" -> RoomJoinRules.INVITE
|
"invite" -> RoomJoinRules.INVITE
|
||||||
"knock" -> RoomJoinRules.KNOCK
|
"knock" -> RoomJoinRules.KNOCK
|
||||||
"private" -> RoomJoinRules.PRIVATE
|
"private" -> RoomJoinRules.PRIVATE
|
||||||
"restricted" -> RoomJoinRules.RESTRICTED
|
"restricted" -> RoomJoinRules.RESTRICTED
|
||||||
else -> {
|
else -> {
|
||||||
Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
|
Timber.w("Invalid value for RoomJoinRules: `$joinRulesStr`")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ class RestrictedRoomPreset(val homeServerCapabilities: HomeServerCapabilities, v
|
|||||||
type = EventType.STATE_ROOM_JOIN_RULES,
|
type = EventType.STATE_ROOM_JOIN_RULES,
|
||||||
stateKey = "",
|
stateKey = "",
|
||||||
content = RoomJoinRulesContent(
|
content = RoomJoinRulesContent(
|
||||||
_joinRules = RoomJoinRules.RESTRICTED.value,
|
joinRulesStr = RoomJoinRules.RESTRICTED.value,
|
||||||
allowList = restrictedList
|
allowList = restrictedList
|
||||||
).toContent()
|
).toContent()
|
||||||
)
|
)
|
||||||
|
@ -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
|
* The root thread eventId if this is a thread timeline, or null if this is NOT a thread timeline
|
||||||
*/
|
*/
|
||||||
val rootThreadEventId: String? = null,
|
val rootThreadEventId: String? = null,
|
||||||
|
/**
|
||||||
|
* If true Sender Info shown in room will get the latest data information (avatar + displayName)
|
||||||
|
*/
|
||||||
|
val useLiveSenderInfo: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,7 +131,7 @@ interface SharedSecretStorageService {
|
|||||||
|
|
||||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||||
|
|
||||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
suspend fun requestSecret(name: String, myOtherDeviceId: String)
|
||||||
|
|
||||||
data class KeyRef(
|
data class KeyRef(
|
||||||
val keyId: String?,
|
val keyId: String?,
|
||||||
|
@ -20,6 +20,6 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
sealed class LazyRoomSyncEphemeral {
|
sealed class LazyRoomSyncEphemeral {
|
||||||
data class Parsed(val _roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
|
data class Parsed(val roomSyncEphemeral: RoomSyncEphemeral) : LazyRoomSyncEphemeral()
|
||||||
object Stored : LazyRoomSyncEphemeral()
|
object Stored : LazyRoomSyncEphemeral()
|
||||||
}
|
}
|
||||||
|
@ -382,11 +382,16 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
return getWellknownTask.execute(
|
return getWellknownTask.execute(
|
||||||
GetWellknownTask.Params(
|
GetWellknownTask.Params(
|
||||||
domain = matrixId.getDomain(),
|
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,
|
override suspend fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
matrixId: String,
|
matrixId: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
@ -74,8 +74,8 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
|
|||||||
* Indicate if the homeserver support MSC3440 for threads
|
* Indicate if the homeserver support MSC3440 for threads
|
||||||
*/
|
*/
|
||||||
internal fun Versions.doesServerSupportThreads(): Boolean {
|
internal fun Versions.doesServerSupportThreads(): Boolean {
|
||||||
return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
|
// TODO Check for v1.3 or whichever spec version formally specifies MSC3440.
|
||||||
unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
|
return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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<CancelGossipRequestWorker.Params>(context, params, sessionManager, Params::class.java) {
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class Params(
|
|
||||||
override val sessionId: String,
|
|
||||||
val requestId: String,
|
|
||||||
val recipients: Map<String, List<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 {
|
|
||||||
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<Any>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
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.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.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
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.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.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.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
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.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
import org.matrix.android.sdk.api.session.crypto.model.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.MXEncryptEventContentResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
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.MXUsersDevicesMap
|
||||||
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.RoomKeyRequestBody
|
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.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
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.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.RoomKeyContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.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.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
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.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.megolm.MXMegolmEncryptionFactory
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
import org.matrix.android.sdk.internal.crypto.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.SetDeviceNameTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
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.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.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
|
|
||||||
private val crossSigningService: DefaultCrossSigningService,
|
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
|
// Actions
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||||
@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val eventDecryptor: EventDecryptor,
|
private val eventDecryptor: EventDecryptor,
|
||||||
|
private val verificationMessageProcessor: VerificationMessageProcessor,
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
private val liveEventManager: Lazy<StreamEventsManager>
|
||||||
) : CryptoService {
|
) : 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
|
// handle state events
|
||||||
if (event.isStateEvent()) {
|
if (event.isStateEvent()) {
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
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<Event>()
|
// val gossipingBuffer = mutableListOf<Event>()
|
||||||
|
|
||||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||||
setDeviceNameTask
|
setDeviceNameTask
|
||||||
@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
// Open the store
|
// Open the store
|
||||||
cryptoStore.open()
|
cryptoStore.open()
|
||||||
|
|
||||||
runCatching {
|
isStarting.set(false)
|
||||||
// if (isInitialSync) {
|
isStarted.set(true)
|
||||||
// // 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")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||||
incomingGossipingRequestManager.close()
|
incomingKeyRequestManager.close()
|
||||||
|
outgoingKeyRequestManager.close()
|
||||||
olmDevice.release()
|
olmDevice.release()
|
||||||
cryptoStore.close()
|
cryptoStore.close()
|
||||||
}
|
}
|
||||||
@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tryOrNull {
|
// Process pending key requests
|
||||||
gossipingBuffer.toList().let {
|
try {
|
||||||
cryptoStore.saveGossipingEvents(it)
|
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?)
|
// (for now at least. Maybe we should alert the user somehow?)
|
||||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||||
|
|
||||||
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
|
if (existingAlgorithm == algorithm) {
|
||||||
// ignore
|
// ignore
|
||||||
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
||||||
return false
|
return false
|
||||||
@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||||
gossipingBuffer.add(event)
|
|
||||||
// Keys are imported directly, not waiting for end of sync
|
// Keys are imported directly, not waiting for end of sync
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
}
|
}
|
||||||
EventType.REQUEST_SECRET,
|
EventType.REQUEST_SECRET -> {
|
||||||
|
secretShareManager.handleSecretRequest(event)
|
||||||
|
}
|
||||||
EventType.ROOM_KEY_REQUEST -> {
|
EventType.ROOM_KEY_REQUEST -> {
|
||||||
// save audit trail
|
event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
|
||||||
gossipingBuffer.add(event)
|
// We'll always get these because we send room key requests to
|
||||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
// '*' (ie. 'all devices') which includes the sending device,
|
||||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
// 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 -> {
|
EventType.SEND_SECRET -> {
|
||||||
gossipingBuffer.add(event)
|
|
||||||
onSecretSendReceived(event)
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
EventType.ROOM_KEY_WITHHELD -> {
|
EventType.ROOM_KEY_WITHHELD -> {
|
||||||
@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
}
|
}
|
||||||
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
val senderId = event.senderId ?: return Unit.also {
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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) {
|
private suspend fun onSecretSendReceived(event: Event) {
|
||||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
|
||||||
if (!event.isEncrypted()) {
|
handleSDKLevelGossip(secretName, secretValue)
|
||||||
// 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<SecretSendEventContent>() ?: 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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if handled by SDK, otherwise should be sent to application layer
|
* 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) {
|
return when (secretName) {
|
||||||
MASTER_KEY_SSSS_NAME -> {
|
MASTER_KEY_SSSS_NAME -> {
|
||||||
crossSigningService.onSecretMSKGossip(secretValue)
|
crossSigningService.onSecretMSKGossip(secretValue)
|
||||||
@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
|
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.
|
* Tells whether the client should ever send encrypted messages to unverified devices.
|
||||||
* The default value is false.
|
* The default value is false.
|
||||||
@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
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.
|
* Re request the encryption keys required to decrypt an event.
|
||||||
*
|
*
|
||||||
* @param event the event to decrypt again.
|
* @param event the event to decrypt again.
|
||||||
*/
|
*/
|
||||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
outgoingKeyRequestManager.requestKeyForEvent(event, true)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestRoomKeyForEvent(event: Event) {
|
override fun requestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
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
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
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<String, Any>("type" to EventType.DUMMY)
|
|
||||||
//
|
|
||||||
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
// 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
|
* Provides the list of unknown devices
|
||||||
*
|
*
|
||||||
@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
return "DefaultCryptoService of $userId ($deviceId)"
|
return "DefaultCryptoService of $userId ($deviceId)"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
|
||||||
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||||
return cryptoStore.getIncomingRoomKeyRequests()
|
return cryptoStore.getGossipingEvents()
|
||||||
|
.mapNotNull {
|
||||||
|
IncomingRoomKeyRequest.fromEvent(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||||
|
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<PagedList<AuditTrail>> {
|
||||||
return cryptoStore.getGossipingEventsTrail()
|
return cryptoStore.getGossipingEventsTrail()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEvents(): List<Event> {
|
override fun getGossipingEvents(): List<AuditTrail> {
|
||||||
return cryptoStore.getGossipingEvents()
|
return cryptoStore.getGossipingEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
||||||
callback.onFailure(failure)
|
// we probably shouldn't block sending on that (but questionable)
|
||||||
return@launch
|
// but some members won't be able to decrypt
|
||||||
}
|
}
|
||||||
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
val userIds = getRoomUserIds(roomId)
|
||||||
|
@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
Timber.v("## CRYPTO | downloadKeys() : starts")
|
||||||
val t0 = clock.epochMillis()
|
val t0 = clock.epochMillis()
|
||||||
val result = doKeyDownloadForUsers(downloadUsers)
|
try {
|
||||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
val result = doKeyDownloadForUsers(downloadUsers)
|
||||||
result.also {
|
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
||||||
it.addEntriesFromMap(stored)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<IncomingShareRequestCommon>()
|
|
||||||
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
|
|
||||||
|
|
||||||
// the listeners
|
|
||||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
|
||||||
|
|
||||||
init {
|
|
||||||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
executor.shutdownNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recently verified devices (map of deviceId and timestamp)
|
|
||||||
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<GossipingDefaultContent>()
|
|
||||||
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<IncomingRequestCancellation>? = 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<SendGossipWorker>(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<SendGossipWorker>(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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<ValidMegolmRequestBody>()
|
||||||
|
|
||||||
|
// the listeners
|
||||||
|
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = 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<Any>().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<Any>()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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?
|
|
||||||
}
|
|
@ -43,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession
|
|||||||
import org.matrix.olm.OlmSession
|
import org.matrix.olm.OlmSession
|
||||||
import org.matrix.olm.OlmUtility
|
import org.matrix.olm.OlmUtility
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.URLEncoder
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
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).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()
|
val olmMessage = OlmMessage()
|
||||||
olmMessage.mCipherText = ciphertext
|
olmMessage.mCipherText = ciphertext
|
||||||
olmMessage.mType = messageType.toLong()
|
olmMessage.mType = messageType.toLong()
|
||||||
@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
|
|
||||||
// Inbound group session
|
// 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.
|
* Add an inbound group session to the session store.
|
||||||
*
|
*
|
||||||
@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
senderKey: String,
|
senderKey: String,
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
forwardingCurve25519KeyChain: List<String>,
|
||||||
keysClaimed: Map<String, String>,
|
keysClaimed: Map<String, String>,
|
||||||
exportFormat: Boolean): Boolean {
|
exportFormat: Boolean): AddSessionResult {
|
||||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
if (existingSession != null) {
|
if (existingSession != null) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||||
try {
|
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?
|
// This is quite unexpected, could throw if native was released?
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
||||||
if (null == candidateOlmInboundSession) {
|
if (null == candidateOlmInboundSession) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateSession.senderKey = senderKey
|
candidateSession.senderKey = senderKey
|
||||||
@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
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)) {
|
if (timelineSet.contains(messageIndexKey)) {
|
||||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
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)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<String, List<String>>
|
|
||||||
val requestId: String
|
|
||||||
val state: OutgoingGossipingRequestState
|
|
||||||
// transaction id for the cancellation, if any
|
|
||||||
// var cancellationTxnId: String?
|
|
||||||
}
|
|
@ -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<String, List<String>>) {
|
|
||||||
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<String, List<String>>) {
|
|
||||||
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<SendGossipRequestWorker>(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<CancelGossipRequestWorker>(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<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
|
||||||
gossipingWorkManager.postWork(reSendWorkRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Pair<String, String>>()
|
||||||
|
|
||||||
|
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<Map<String, List<String>>, RoomKeyRequestBody>? {
|
||||||
|
val sender = event.senderId ?: return null
|
||||||
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: 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<EncryptedEventContent>() ?: 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<String, List<String>>, 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<RoomKeyWithHeldContent>()?.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<String, List<String>>, 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<Any>()
|
||||||
|
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<Any>()
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, List<String>>,
|
|
||||||
// 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
|
|
||||||
}
|
|
@ -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<DefaultKeysBackupService>,
|
||||||
|
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<Info, LastTry>()
|
||||||
|
|
||||||
|
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<ImportRoomKeysResult> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -79,7 +79,7 @@ internal class RoomDecryptorProvider @Inject constructor(
|
|||||||
newSessionListeners.toList().forEach {
|
newSessionListeners.toList().forEach {
|
||||||
try {
|
try {
|
||||||
it.onNewSession(roomId, senderKey, sessionId)
|
it.onNewSession(roomId, senderKey, sessionId)
|
||||||
} catch (e: Throwable) {
|
} catch (ignore: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,21 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
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.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 org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@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.
|
// MXEncrypting instance for each room.
|
||||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
||||||
@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
|
|||||||
|
|
||||||
fun get(roomId: String): IMXEncrypting? {
|
fun get(roomId: String): IMXEncrypting? {
|
||||||
return synchronized(roomEncryptors) {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<String, Long>()
|
||||||
|
private val verifMutex = Mutex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secrets are exchanged as part of interactive verification,
|
||||||
|
* so we can just store in memory.
|
||||||
|
*/
|
||||||
|
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
|
||||||
|
|
||||||
|
// the listeners
|
||||||
|
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = 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<SecretShareRequest>()
|
||||||
|
?: 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<Any>()
|
||||||
|
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<Any>()
|
||||||
|
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<SecretSendEventContent>() ?: 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<SendGossipRequestWorker.Params>(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<Any>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<SendGossipWorker.Params>(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<Any>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,12 +17,13 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.actions
|
package org.matrix.android.sdk.internal.crypto.actions
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
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.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.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.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
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.RoomDecryptorProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MegolmSessionDataImporter @Inject constructor(
|
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val clock: Clock,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||||||
if (null != decrypting) {
|
if (null != decrypting) {
|
||||||
try {
|
try {
|
||||||
val sessionId = megolmSessionData.sessionId
|
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++
|
totalNumbersOfImportedKeys++
|
||||||
|
|
||||||
// cancel any outstanding room key requests for this session
|
// 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
|
// Have another go at decrypting events sent with this session
|
||||||
when (decrypting) {
|
when (decrypting) {
|
||||||
@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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()
|
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)
|
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,6 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
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.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.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
@ -44,23 +42,4 @@ internal interface IMXDecrypting {
|
|||||||
* @param event the key event.
|
* @param event the key event.
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
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)
|
|
||||||
}
|
}
|
||||||
|
@ -17,51 +17,32 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
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.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
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.NewSessionListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
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.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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
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.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.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.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
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.keysbackup.DefaultKeysBackupService
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
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 org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
internal class MXMegolmDecryption(private val userId: String,
|
internal class MXMegolmDecryption(
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val liveEventManager: Lazy<StreamEventsManager>
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
) : IMXDecrypting {
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
|
||||||
) : IMXDecrypting, IMXWithHeldExtension {
|
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
var newSessionListener: NewSessionListener? = null
|
||||||
|
|
||||||
@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
// If cross signing is enabled, we don't send request until the keys are trusted
|
return decryptEvent(event, timeline, true)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
if (throwable is MXCryptoError.OlmError) {
|
if (throwable is MXCryptoError.OlmError) {
|
||||||
// TODO Check the value of .message
|
// TODO Check the value of .message
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||||
// addEventToPendingList(event, timeline)
|
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
||||||
// The session might has been partially withheld (and only pass ratcheted)
|
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
requestKeysForEvent(event)
|
||||||
|
}
|
||||||
|
// Check if partially withheld
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
if (withHeldInfo != null) {
|
if (withHeldInfo != null) {
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, true)
|
|
||||||
}
|
|
||||||
// Encapsulate as withHeld exception
|
// Encapsulate as withHeld exception
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||||
"UNKNOWN_MESSAGE_INDEX",
|
"UNKNOWN_MESSAGE_INDEX",
|
||||||
@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (throwable is MXCryptoError.Base) {
|
if (throwable is MXCryptoError.Base) {
|
||||||
if (
|
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||||
/** if the session is unknown*/
|
// Check if it was withheld by sender to enrich error code
|
||||||
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
|
||||||
) {
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
if (withHeldInfo != null) {
|
if (withHeldInfo != null) {
|
||||||
if (requestKeysOnFail) {
|
if (requestKeysOnFail) {
|
||||||
requestKeysForEvent(event, true)
|
requestKeysForEvent(event)
|
||||||
}
|
}
|
||||||
// Encapsulate as withHeld exception
|
// Encapsulate as withHeld exception
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
withHeldInfo.code?.value ?: "",
|
withHeldInfo.code?.value ?: "",
|
||||||
withHeldInfo.reason
|
withHeldInfo.reason)
|
||||||
)
|
}
|
||||||
} else {
|
|
||||||
// This is un-used in Matrix Android SDK2, not sure if needed
|
if (requestKeysOnFail) {
|
||||||
// addEventToPendingList(event, timeline)
|
requestKeysForEvent(event)
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
private fun requestKeysForEvent(event: Event) {
|
||||||
val sender = event.senderId ?: return
|
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * 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<EncryptedEventContent>() ?: 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.
|
* Handle a key event.
|
||||||
*
|
*
|
||||||
@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
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}")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||||
?: return
|
?: return
|
||||||
@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
||||||
val added = olmDevice.addInboundGroupSession(
|
val addSessionResult = olmDevice.addInboundGroupSession(
|
||||||
roomKeyContent.sessionId,
|
roomKeyContent.sessionId,
|
||||||
roomKeyContent.sessionKey,
|
roomKeyContent.sessionKey,
|
||||||
roomKeyContent.roomId,
|
roomKeyContent.roomId,
|
||||||
@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||||||
exportFormat
|
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()
|
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)
|
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")
|
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
||||||
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
|
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<Any>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,46 +17,24 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
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.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
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.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 org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
@UserId private val userId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val eventsManager: Lazy<StreamEventsManager>
|
private val eventsManager: Lazy<StreamEventsManager>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(): MXMegolmDecryption {
|
fun create(): MXMegolmDecryption {
|
||||||
return MXMegolmDecryption(
|
return MXMegolmDecryption(
|
||||||
userId,
|
|
||||||
olmDevice,
|
olmDevice,
|
||||||
deviceListManager,
|
outgoingKeyRequestManager,
|
||||||
outgoingGossipingRequestManager,
|
|
||||||
messageEncrypter,
|
|
||||||
ensureOlmSessionsForDevicesAction,
|
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
sendToDeviceTask,
|
eventsManager)
|
||||||
coroutineDispatchers,
|
|
||||||
cryptoCoroutineScope,
|
|
||||||
eventsManager
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
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.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.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
@ -80,7 +79,7 @@ internal class MXMegolmEncryption(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default rotation periods
|
// Default rotation periods
|
||||||
// TODO: Make it configurable via parameters
|
// TODO Make it configurable via parameters
|
||||||
// Session rotation periods
|
// Session rotation periods
|
||||||
private var sessionRotationPeriodMsgs: Int = 100
|
private var sessionRotationPeriodMsgs: Int = 100
|
||||||
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
|
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
|
// 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
|
// share with), because we don't want to try to claim a one-time-key
|
||||||
// for dead devices on every message.
|
// for dead devices on every message.
|
||||||
val gossipingEventBuffer = arrayListOf<Event>()
|
for ((_, devicesToShareWith) in devicesByUser) {
|
||||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
|
||||||
for (deviceInfo in devicesToShareWith) {
|
for (deviceInfo in devicesToShareWith) {
|
||||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||||
gossipingEventBuffer.add(
|
// XXX is it needed to add it to the audit trail?
|
||||||
Event(
|
// For now decided that no, we are more interested by forward trail
|
||||||
type = EventType.ROOM_KEY,
|
|
||||||
senderId = myUserId,
|
|
||||||
content = submap.apply {
|
|
||||||
this["session_key"] = ""
|
|
||||||
// we add a fake key for trail
|
|
||||||
this["_dest"] = "$userId|${deviceInfo.deviceId}"
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
|
|
||||||
|
|
||||||
if (haveTargets) {
|
if (haveTargets) {
|
||||||
t0 = clock.epochMillis()
|
t0 = clock.epochMillis()
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
||||||
@ -346,7 +334,8 @@ internal class MXMegolmEncryption(
|
|||||||
senderKey = senderKey,
|
senderKey = senderKey,
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
codeString = code.value
|
codeString = code.value,
|
||||||
|
fromDevice = myDeviceId
|
||||||
)
|
)
|
||||||
val params = SendToDeviceTask.Params(
|
val params = SendToDeviceTask.Params(
|
||||||
EventType.ROOM_KEY_WITHHELD,
|
EventType.ROOM_KEY_WITHHELD,
|
||||||
|
@ -265,8 +265,4 @@ internal class MXOlmDecryption(
|
|||||||
|
|
||||||
return res["payload"]
|
return res["payload"]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ internal class MXOlmEncryption(
|
|||||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
|
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
|
||||||
// pick the list of recipients based on the membership list.
|
// 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)
|
ensureSession(userIds)
|
||||||
val deviceInfos = ArrayList<CryptoDeviceInfo>()
|
val deviceInfos = ArrayList<CryptoDeviceInfo>()
|
||||||
for (userId in userIds) {
|
for (userId in userIds) {
|
||||||
|
@ -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<String, String> {
|
||||||
|
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<String, Map<String, String>>) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -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.Optional
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
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.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
import org.matrix.android.sdk.internal.crypto.tasks.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.util.logLimit
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||||
import org.matrix.olm.OlmPkSigning
|
import org.matrix.olm.OlmPkSigning
|
||||||
import org.matrix.olm.OlmUtility
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -70,19 +70,14 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
|
private val crossSigningOlm: CrossSigningOlm,
|
||||||
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
||||||
) : CrossSigningService,
|
) : CrossSigningService,
|
||||||
DeviceListManager.UserDevicesUpdateListener {
|
DeviceListManager.UserDevicesUpdateListener {
|
||||||
|
|
||||||
private var olmUtility: OlmUtility? = null
|
|
||||||
|
|
||||||
private var masterPkSigning: OlmPkSigning? = null
|
|
||||||
private var userPkSigning: OlmPkSigning? = null
|
|
||||||
private var selfSigningPkSigning: OlmPkSigning? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
try {
|
||||||
olmUtility = OlmUtility()
|
|
||||||
|
|
||||||
// Try to get stored keys if they exist
|
// Try to get stored keys if they exist
|
||||||
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
|
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
|
||||||
@ -95,7 +90,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
?.let { privateKeySeed ->
|
?.let { privateKeySeed ->
|
||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
||||||
masterPkSigning = pkSigning
|
crossSigningOlm.masterPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
Timber.i("## CrossSigning - Loading master key success")
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## CrossSigning - Public master key does not match the private key")
|
Timber.w("## CrossSigning - Public master key does not match the private key")
|
||||||
@ -108,7 +103,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
?.let { privateKeySeed ->
|
?.let { privateKeySeed ->
|
||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
||||||
userPkSigning = pkSigning
|
crossSigningOlm.userPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading User Signing key success")
|
Timber.i("## CrossSigning - Loading User Signing key success")
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## CrossSigning - Public User key does not match the private key")
|
Timber.w("## CrossSigning - Public User key does not match the private key")
|
||||||
@ -121,7 +116,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
?.let { privateKeySeed ->
|
?.let { privateKeySeed ->
|
||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||||
selfSigningPkSigning = pkSigning
|
crossSigningOlm.selfSigningPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading Self Signing key success")
|
Timber.i("## CrossSigning - Loading Self Signing key success")
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
|
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
|
||||||
@ -143,8 +138,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
olmUtility?.releaseUtility()
|
crossSigningOlm.release()
|
||||||
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
|
|
||||||
deviceListManager.removeListener(this)
|
deviceListManager.removeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,9 +171,9 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
||||||
setUserKeysAsTrusted(userId, true)
|
setUserKeysAsTrusted(userId, true)
|
||||||
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
||||||
masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
||||||
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
||||||
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
||||||
|
|
||||||
callback.onSuccess(Unit)
|
callback.onSuccess(Unit)
|
||||||
}
|
}
|
||||||
@ -198,8 +192,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
||||||
masterPkSigning?.releaseSigning()
|
crossSigningOlm.masterPkSigning?.releaseSigning()
|
||||||
masterPkSigning = pkSigning
|
crossSigningOlm.masterPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading MSK success")
|
Timber.i("## CrossSigning - Loading MSK success")
|
||||||
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
|
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
|
||||||
return
|
return
|
||||||
@ -225,8 +219,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||||
selfSigningPkSigning?.releaseSigning()
|
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
|
||||||
selfSigningPkSigning = pkSigning
|
crossSigningOlm.selfSigningPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading SSK success")
|
Timber.i("## CrossSigning - Loading SSK success")
|
||||||
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
|
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
|
||||||
return
|
return
|
||||||
@ -252,8 +246,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
||||||
userPkSigning?.releaseSigning()
|
crossSigningOlm.userPkSigning?.releaseSigning()
|
||||||
userPkSigning = pkSigning
|
crossSigningOlm.userPkSigning = pkSigning
|
||||||
Timber.i("## CrossSigning - Loading USK success")
|
Timber.i("## CrossSigning - Loading USK success")
|
||||||
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
|
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
|
||||||
return
|
return
|
||||||
@ -282,8 +276,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
||||||
masterPkSigning?.releaseSigning()
|
crossSigningOlm.masterPkSigning?.releaseSigning()
|
||||||
masterPkSigning = pkSigning
|
crossSigningOlm.masterPkSigning = pkSigning
|
||||||
masterKeyIsTrusted = true
|
masterKeyIsTrusted = true
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
Timber.i("## CrossSigning - Loading master key success")
|
||||||
} else {
|
} else {
|
||||||
@ -299,8 +293,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
||||||
userPkSigning?.releaseSigning()
|
crossSigningOlm.userPkSigning?.releaseSigning()
|
||||||
userPkSigning = pkSigning
|
crossSigningOlm.userPkSigning = pkSigning
|
||||||
userKeyIsTrusted = true
|
userKeyIsTrusted = true
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
Timber.i("## CrossSigning - Loading master key success")
|
||||||
} else {
|
} else {
|
||||||
@ -316,8 +310,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val pkSigning = OlmPkSigning()
|
val pkSigning = OlmPkSigning()
|
||||||
try {
|
try {
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
||||||
selfSigningPkSigning?.releaseSigning()
|
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
|
||||||
selfSigningPkSigning = pkSigning
|
crossSigningOlm.selfSigningPkSigning = pkSigning
|
||||||
selfSignedKeyIsTrusted = true
|
selfSignedKeyIsTrusted = true
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
Timber.i("## CrossSigning - Loading master key success")
|
||||||
} else {
|
} else {
|
||||||
@ -405,7 +399,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
|
|
||||||
// Check that Alice USK signature of Bob MSK is valid
|
// Check that Alice USK signature of Bob MSK is valid
|
||||||
try {
|
try {
|
||||||
olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
||||||
|
masterKeySignaturesMadeByMyUserKey,
|
||||||
|
myUserKey.unpaddedBase64PublicKey,
|
||||||
|
otherMasterKey.canonicalSignable()
|
||||||
|
)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
|
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
|
||||||
}
|
}
|
||||||
@ -459,7 +457,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
if (potentialDevice != null && potentialDevice.isVerified) {
|
||||||
// Check signature validity?
|
// Check signature validity?
|
||||||
try {
|
try {
|
||||||
olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
|
||||||
isMaterKeyTrusted = true
|
isMaterKeyTrusted = true
|
||||||
return@forEach
|
return@forEach
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
@ -488,7 +486,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
// Check that Alice USK signature of Alice MSK is valid
|
||||||
try {
|
try {
|
||||||
olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
||||||
|
userKeySignaturesMadeByMyMasterKey,
|
||||||
|
myMasterKey.unpaddedBase64PublicKey,
|
||||||
|
myUserKey.canonicalSignable()
|
||||||
|
)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
|
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
|
||||||
}
|
}
|
||||||
@ -507,7 +509,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
// Check that Alice USK signature of Alice MSK is valid
|
||||||
try {
|
try {
|
||||||
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
||||||
|
ssKeySignaturesMadeByMyMasterKey,
|
||||||
|
myMasterKey.unpaddedBase64PublicKey,
|
||||||
|
mySSKey.canonicalSignable()
|
||||||
|
)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
|
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
|
||||||
}
|
}
|
||||||
@ -560,7 +566,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
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"))
|
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@ -569,7 +575,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
val newSignature = JsonCanonicalizer.getCanonicalJson(
|
val newSignature = JsonCanonicalizer.getCanonicalJson(
|
||||||
Map::class.java,
|
Map::class.java,
|
||||||
otherMasterKeys.signalableJSONDictionary()
|
otherMasterKeys.signalableJSONDictionary()
|
||||||
).let { userPkSigning?.sign(it) }
|
).let { crossSigningOlm.userPkSigning?.sign(it) }
|
||||||
|
|
||||||
if (newSignature == null) {
|
if (newSignature == null) {
|
||||||
// race??
|
// race??
|
||||||
@ -616,13 +622,13 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
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"))
|
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign with self signing
|
// Sign with self signing
|
||||||
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
|
val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable())
|
||||||
|
|
||||||
if (newSignature == null) {
|
if (newSignature == null) {
|
||||||
// race??
|
// race??
|
||||||
@ -695,7 +701,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
|
|
||||||
// Check bob's device is signed by bob's SSK
|
// Check bob's device is signed by bob's SSK
|
||||||
try {
|
try {
|
||||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
||||||
|
otherSSKSignature,
|
||||||
|
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
|
||||||
|
otherDevice.canonicalSignable()
|
||||||
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
|
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
|
// Check bob's device is signed by bob's SSK
|
||||||
try {
|
try {
|
||||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
||||||
|
otherSSKSignature,
|
||||||
|
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
|
||||||
|
otherDevice.canonicalSignable()
|
||||||
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
|
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?
|
// If it's me, recheck trust of all users and devices?
|
||||||
val users = ArrayList<String>()
|
val users = ArrayList<String>()
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
if (otherUserId == userId && currentTrust != trusted) {
|
||||||
// reRequestAllPendingRoomKeyRequest()
|
// notify key requester
|
||||||
|
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
||||||
cryptoStore.updateUsersTrust {
|
cryptoStore.updateUsersTrust {
|
||||||
users.add(it)
|
users.add(it)
|
||||||
checkUserTrust(it).isVerified()
|
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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -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.MegolmSessionData
|
||||||
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
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.SignalableMegolmBackupAuthData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.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.model.rest.UpdateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
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.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.GetKeysBackupLastVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
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.GetRoomSessionDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
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.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.StoreSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
@ -107,21 +103,17 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val objectSigner: ObjectSigner,
|
private val objectSigner: ObjectSigner,
|
||||||
|
private val crossSigningOlm: CrossSigningOlm,
|
||||||
// Actions
|
// Actions
|
||||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||||
// Tasks
|
// Tasks
|
||||||
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
||||||
private val deleteBackupTask: DeleteBackupTask,
|
private val deleteBackupTask: DeleteBackupTask,
|
||||||
private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask,
|
|
||||||
private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask,
|
|
||||||
private val deleteSessionDataTask: DeleteSessionsDataTask,
|
|
||||||
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
||||||
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
||||||
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
||||||
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
||||||
private val getSessionsDataTask: GetSessionsDataTask,
|
private val getSessionsDataTask: GetSessionsDataTask,
|
||||||
private val storeRoomSessionDataTask: StoreRoomSessionDataTask,
|
|
||||||
private val storeSessionsDataTask: StoreRoomSessionsDataTask,
|
|
||||||
private val storeSessionDataTask: StoreSessionsDataTask,
|
private val storeSessionDataTask: StoreSessionsDataTask,
|
||||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||||
// Task executor
|
// Task executor
|
||||||
@ -168,58 +160,76 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
override fun prepareKeysBackupVersion(password: String?,
|
override fun prepareKeysBackupVersion(password: String?,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
try {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
val olmPkDecryption = OlmPkDecryption()
|
||||||
val olmPkDecryption = OlmPkDecryption()
|
val signalableMegolmBackupAuthData = if (password != null) {
|
||||||
val signalableMegolmBackupAuthData = if (password != null) {
|
// Generate a private key from the password
|
||||||
// Generate a private key from the password
|
val backgroundProgressListener = if (progressListener == null) {
|
||||||
val backgroundProgressListener = if (progressListener == null) {
|
null
|
||||||
null
|
} else {
|
||||||
} else {
|
object : ProgressListener {
|
||||||
object : ProgressListener {
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
override fun onProgress(progress: Int, total: Int) {
|
uiHandler.post {
|
||||||
uiHandler.post {
|
try {
|
||||||
try {
|
progressListener.onProgress(progress, total)
|
||||||
progressListener.onProgress(progress, total)
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||||
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 generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
SignalableMegolmBackupAuthData(
|
||||||
|
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||||
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
privateKeySalt = generatePrivateKeyResult.salt,
|
||||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
|
||||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
|
||||||
signatures = objectSigner.signObject(canonicalJson)
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
MegolmBackupCreationInfo(
|
val publicKey = olmPkDecryption.generateKey()
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
SignalableMegolmBackupAuthData(
|
||||||
authData = signedMegolmBackupAuthData,
|
publicKey = publicKey
|
||||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
|
||||||
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||||
|
|
||||||
|
val signatures = mutableMapOf<String, MutableMap<String, String>>()
|
||||||
|
|
||||||
|
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<Unit>?) {
|
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
// If we're currently backing up to this backup... stop.
|
||||||
// If we're currently backing up to this backup... stop.
|
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
||||||
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
||||||
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
resetKeysBackupData()
|
||||||
resetKeysBackupData()
|
keysBackupVersion = null
|
||||||
keysBackupVersion = null
|
keysBackupStateManager.state = KeysBackupState.Unknown
|
||||||
keysBackupStateManager.state = KeysBackupState.Unknown
|
}
|
||||||
}
|
|
||||||
|
|
||||||
deleteBackupTask
|
deleteBackupTask
|
||||||
.configureWith(DeleteBackupTask.Params(version)) {
|
.configureWith(DeleteBackupTask.Params(version)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
private fun eventuallyRestartBackup() {
|
private fun eventuallyRestartBackup() {
|
||||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||||
if (state == KeysBackupState.Unknown) {
|
if (state == KeysBackupState.Unknown) {
|
||||||
checkAndStartKeysBackup()
|
checkAndStartKeysBackup()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
eventuallyRestartBackup()
|
|
||||||
|
|
||||||
uiHandler.post { callback?.onSuccess(Unit) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
eventuallyRestartBackup()
|
|
||||||
|
|
||||||
uiHandler.post { callback?.onFailure(failure) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?,
|
override fun backupAllGroupSessions(progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<Unit>?) {
|
callback: MatrixCallback<Unit>?) {
|
||||||
|
if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) {
|
||||||
|
callback?.onFailure(Throwable("Backup not enabled"))
|
||||||
|
return
|
||||||
|
}
|
||||||
// Get a status right now
|
// Get a status right now
|
||||||
getBackupProgress(object : ProgressListener {
|
getBackupProgress(object : ProgressListener {
|
||||||
override fun onProgress(progress: Int, total: Int) {
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
@ -427,18 +439,41 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
for ((keyId, mySignature) in mySigs) {
|
for ((keyId, mySignature) in mySigs) {
|
||||||
// XXX: is this how we're supposed to get the device id?
|
// XXX: is this how we're supposed to get the device id?
|
||||||
var deviceId: String? = null
|
var deviceOrCrossSigningKeyId: String? = null
|
||||||
val components = keyId.split(":")
|
val components = keyId.split(":")
|
||||||
if (components.size == 2) {
|
if (components.size == 2) {
|
||||||
deviceId = components[1]
|
deviceOrCrossSigningKeyId = components[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceId != null) {
|
// Let's check if it's my master key
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
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
|
var isSignatureValid = false
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId")
|
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId")
|
||||||
} else {
|
} else {
|
||||||
val fingerprint = device.fingerprint()
|
val fingerprint = device.fingerprint()
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
@ -455,8 +490,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val signature = KeysBackupVersionTrustSignature(
|
val signature = KeysBackupVersionTrustSignature.DeviceSignature(
|
||||||
deviceId = deviceId,
|
deviceId = deviceOrCrossSigningKeyId,
|
||||||
device = device,
|
device = device,
|
||||||
valid = isSignatureValid,
|
valid = isSignatureValid,
|
||||||
)
|
)
|
||||||
@ -480,10 +515,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
if (authData == null) {
|
if (authData == null) {
|
||||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
||||||
// Get current signatures, or create an empty set
|
// Get current signatures, or create an empty set
|
||||||
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
|
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
|
||||||
@ -536,11 +572,15 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||||
|
|
||||||
callback.onSuccess(data)
|
uiHandler.post {
|
||||||
|
callback.onSuccess(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
callback.onFailure(failure)
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -554,15 +594,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val isValid = withContext(coroutineDispatchers.crypto) {
|
val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||||
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
||||||
}
|
}
|
||||||
@ -574,15 +613,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||||
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recoveryKey == null) {
|
if (recoveryKey == null) {
|
||||||
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check trust using the recovery key
|
// Check trust using the recovery key
|
||||||
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
||||||
@ -593,30 +631,28 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
override fun onSecretKeyGossip(secret: String) {
|
override fun onSecretKeyGossip(secret: String) {
|
||||||
Timber.i("## CrossSigning - onSecretKeyGossip")
|
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
try {
|
try {
|
||||||
when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) {
|
val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult()
|
||||||
KeysBackupLastVersionResult.NoKeysBackup -> {
|
?: return@launch Unit.also {
|
||||||
Timber.d("No keys backup found")
|
Timber.d("Failed to get backup last version")
|
||||||
}
|
|
||||||
is KeysBackupLastVersionResult.KeysBackup -> {
|
|
||||||
val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult
|
|
||||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
|
||||||
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
|
||||||
awaitCallback<Unit> {
|
|
||||||
trustKeysBackupVersion(keysBackupVersion, true, it)
|
|
||||||
}
|
|
||||||
val importResult = awaitCallback<ImportRoomKeysResult> {
|
|
||||||
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 recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||||
|
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
trustKeysBackupVersion(keysBackupVersion, true, it)
|
||||||
}
|
}
|
||||||
|
// we don't want to start immediately downloading all as it can take very long
|
||||||
|
|
||||||
|
// val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||||
|
// 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) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
||||||
@ -679,9 +715,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val decryption = withContext(coroutineDispatchers.crypto) {
|
val decryption = withContext(coroutineDispatchers.computation) {
|
||||||
// Check if the recovery is valid before going any further
|
// Check if the recovery is valid before going any further
|
||||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||||
@ -754,7 +790,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||||
|
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<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val progressListener = if (stepProgressListener != null) {
|
val progressListener = if (stepProgressListener != null) {
|
||||||
object : ProgressListener {
|
object : ProgressListener {
|
||||||
@ -791,7 +839,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||||
|
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) {
|
} else if (roomId != null) {
|
||||||
// Get all keys for the room
|
// 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
|
// Convert to KeysBackupData
|
||||||
KeysBackupData(mutableMapOf(roomId to data))
|
KeysBackupData(mutableMapOf(roomId to data))
|
||||||
} else {
|
} else {
|
||||||
// Get all keys
|
// 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")
|
Timber.v("backupKeys: Invalid configuration")
|
||||||
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
|
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
|
||||||
resetBackupAllGroupSessionsListeners()
|
resetBackupAllGroupSessionsListeners()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
|||||||
@SessionScope
|
@SessionScope
|
||||||
internal class WarnOnUnknownDeviceRepository @Inject constructor() {
|
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.
|
// Warn the user if some new devices are detected while encrypting a message.
|
||||||
private var warnOnUnknownDevices = false
|
private var warnOnUnknownDevices = false
|
||||||
|
|
||||||
|
@ -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.session.securestorage.SsssPassphrase
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
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.keysbackup.generatePrivateKeyWithPassword
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||||
@ -57,7 +57,7 @@ import kotlin.experimental.and
|
|||||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val accountDataService: SessionAccountDataService,
|
private val accountDataService: SessionAccountDataService,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val secretShareManager: SecretShareManager,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : SharedSecretStorageService {
|
) : SharedSecretStorageService {
|
||||||
@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||||||
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
secretShareManager.requestSecretTo(myOtherDeviceId, name)
|
||||||
name,
|
|
||||||
mapOf(userId to listOf(myOtherDeviceId))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
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.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
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.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
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.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.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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.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.OlmInboundGroupSessionWrapper2
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
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.model.OutboundGroupSessionWrapper
|
||||||
@ -82,6 +81,15 @@ internal interface IMXCryptoStore {
|
|||||||
*/
|
*/
|
||||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
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.
|
* 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<String, Int>
|
fun getDeviceTrackingStatuses(): Map<String, Int>
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the pending IncomingRoomKeyRequest requests
|
|
||||||
*/
|
|
||||||
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
|
||||||
|
|
||||||
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
|
||||||
|
|
||||||
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
|
||||||
|
|
||||||
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
|
|
||||||
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if the store contains data for the passed account.
|
* Indicate if the store contains data for the passed account.
|
||||||
*
|
*
|
||||||
@ -377,7 +373,9 @@ internal interface IMXCryptoStore {
|
|||||||
* @param requestBody the request body
|
* @param requestBody the request body
|
||||||
* @return an OutgoingRoomKeyRequest instance or null
|
* @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<OutgoingKeyRequest>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
* 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
|
* @param request the request
|
||||||
* @return either the same instance as passed in, or the existing one.
|
* @return either the same instance as passed in, or the existing one.
|
||||||
*/
|
*/
|
||||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
|
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, 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<String, List<String>>): 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<Event>)
|
fun saveWithheldAuditTrail(
|
||||||
|
roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
code: WithHeldCode,
|
||||||
|
userId: String,
|
||||||
|
deviceId: String
|
||||||
|
)
|
||||||
|
|
||||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
fun saveForwardKeyAuditTrail(
|
||||||
updateGossipingRequestState(
|
roomId: String,
|
||||||
requestUserId = request.userId,
|
sessionId: String,
|
||||||
requestDeviceId = request.deviceId,
|
senderKey: String,
|
||||||
requestId = request.requestId,
|
algorithm: String,
|
||||||
state = state
|
userId: String,
|
||||||
)
|
deviceId: String,
|
||||||
}
|
chainIndex: Long?
|
||||||
|
)
|
||||||
|
|
||||||
fun updateGossipingRequestState(requestUserId: String?,
|
fun saveIncomingForwardKeyAuditTrail(
|
||||||
requestDeviceId: String?,
|
roomId: String,
|
||||||
requestId: String?,
|
sessionId: String,
|
||||||
state: GossipingRequestState)
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
/**
|
userId: String,
|
||||||
* Search an IncomingRoomKeyRequest
|
deviceId: String,
|
||||||
*
|
chainIndex: Long?
|
||||||
* @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 addNewSessionListener(listener: NewSessionListener)
|
fun addNewSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
@ -477,17 +495,15 @@ internal interface IMXCryptoStore {
|
|||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||||
// Dev tools
|
// Dev tools
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getGossipingEvents(): List<AuditTrail>
|
||||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
|
||||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
|
||||||
fun getGossipingEvents(): List<Event>
|
|
||||||
|
|
||||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||||
fun areDeviceKeysUploaded(): Boolean
|
fun areDeviceKeysUploaded(): Boolean
|
||||||
fun tidyUpDataBase()
|
fun tidyUpDataBase()
|
||||||
fun logDbUsageInfo()
|
fun logDbUsageInfo()
|
||||||
|
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user