mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 02:15:46 +03:00
Remove legacy crypto code
This commit is contained in:
parent
2709cb2973
commit
dfbb3122e7
174 changed files with 28 additions and 23604 deletions
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble ${{ matrix.target }} debug apk
|
- name: Assemble ${{ matrix.target }} debug apk
|
||||||
run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload ${{ matrix.target }} debug APKs
|
- name: Upload ${{ matrix.target }} debug APKs
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -57,7 +57,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Assemble GPlay unsigned apk
|
- name: Assemble GPlay unsigned apk
|
||||||
run: ./gradlew clean assembleGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload Gplay unsigned APKs
|
- name: Upload Gplay unsigned APKs
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -79,7 +79,7 @@ jobs:
|
||||||
- name: Execute exodus-standalone
|
- name: Execute exodus-standalone
|
||||||
uses: docker://exodusprivacy/exodus-standalone:latest
|
uses: docker://exodusprivacy/exodus-standalone:latest
|
||||||
with:
|
with:
|
||||||
args: /github/workspace/gplayRustCrypto/release/vector-gplay-rustCrypto-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
|
args: /github/workspace/gplay/release/vector-gplay-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
|
||||||
- name: Upload exodus json report
|
- name: Upload exodus json report
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
37
.github/workflows/elementr.yml
vendored
37
.github/workflows/elementr.yml
vendored
|
@ -1,37 +0,0 @@
|
||||||
name: ER APK Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request: { }
|
|
||||||
push:
|
|
||||||
branches: [ develop ]
|
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
|
||||||
env:
|
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
debug:
|
|
||||||
name: Build debug APKs ER
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
target: [ Gplay, Fdroid ]
|
|
||||||
# Allow all jobs on develop. Just one per PR.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.ref == 'refs/heads/develop' && format('elementr-{0}-{1}', matrix.target, github.sha) || format('build-er-debug-{0}-{1}', matrix.target, github.ref) }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Assemble ${{ matrix.target }} debug apk
|
|
||||||
run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES
|
|
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
yes n | towncrier build --version nightly
|
yes n | towncrier build --version nightly
|
||||||
- name: Build and upload Gplay Nightly APK
|
- name: Build and upload Gplay Nightly APK
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
|
||||||
env:
|
env:
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||||
|
|
46
.github/workflows/nightly_er.yml
vendored
46
.github/workflows/nightly_er.yml
vendored
|
@ -1,46 +0,0 @@
|
||||||
name: Build and release Element R nightly APK
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Every nights at 4
|
|
||||||
- cron: "0 4 * * *"
|
|
||||||
|
|
||||||
env:
|
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
name: Build and publish ER nightly Gplay APK to Firebase
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Python 3.8
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Install towncrier
|
|
||||||
run: |
|
|
||||||
python3 -m pip install towncrier
|
|
||||||
- name: Prepare changelog file
|
|
||||||
run: |
|
|
||||||
mv towncrier.toml towncrier.toml.bak
|
|
||||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
|
||||||
rm towncrier.toml.bak
|
|
||||||
yes n | towncrier build --version nightly
|
|
||||||
- name: Build and upload Gplay Nightly ER APK
|
|
||||||
run: |
|
|
||||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
env:
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
|
||||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_R_NIGHTLY_FIREBASE_TOKEN }}
|
|
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
|
@ -49,10 +49,8 @@ jobs:
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
# Not always, if ktlint or detekt fail, avoid running the long lint check.
|
# Not always, if ktlint or detekt fail, avoid running the long lint check.
|
||||||
run: |
|
run: |
|
||||||
./gradlew vector-app:lintGplayKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
./gradlew vector-app:lintGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
./gradlew vector-app:lintFdroidKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
./gradlew vector-app:lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
./gradlew vector-app:lintGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
./gradlew vector-app:lintFdroidRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|
102
.github/workflows/tests-rust.yml
vendored
102
.github/workflows/tests-rust.yml
vendored
|
@ -1,102 +0,0 @@
|
||||||
name: Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request: { }
|
|
||||||
push:
|
|
||||||
branches: [ main, develop ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
|
|
||||||
# Enrich gradle.properties for CI/CD
|
|
||||||
env:
|
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx5g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx3g" -Dkotlin.incremental=false
|
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
name: Runs all tests with rust crypto
|
|
||||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
|
||||||
timeout-minutes: 90 # We might need to increase it if the time for tests grows
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
api-level: [28]
|
|
||||||
# Allow all jobs on main and develop. Just one per PR.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-rust-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-rust-{0}', github.sha) || format('unit-tests-rust-{0}', github.ref) }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
distribution: 'adopt'
|
|
||||||
java-version: '11'
|
|
||||||
- uses: gradle/gradle-build-action@v2
|
|
||||||
with:
|
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
|
||||||
gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/develop' }}
|
|
||||||
|
|
||||||
# - name: Run screenshot tests
|
|
||||||
# run: ./gradlew verifyScreenshots $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
|
|
||||||
# - name: Archive Screenshot Results on Error
|
|
||||||
# if: failure()
|
|
||||||
# uses: actions/upload-artifact@v3
|
|
||||||
# with:
|
|
||||||
# name: screenshot-results
|
|
||||||
# path: |
|
|
||||||
# **/out/failures/
|
|
||||||
# **/build/reports/tests/*UnitTest/
|
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
- uses: michaelkaye/setup-matrix-synapse@v1.0.4
|
|
||||||
with:
|
|
||||||
uploadLogs: true
|
|
||||||
httpPort: 8080
|
|
||||||
disableRateLimiting: true
|
|
||||||
public_baseurl: "http://10.0.2.2:8080/"
|
|
||||||
|
|
||||||
- name: Run all the codecoverage tests at once
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
# continue-on-error: true
|
|
||||||
with:
|
|
||||||
api-level: ${{ matrix.api-level }}
|
|
||||||
arch: x86
|
|
||||||
profile: Nexus 5X
|
|
||||||
target: playstore
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: true
|
|
||||||
# emulator-build: 7425822
|
|
||||||
script: |
|
|
||||||
./gradlew gatherGplayRustCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
./gradlew instrumentationTestsRustWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
|
|
||||||
- name: Upload Rust Integration Test Report Log
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: integration-test-rust-error-results
|
|
||||||
path: |
|
|
||||||
*/build/outputs/androidTest-results/connected/
|
|
||||||
*/build/reports/androidTests/connected/
|
|
||||||
|
|
||||||
# For now ignore sonar
|
|
||||||
# - name: Publish results to Sonar
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.SONARQUBE_GITHUB_API_TOKEN }} # Needed to get PR information, if any
|
|
||||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
# ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
# if: ${{ always() && env.GITHUB_TOKEN != '' && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
|
||||||
# run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
|
|
||||||
- name: Format unit test results
|
|
||||||
if: always()
|
|
||||||
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml
|
|
||||||
|
|
||||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -73,7 +73,7 @@ jobs:
|
||||||
disable-animations: true
|
disable-animations: true
|
||||||
# emulator-build: 7425822
|
# emulator-build: 7425822
|
||||||
script: |
|
script: |
|
||||||
./gradlew gatherGplayKotlinCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||||
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||||
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||||
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
||||||
|
|
|
@ -312,7 +312,7 @@ tasks.register("recordScreenshots", GradleBuild) {
|
||||||
|
|
||||||
tasks.register("verifyScreenshots", GradleBuild) {
|
tasks.register("verifyScreenshots", GradleBuild) {
|
||||||
startParameter.projectProperties.screenshot = ""
|
startParameter.projectProperties.screenshot = ""
|
||||||
tasks = [':vector:verifyPaparazziRustCryptoDebug']
|
tasks = [':vector:verifyPaparazziDebug']
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.initScreenshotTests = { project ->
|
ext.initScreenshotTests = { project ->
|
||||||
|
|
|
@ -87,11 +87,5 @@ task unitTestsWithCoverage(type: GradleBuild) {
|
||||||
task instrumentationTestsWithCoverage(type: GradleBuild) {
|
task instrumentationTestsWithCoverage(type: GradleBuild) {
|
||||||
startParameter.projectProperties.coverage = "true"
|
startParameter.projectProperties.coverage = "true"
|
||||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
||||||
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
|
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
|
||||||
}
|
|
||||||
|
|
||||||
task instrumentationTestsRustWithCoverage(type: GradleBuild) {
|
|
||||||
startParameter.projectProperties.coverage = "true"
|
|
||||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
|
||||||
tasks = [':vector-app:connectedGplayRustCryptoDebugAndroidTest', ':vector:connectedRustCryptoDebugAndroidTest', 'matrix-sdk-android:connectedRustCryptoDebugAndroidTest']
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ mv towncrier.toml towncrier.toml.bak
|
||||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
||||||
rm towncrier.toml.bak
|
rm towncrier.toml.bak
|
||||||
yes n | towncrier build --version nightly
|
yes n | towncrier build --version nightly
|
||||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can reset the change on the codebase.
|
Then you can reset the change on the codebase.
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
android {
|
|
||||||
|
|
||||||
flavorDimensions "crypto"
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
kotlinCrypto {
|
|
||||||
dimension "crypto"
|
|
||||||
// versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
|
|
||||||
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"JC\""
|
|
||||||
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"KotlinCrypto\""
|
|
||||||
}
|
|
||||||
rustCrypto {
|
|
||||||
dimension "crypto"
|
|
||||||
isDefault = true
|
|
||||||
// // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
|
|
||||||
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"RC\""
|
|
||||||
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"RustCrypto\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ plugins {
|
||||||
id 'com.android.library'
|
id 'com.android.library'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
}
|
}
|
||||||
apply from: '../flavor.gradle'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "org.matrix.android.sdk.flow"
|
namespace "org.matrix.android.sdk.flow"
|
||||||
|
|
|
@ -41,7 +41,6 @@ dokkaHtml {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply from: '../flavor.gradle'
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "org.matrix.android.sdk"
|
namespace "org.matrix.android.sdk"
|
||||||
|
@ -158,7 +157,7 @@ dependencies {
|
||||||
// implementation libs.androidx.appCompat
|
// implementation libs.androidx.appCompat
|
||||||
implementation libs.androidx.core
|
implementation libs.androidx.core
|
||||||
|
|
||||||
rustCryptoImplementation libs.androidx.lifecycleLivedata
|
implementation libs.androidx.lifecycleLivedata
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
implementation libs.androidx.lifecycleCommon
|
implementation libs.androidx.lifecycleCommon
|
||||||
|
@ -216,8 +215,8 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.google.phonenumber
|
implementation libs.google.phonenumber
|
||||||
|
|
||||||
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.15")
|
implementation("org.matrix.rustcomponents:crypto-android:0.3.15")
|
||||||
// rustCryptoApi project(":library:rustCrypto")
|
// api project(":library:rustCrypto")
|
||||||
|
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
|
||||||
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
internal class CryptoStoreHelper {
|
|
||||||
|
|
||||||
fun createStore(): IMXCryptoStore {
|
|
||||||
return RealmCryptoStore(
|
|
||||||
realmConfiguration = RealmConfiguration.Builder()
|
|
||||||
.name("test.realm")
|
|
||||||
.modules(RealmCryptoStoreModule())
|
|
||||||
.build(),
|
|
||||||
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
|
|
||||||
userId = "userId_" + Random.nextInt(),
|
|
||||||
deviceId = "deviceId_sample",
|
|
||||||
clock = DefaultClock(),
|
|
||||||
myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import io.realm.Realm
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.common.RetryTestRule
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
|
||||||
import org.matrix.olm.OlmAccount
|
|
||||||
import org.matrix.olm.OlmManager
|
|
||||||
import org.matrix.olm.OlmSession
|
|
||||||
|
|
||||||
private const val DUMMY_DEVICE_KEY = "DeviceKey"
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@Ignore
|
|
||||||
class CryptoStoreTest : InstrumentedTest {
|
|
||||||
|
|
||||||
@get:Rule val rule = RetryTestRule(3)
|
|
||||||
|
|
||||||
private val cryptoStoreHelper = CryptoStoreHelper()
|
|
||||||
private val clock = DefaultClock()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
Realm.init(context())
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Test
|
|
||||||
// fun test_metadata_realm_ok() {
|
|
||||||
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
|
||||||
//
|
|
||||||
// assertFalse(cryptoStore.hasData())
|
|
||||||
//
|
|
||||||
// cryptoStore.open()
|
|
||||||
//
|
|
||||||
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
|
||||||
//
|
|
||||||
// assertTrue(cryptoStore.hasData())
|
|
||||||
//
|
|
||||||
// // Cleanup
|
|
||||||
// cryptoStore.close()
|
|
||||||
// cryptoStore.deleteStore()
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_lastSessionUsed() {
|
|
||||||
// Ensure Olm is initialized
|
|
||||||
OlmManager()
|
|
||||||
|
|
||||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
|
||||||
|
|
||||||
assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
|
||||||
|
|
||||||
val olmAccount1 = OlmAccount().apply {
|
|
||||||
generateOneTimeKeys(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmSession1 = OlmSession().apply {
|
|
||||||
initOutboundSession(
|
|
||||||
olmAccount1,
|
|
||||||
olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
|
||||||
olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessionId1 = olmSession1.sessionIdentifier()
|
|
||||||
val olmSessionWrapper1 = OlmSessionWrapper(olmSession1)
|
|
||||||
|
|
||||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
|
||||||
|
|
||||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
|
||||||
|
|
||||||
val olmAccount2 = OlmAccount().apply {
|
|
||||||
generateOneTimeKeys(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmSession2 = OlmSession().apply {
|
|
||||||
initOutboundSession(
|
|
||||||
olmAccount2,
|
|
||||||
olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
|
||||||
olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessionId2 = olmSession2.sessionIdentifier()
|
|
||||||
val olmSessionWrapper2 = OlmSessionWrapper(olmSession2)
|
|
||||||
|
|
||||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
|
||||||
|
|
||||||
// Ensure sessionIds are distinct
|
|
||||||
assertNotEquals(sessionId1, sessionId2)
|
|
||||||
|
|
||||||
// Note: we cannot be sure what will be the result of getLastUsedSessionId() here
|
|
||||||
|
|
||||||
olmSessionWrapper2.onMessageReceived(clock.epochMillis())
|
|
||||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
|
||||||
|
|
||||||
// sessionId2 is returned now
|
|
||||||
assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
|
||||||
|
|
||||||
Thread.sleep(2)
|
|
||||||
|
|
||||||
olmSessionWrapper1.onMessageReceived(clock.epochMillis())
|
|
||||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
|
||||||
|
|
||||||
// sessionId1 is returned now
|
|
||||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
olmSession1.releaseSession()
|
|
||||||
olmSession2.releaseSession()
|
|
||||||
|
|
||||||
olmAccount1.releaseAccount()
|
|
||||||
olmAccount2.releaseAccount()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
|
||||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
|
||||||
class PreShareKeysTest : InstrumentedTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
val e2eRoomID = testData.roomId
|
|
||||||
val aliceSession = testData.firstSession
|
|
||||||
val bobSession = testData.secondSession!!
|
|
||||||
|
|
||||||
// clear any outbound session
|
|
||||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
|
||||||
|
|
||||||
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
|
||||||
|
|
||||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
|
||||||
Log.d("#E2E", "Room Key Received from alice $preShareCount")
|
|
||||||
|
|
||||||
// Force presharing of new outbound key
|
|
||||||
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID)
|
|
||||||
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
|
||||||
newKeysCount > preShareCount
|
|
||||||
}
|
|
||||||
|
|
||||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
|
||||||
|
|
||||||
// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
|
||||||
// val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
|
||||||
//
|
|
||||||
// val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
|
||||||
// val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId)!!
|
|
||||||
// val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
|
||||||
// assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
|
||||||
// assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
|
||||||
|
|
||||||
// val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
|
|
||||||
//
|
|
||||||
// assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
|
||||||
|
|
||||||
// val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
|
||||||
// .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
|
||||||
//
|
|
||||||
// assertEquals("The session received by bob should match what alice sent", 0, sharedIndex)
|
|
||||||
|
|
||||||
// Just send a real message as test
|
|
||||||
val sentEventId = testHelper.sendMessageInRoom(aliceSession.getRoom(e2eRoomID)!!, "Allo")
|
|
||||||
|
|
||||||
val sentEvent = aliceSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!
|
|
||||||
|
|
||||||
// assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that no additional key was shared
|
|
||||||
assertEquals(newKeysCount, bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
|
||||||
import org.junit.Assert
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
|
||||||
import org.matrix.olm.OlmSession
|
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ref:
|
|
||||||
* - https://github.com/matrix-org/matrix-doc/pull/1719
|
|
||||||
* - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages
|
|
||||||
* - https://github.com/matrix-org/matrix-js-sdk/pull/780
|
|
||||||
* - https://github.com/matrix-org/matrix-ios-sdk/pull/778
|
|
||||||
* - https://github.com/matrix-org/matrix-ios-sdk/pull/784
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
|
||||||
class UnwedgingTest : InstrumentedTest {
|
|
||||||
|
|
||||||
private lateinit var messagesReceivedByBob: List<TimelineEvent>
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun init() {
|
|
||||||
messagesReceivedByBob = emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - Alice & Bob in a e2e room
|
|
||||||
* - Alice sends a 1st message with a 1st megolm session
|
|
||||||
* - Store the olm session between A&B devices
|
|
||||||
* - Alice sends a 2nd message with a 2nd megolm session
|
|
||||||
* - Simulate Alice using a backup of her OS and make her crypto state like after the first message
|
|
||||||
* - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
|
|
||||||
*
|
|
||||||
* What Bob must see:
|
|
||||||
* -> No issue with the 2 first messages
|
|
||||||
* -> The third event must fail to decrypt at first because Bob the olm session is wedged
|
|
||||||
* -> This is automatically fixed after SDKs restarted the olm session
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
fun testUnwedging() = runCryptoTest(
|
|
||||||
context(),
|
|
||||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
|
||||||
) { cryptoTestHelper, testHelper ->
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val aliceRoomId = cryptoTestData.roomId
|
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
|
||||||
|
|
||||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
|
||||||
val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
|
|
||||||
|
|
||||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
|
||||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
|
||||||
|
|
||||||
val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20))
|
|
||||||
bobTimeline.start()
|
|
||||||
|
|
||||||
messagesReceivedByBob = emptyList()
|
|
||||||
|
|
||||||
// - Alice sends a 1st message with a 1st megolm session
|
|
||||||
roomFromAlicePOV.sendService().sendTextMessage("First message")
|
|
||||||
|
|
||||||
// Wait for the message to be received by Bob
|
|
||||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1)
|
|
||||||
|
|
||||||
messagesReceivedByBob.size shouldBeEqualTo 1
|
|
||||||
val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
|
||||||
|
|
||||||
// - Store the olm session between A&B devices
|
|
||||||
// Let us pickle our session with bob here so we can later unpickle it
|
|
||||||
// and wedge our session.
|
|
||||||
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)
|
|
||||||
sessionIdsForBob!!.size shouldBeEqualTo 1
|
|
||||||
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)!!
|
|
||||||
|
|
||||||
val oldSession = serializeForRealm(olmSession.olmSession)
|
|
||||||
|
|
||||||
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
|
|
||||||
|
|
||||||
messagesReceivedByBob = emptyList()
|
|
||||||
Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
|
|
||||||
// - Alice sends a 2nd message with a 2nd megolm session
|
|
||||||
roomFromAlicePOV.sendService().sendTextMessage("Second message")
|
|
||||||
|
|
||||||
// Wait for the message to be received by Bob
|
|
||||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2)
|
|
||||||
|
|
||||||
messagesReceivedByBob.size shouldBeEqualTo 2
|
|
||||||
// Session should have changed
|
|
||||||
val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
|
||||||
Assert.assertNotEquals(firstMessageSession, secondMessageSession)
|
|
||||||
|
|
||||||
// Let us wedge the session now. Set crypto state like after the first message
|
|
||||||
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
|
|
||||||
|
|
||||||
aliceCryptoStore.storeSession(
|
|
||||||
OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!),
|
|
||||||
bobSession.cryptoService().getMyCryptoDevice().identityKey()!!
|
|
||||||
)
|
|
||||||
olmDevice.clearOlmSessionCache()
|
|
||||||
|
|
||||||
// Force new session, and key share
|
|
||||||
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
|
|
||||||
|
|
||||||
Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
|
|
||||||
// - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
|
|
||||||
roomFromAlicePOV.sendService().sendTextMessage("Third message")
|
|
||||||
// Bob should not be able to decrypt, because the session key could not be sent
|
|
||||||
// Wait for the message to be received by Bob
|
|
||||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3)
|
|
||||||
|
|
||||||
messagesReceivedByBob.size shouldBeEqualTo 3
|
|
||||||
|
|
||||||
val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
|
||||||
Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
|
|
||||||
Assert.assertNotEquals(secondMessageSession, thirdMessageSession)
|
|
||||||
|
|
||||||
Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType())
|
|
||||||
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
|
|
||||||
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
|
|
||||||
// Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
|
|
||||||
|
|
||||||
Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
|
||||||
|
|
||||||
// It's a trick to force key request on fail to decrypt
|
|
||||||
bobSession.cryptoService().crossSigningService()
|
|
||||||
.initializeCrossSigning(
|
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
|
||||||
user = bobSession.myUserId,
|
|
||||||
password = TestConstants.PASSWORD,
|
|
||||||
session = flowResponse.session
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait until we received back the key
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
// we should get back the key and be able to decrypt
|
|
||||||
val result = tryOrNull {
|
|
||||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
|
||||||
}
|
|
||||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
|
||||||
result != null
|
|
||||||
}
|
|
||||||
|
|
||||||
bobTimeline.dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
val listener = object : Timeline.Listener {
|
|
||||||
override fun onTimelineFailure(throwable: Throwable) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
|
||||||
val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED }
|
|
||||||
|
|
||||||
if (messagesReceived.size == expectedCount) {
|
|
||||||
removeListener(this)
|
|
||||||
continuation.resume(messagesReceived)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener)
|
|
||||||
continuation.invokeOnCancellation { removeListener(listener) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,609 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.amshove.kluent.internal.assertEquals
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
|
||||||
class SASTest : InstrumentedTest {
|
|
||||||
|
|
||||||
val scope = CoroutineScope(SupervisorJob())
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
|
|
||||||
Log.d("#E2E", "verification: doE2ETestWithAliceAndBobInARoom")
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
Log.d("#E2E", "verification: initializeCrossSigning")
|
|
||||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val bobSession = cryptoTestData.secondSession
|
|
||||||
|
|
||||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
|
||||||
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
|
||||||
|
|
||||||
Log.d("#E2E", "verification: requestVerificationAndWaitForReadyState")
|
|
||||||
val txId = SasVerificationTestHelper(testHelper)
|
|
||||||
.requestVerificationAndWaitForReadyState(scope, cryptoTestData, listOf(VerificationMethod.SAS))
|
|
||||||
|
|
||||||
Log.d("#E2E", "verification: startKeyVerification")
|
|
||||||
aliceVerificationService.startKeyVerification(
|
|
||||||
VerificationMethod.SAS,
|
|
||||||
bobSession.myUserId,
|
|
||||||
txId
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.d("#E2E", "verification: ensure bob has received start")
|
|
||||||
testHelper.retryWithBackoff {
|
|
||||||
Log.d("#E2E", "verification: ${bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state}")
|
|
||||||
bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state == EVerificationState.Started
|
|
||||||
}
|
|
||||||
|
|
||||||
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId)
|
|
||||||
|
|
||||||
assertNotNull("Bob should have started verif transaction", bobKeyTx)
|
|
||||||
assertTrue(bobKeyTx is SasVerificationTransaction)
|
|
||||||
|
|
||||||
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId)
|
|
||||||
assertTrue(aliceKeyTx is SasVerificationTransaction)
|
|
||||||
|
|
||||||
assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
|
|
||||||
|
|
||||||
val aliceCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
|
|
||||||
aliceVerificationService.requestEventFlow().onEach {
|
|
||||||
Log.d("#E2E", "alice flow event $it | ${it.getTransaction()?.dbgState()}")
|
|
||||||
val tx = it.getTransaction()
|
|
||||||
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
|
|
||||||
if (tx.state() is SasTransactionState.Cancelled) {
|
|
||||||
aliceCancelled.complete(tx.state() as SasTransactionState.Cancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
val bobCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
|
|
||||||
bobVerificationService.requestEventFlow().onEach {
|
|
||||||
Log.d("#E2E", "bob flow event $it | ${it.getTransaction()?.dbgState()}")
|
|
||||||
val tx = it.getTransaction()
|
|
||||||
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
|
|
||||||
if (tx.state() is SasTransactionState.Cancelled) {
|
|
||||||
bobCancelled.complete(tx.state() as SasTransactionState.Cancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
aliceVerificationService.cancelVerificationRequest(bobSession.myUserId, txId)
|
|
||||||
|
|
||||||
val cancelledAlice = aliceCancelled.await()
|
|
||||||
val cancelledBob = bobCancelled.await()
|
|
||||||
|
|
||||||
assertEquals("Should be User cancelled on alice side", CancelCode.User, cancelledAlice.cancelCode)
|
|
||||||
assertEquals("Should be User cancelled on bob side", CancelCode.User, cancelledBob.cancelCode)
|
|
||||||
|
|
||||||
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId))
|
|
||||||
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
@Test
|
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
fail("Not passing for the moment")
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
|
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
|
||||||
|
|
||||||
val protocols = listOf("meh_dont_know")
|
|
||||||
val tid = "00000000"
|
|
||||||
|
|
||||||
// Bob should receive a cancel
|
|
||||||
var cancelReason: CancelCode? = null
|
|
||||||
val cancelLatch = CountDownLatch(1)
|
|
||||||
|
|
||||||
val bobListener = object : VerificationService.Listener {
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
tx as SasVerificationTransaction
|
|
||||||
if (tx.transactionId == tid && tx.state() is SasTransactionState.Cancelled) {
|
|
||||||
cancelReason = (tx.state() as SasTransactionState.Cancelled).cancelCode
|
|
||||||
cancelLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// bobSession.cryptoService().verificationService().addListener(bobListener)
|
|
||||||
|
|
||||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
|
||||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
|
||||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
|
||||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
|
||||||
// TODO canceledToDeviceEvent = event
|
|
||||||
// TODO cancelLatch.countDown()
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO })
|
|
||||||
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val aliceUserID = aliceSession.myUserId
|
|
||||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
|
|
||||||
val aliceListener = object : VerificationService.Listener {
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
tx as SasVerificationTransaction
|
|
||||||
if (tx.state() is SasTransactionState.SasStarted) {
|
|
||||||
runBlocking {
|
|
||||||
tx.acceptVerification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
|
|
||||||
|
|
||||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
|
|
||||||
|
|
||||||
testHelper.await(cancelLatch)
|
|
||||||
|
|
||||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
fail("Not passing for the moment")
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
|
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
|
||||||
|
|
||||||
val mac = listOf("shaBit")
|
|
||||||
val tid = "00000000"
|
|
||||||
|
|
||||||
// Bob should receive a cancel
|
|
||||||
val canceledToDeviceEvent: Event? = null
|
|
||||||
val cancelLatch = CountDownLatch(1)
|
|
||||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
|
||||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
|
||||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
|
||||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
|
||||||
// TODO canceledToDeviceEvent = event
|
|
||||||
// TODO cancelLatch.countDown()
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO })
|
|
||||||
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val aliceUserID = aliceSession.myUserId
|
|
||||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
|
|
||||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac)
|
|
||||||
|
|
||||||
testHelper.await(cancelLatch)
|
|
||||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
|
||||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
fail("Not passing for the moment")
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
|
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
|
||||||
|
|
||||||
val codes = listOf("bin", "foo", "bar")
|
|
||||||
val tid = "00000000"
|
|
||||||
|
|
||||||
// Bob should receive a cancel
|
|
||||||
var canceledToDeviceEvent: Event? = null
|
|
||||||
val cancelLatch = CountDownLatch(1)
|
|
||||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
|
||||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
|
||||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
|
||||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
|
||||||
// TODO canceledToDeviceEvent = event
|
|
||||||
// TODO cancelLatch.countDown()
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO }
|
|
||||||
// TODO })
|
|
||||||
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val aliceUserID = aliceSession.myUserId
|
|
||||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
|
|
||||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes)
|
|
||||||
|
|
||||||
testHelper.await(cancelLatch)
|
|
||||||
|
|
||||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
|
||||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fakeBobStart(
|
|
||||||
bobSession: Session,
|
|
||||||
aliceUserID: String?,
|
|
||||||
aliceDevice: String?,
|
|
||||||
tid: String,
|
|
||||||
protocols: List<String> = SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
|
|
||||||
hashes: List<String> = SasVerificationTransaction.KNOWN_HASHES,
|
|
||||||
mac: List<String> = SasVerificationTransaction.KNOWN_MACS,
|
|
||||||
codes: List<String> = SasVerificationTransaction.KNOWN_SHORT_CODES
|
|
||||||
) {
|
|
||||||
val startMessage = KeyVerificationStart(
|
|
||||||
fromDevice = bobSession.cryptoService().getMyCryptoDevice().deviceId,
|
|
||||||
method = VerificationMethod.SAS.toValue(),
|
|
||||||
transactionId = tid,
|
|
||||||
keyAgreementProtocols = protocols,
|
|
||||||
hashes = hashes,
|
|
||||||
messageAuthenticationCodes = mac,
|
|
||||||
shortAuthenticationStrings = codes
|
|
||||||
)
|
|
||||||
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
|
||||||
|
|
||||||
// TODO val sendLatch = CountDownLatch(1)
|
|
||||||
// TODO bobSession.cryptoRestClient.sendToDevice(
|
|
||||||
// TODO EventType.KEY_VERIFICATION_START,
|
|
||||||
// TODO contentMap,
|
|
||||||
// TODO tid,
|
|
||||||
// TODO TestMatrixCallback<Void>(sendLatch)
|
|
||||||
// TODO )
|
|
||||||
}
|
|
||||||
|
|
||||||
// any two devices may only have at most one key verification in flight at a time.
|
|
||||||
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
|
|
||||||
@Test
|
|
||||||
fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val bobSession = cryptoTestData.secondSession
|
|
||||||
|
|
||||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
|
||||||
|
|
||||||
val aliceCreatedLatch = CountDownLatch(2)
|
|
||||||
val aliceCancelledLatch = CountDownLatch(1)
|
|
||||||
val createdTx = mutableListOf<VerificationTransaction>()
|
|
||||||
val aliceListener = object : VerificationService.Listener {
|
|
||||||
override fun transactionCreated(tx: VerificationTransaction) {
|
|
||||||
createdTx.add(tx)
|
|
||||||
aliceCreatedLatch.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
tx as SasVerificationTransaction
|
|
||||||
if (tx.state() is SasTransactionState.Cancelled && !(tx.state() as SasTransactionState.Cancelled).byMe) {
|
|
||||||
aliceCancelledLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// aliceVerificationService.addListener(aliceListener)
|
|
||||||
|
|
||||||
val bobUserId = bobSession!!.myUserId
|
|
||||||
val bobDeviceId = bobSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), forceDownload = true)
|
|
||||||
// aliceVerificationService.beginKeyVerification(listOf(VerificationMethod.SAS), bobUserId, bobDeviceId)
|
|
||||||
// aliceVerificationService.beginKeyVerification(bobUserId, bobDeviceId)
|
|
||||||
// testHelper.await(aliceCreatedLatch)
|
|
||||||
// testHelper.await(aliceCancelledLatch)
|
|
||||||
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that when alice starts a 'correct' request, bob agrees.
|
|
||||||
*/
|
|
||||||
// @Test
|
|
||||||
// @Ignore("This test will be ignored until it is fixed")
|
|
||||||
// fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
//
|
|
||||||
// val aliceSession = cryptoTestData.firstSession
|
|
||||||
// val bobSession = cryptoTestData.secondSession
|
|
||||||
//
|
|
||||||
// val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
|
||||||
// val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
|
||||||
//
|
|
||||||
// val aliceAcceptedLatch = CountDownLatch(1)
|
|
||||||
// val aliceListener = object : VerificationService.Listener {
|
|
||||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
// if (tx.state() is VerificationTxState.OnAccepted) {
|
|
||||||
// aliceAcceptedLatch.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// aliceVerificationService.addListener(aliceListener)
|
|
||||||
//
|
|
||||||
// val bobListener = object : VerificationService.Listener {
|
|
||||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
// if (tx.state() is VerificationTxState.OnStarted && tx is SasVerificationTransaction) {
|
|
||||||
// bobVerificationService.removeListener(this)
|
|
||||||
// runBlocking {
|
|
||||||
// tx.acceptVerification()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// bobVerificationService.addListener(bobListener)
|
|
||||||
//
|
|
||||||
// val bobUserId = bobSession.myUserId
|
|
||||||
// val bobDeviceId = runBlocking {
|
|
||||||
// bobSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
|
|
||||||
// testHelper.await(aliceAcceptedLatch)
|
|
||||||
//
|
|
||||||
// aliceVerificationService.getExistingTransaction(bobUserId, )
|
|
||||||
//
|
|
||||||
// assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
|
|
||||||
//
|
|
||||||
// // check that agreement is valid
|
|
||||||
// assertTrue("Agreed Protocol should be Valid", accepted != null)
|
|
||||||
// assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol))
|
|
||||||
// assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash))
|
|
||||||
// assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode))
|
|
||||||
//
|
|
||||||
// accepted!!.shortAuthenticationStrings.forEach {
|
|
||||||
// assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Test
|
|
||||||
// fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
// cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
|
||||||
// val sasTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
|
|
||||||
// val aliceSession = cryptoTestData.firstSession
|
|
||||||
// val bobSession = cryptoTestData.secondSession!!
|
|
||||||
// val transactionId = sasTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, supportedMethods)
|
|
||||||
//
|
|
||||||
// val latch = CountDownLatch(2)
|
|
||||||
// val aliceListener = object : VerificationService.Listener {
|
|
||||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
// Timber.v("Alice transactionUpdated: ${tx.state()}")
|
|
||||||
// latch.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
|
|
||||||
// val bobListener = object : VerificationService.Listener {
|
|
||||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
// Timber.v("Bob transactionUpdated: ${tx.state()}")
|
|
||||||
// latch.countDown()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// bobSession.cryptoService().verificationService().addListener(bobListener)
|
|
||||||
// aliceSession.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, transactionId)
|
|
||||||
//
|
|
||||||
// testHelper.await(latch)
|
|
||||||
// val aliceTx =
|
|
||||||
// aliceSession.cryptoService().verificationService().getExistingTransaction(bobSession.myUserId, transactionId) as SasVerificationTransaction
|
|
||||||
// val bobTx = bobSession.cryptoService().verificationService().getExistingTransaction(aliceSession.myUserId, transactionId) as SasVerificationTransaction
|
|
||||||
//
|
|
||||||
// assertEquals("Should have same SAS", aliceTx.getDecimalCodeRepresentation(), bobTx.getDecimalCodeRepresentation())
|
|
||||||
//
|
|
||||||
// val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
|
|
||||||
// val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
|
|
||||||
//
|
|
||||||
// assertEquals(
|
|
||||||
// "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
|
|
||||||
// bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
|
||||||
val sasVerificationTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
|
|
||||||
val transactionId = sasVerificationTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, listOf(VerificationMethod.SAS))
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val bobSession = cryptoTestData.secondSession
|
|
||||||
|
|
||||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
|
||||||
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
|
||||||
|
|
||||||
val verifiedLatch = CountDownLatch(2)
|
|
||||||
val aliceListener = object : VerificationService.Listener {
|
|
||||||
|
|
||||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
|
||||||
Timber.v("RequestUpdated pr=$pr")
|
|
||||||
}
|
|
||||||
|
|
||||||
var matched = false
|
|
||||||
var verified = false
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
if (tx !is SasVerificationTransaction) return
|
|
||||||
Timber.v("Alice transactionUpdated: ${tx.state()} on thread:${Thread.currentThread()}")
|
|
||||||
when (tx.state()) {
|
|
||||||
SasTransactionState.SasShortCodeReady -> {
|
|
||||||
if (!matched) {
|
|
||||||
matched = true
|
|
||||||
runBlocking {
|
|
||||||
delay(500)
|
|
||||||
tx.userHasVerifiedShortCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SasTransactionState.Done -> {
|
|
||||||
if (!verified) {
|
|
||||||
verified = true
|
|
||||||
verifiedLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// aliceVerificationService.addListener(aliceListener)
|
|
||||||
|
|
||||||
val bobListener = object : VerificationService.Listener {
|
|
||||||
var accepted = false
|
|
||||||
var matched = false
|
|
||||||
var verified = false
|
|
||||||
|
|
||||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
|
||||||
Timber.v("RequestUpdated: pr=$pr")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
if (tx !is SasVerificationTransaction) return
|
|
||||||
Timber.v("Bob transactionUpdated: ${tx.state()} on thread: ${Thread.currentThread()}")
|
|
||||||
when (tx.state()) {
|
|
||||||
// VerificationTxState.SasStarted -> {
|
|
||||||
// if (!accepted) {
|
|
||||||
// accepted = true
|
|
||||||
// runBlocking {
|
|
||||||
// tx.acceptVerification()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
SasTransactionState.SasShortCodeReady -> {
|
|
||||||
if (!matched) {
|
|
||||||
matched = true
|
|
||||||
runBlocking {
|
|
||||||
delay(500)
|
|
||||||
tx.userHasVerifiedShortCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SasTransactionState.Done -> {
|
|
||||||
if (!verified) {
|
|
||||||
verified = true
|
|
||||||
verifiedLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// bobVerificationService.addListener(bobListener)
|
|
||||||
|
|
||||||
val bobUserId = bobSession.myUserId
|
|
||||||
val bobDeviceId = runBlocking {
|
|
||||||
bobSession.cryptoService().getMyCryptoDevice().deviceId
|
|
||||||
}
|
|
||||||
aliceVerificationService.startKeyVerification(VerificationMethod.SAS, bobUserId, transactionId)
|
|
||||||
|
|
||||||
Timber.v("Await after beginKey ${Thread.currentThread()}")
|
|
||||||
testHelper.await(verifiedLatch)
|
|
||||||
|
|
||||||
// Assert that devices are verified
|
|
||||||
val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
|
|
||||||
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
|
|
||||||
bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyCryptoDevice().deviceId)
|
|
||||||
|
|
||||||
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
|
|
||||||
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
|
||||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
|
||||||
val aliceSession = cryptoTestData.firstSession
|
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
|
||||||
|
|
||||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
|
||||||
val bobVerificationService = bobSession.cryptoService().verificationService()
|
|
||||||
|
|
||||||
val req = aliceVerificationService.requestKeyVerificationInDMs(
|
|
||||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
|
||||||
bobSession.myUserId,
|
|
||||||
cryptoTestData.roomId
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestID = req.transactionId
|
|
||||||
|
|
||||||
Log.v("TEST", "== requestID is $requestID")
|
|
||||||
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
|
|
||||||
Log.v("TEST", "== prBobPOV is $prBobPOV")
|
|
||||||
prBobPOV?.transactionId == requestID
|
|
||||||
}
|
|
||||||
|
|
||||||
bobVerificationService.readyPendingVerification(
|
|
||||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
|
||||||
aliceSession.myUserId,
|
|
||||||
requestID
|
|
||||||
)
|
|
||||||
|
|
||||||
// wait for alice to get the ready
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
|
|
||||||
Log.v("TEST", "== prAlicePOV is $prAlicePOV")
|
|
||||||
prAlicePOV?.transactionId == requestID && prAlicePOV.state == EVerificationState.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start concurrent!
|
|
||||||
aliceVerificationService.startKeyVerification(
|
|
||||||
method = VerificationMethod.SAS,
|
|
||||||
otherUserId = bobSession.myUserId,
|
|
||||||
requestId = requestID,
|
|
||||||
)
|
|
||||||
|
|
||||||
bobVerificationService.startKeyVerification(
|
|
||||||
method = VerificationMethod.SAS,
|
|
||||||
otherUserId = aliceSession.myUserId,
|
|
||||||
requestId = requestID,
|
|
||||||
)
|
|
||||||
|
|
||||||
// we should reach SHOW SAS on both
|
|
||||||
var alicePovTx: SasVerificationTransaction?
|
|
||||||
var bobPovTx: SasVerificationTransaction?
|
|
||||||
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID) as? SasVerificationTransaction
|
|
||||||
Log.v("TEST", "== alicePovTx is $alicePovTx")
|
|
||||||
alicePovTx?.state() == SasTransactionState.SasShortCodeReady
|
|
||||||
}
|
|
||||||
// wait for alice to get the ready
|
|
||||||
testHelper.retryPeriodically {
|
|
||||||
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID) as? SasVerificationTransaction
|
|
||||||
Log.v("TEST", "== bobPovTx is $bobPovTx")
|
|
||||||
bobPovTx?.state() == SasTransactionState.SasShortCodeReady
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
|
@ -1,251 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
|
||||||
import org.amshove.kluent.shouldBeNull
|
|
||||||
import org.amshove.kluent.shouldNotBeNull
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
|
||||||
class QrCodeTest : InstrumentedTest {
|
|
||||||
|
|
||||||
private val qrCode1 = QrCodeData.VerifyingAnotherUser(
|
|
||||||
transactionId = "MaTransaction",
|
|
||||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
|
||||||
otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
|
||||||
sharedSecret = "MTIzNDU2Nzg"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val value1 =
|
|
||||||
"MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
|
||||||
|
|
||||||
private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted(
|
|
||||||
transactionId = "MaTransaction",
|
|
||||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
|
||||||
otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
|
||||||
sharedSecret = "MTIzNDU2Nzg"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val value2 =
|
|
||||||
"MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
|
||||||
|
|
||||||
private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted(
|
|
||||||
transactionId = "MaTransaction",
|
|
||||||
deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
|
||||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
|
||||||
sharedSecret = "MTIzNDU2Nzg"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val value3 =
|
|
||||||
"MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678"
|
|
||||||
|
|
||||||
private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1)
|
|
||||||
|
|
||||||
private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5")
|
|
||||||
|
|
||||||
private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55")
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testEncoding1() {
|
|
||||||
qrCode1.toEncodedString() shouldBeEqualTo value1
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testEncoding2() {
|
|
||||||
qrCode2.toEncodedString() shouldBeEqualTo value2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testEncoding3() {
|
|
||||||
qrCode3.toEncodedString() shouldBeEqualTo value3
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSymmetry1() {
|
|
||||||
qrCode1.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode1
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSymmetry2() {
|
|
||||||
qrCode2.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testSymmetry3() {
|
|
||||||
qrCode3.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode3
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testCase1() {
|
|
||||||
val url = qrCode1.toEncodedString()
|
|
||||||
|
|
||||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
|
||||||
checkHeader(byteArray)
|
|
||||||
|
|
||||||
// Mode
|
|
||||||
byteArray[7] shouldBeEqualTo 0
|
|
||||||
|
|
||||||
checkSizeAndTransaction(byteArray)
|
|
||||||
|
|
||||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
|
||||||
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testCase2() {
|
|
||||||
val url = qrCode2.toEncodedString()
|
|
||||||
|
|
||||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
|
||||||
checkHeader(byteArray)
|
|
||||||
|
|
||||||
// Mode
|
|
||||||
byteArray[7] shouldBeEqualTo 1
|
|
||||||
|
|
||||||
checkSizeAndTransaction(byteArray)
|
|
||||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
|
||||||
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testCase3() {
|
|
||||||
val url = qrCode3.toEncodedString()
|
|
||||||
|
|
||||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
|
||||||
checkHeader(byteArray)
|
|
||||||
|
|
||||||
// Mode
|
|
||||||
byteArray[7] shouldBeEqualTo 2
|
|
||||||
|
|
||||||
checkSizeAndTransaction(byteArray)
|
|
||||||
compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray)
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray)
|
|
||||||
|
|
||||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testLongTransactionId() {
|
|
||||||
// Size on two bytes (2_000 = 0x07D0)
|
|
||||||
val longTransactionId = "PatternId_".repeat(200)
|
|
||||||
|
|
||||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
|
||||||
|
|
||||||
val result = qrCode.toEncodedString()
|
|
||||||
val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId")
|
|
||||||
|
|
||||||
result shouldBeEqualTo expected
|
|
||||||
|
|
||||||
// Reverse operation
|
|
||||||
expected.toQrCodeData() shouldBeEqualTo qrCode
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAnyTransactionId() {
|
|
||||||
for (qty in 0 until 0x1FFF step 200) {
|
|
||||||
val longTransactionId = "a".repeat(qty)
|
|
||||||
|
|
||||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
|
||||||
|
|
||||||
// Symmetric operation
|
|
||||||
qrCode.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error cases
|
|
||||||
@Test
|
|
||||||
fun testErrorHeader() {
|
|
||||||
value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull()
|
|
||||||
value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull()
|
|
||||||
value1.replace("MATRIX", "").toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorVersion() {
|
|
||||||
value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull()
|
|
||||||
value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull()
|
|
||||||
value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull()
|
|
||||||
value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorSecretTooShort() {
|
|
||||||
value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorNoTransactionNoKeyNoSecret() {
|
|
||||||
// But keep transaction length
|
|
||||||
"MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorNoKeyNoSecret() {
|
|
||||||
"MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorTransactionLengthTooShort() {
|
|
||||||
// In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch
|
|
||||||
value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testErrorTransactionLengthTooBig() {
|
|
||||||
value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compareArray(actual: ByteArray, expected: ByteArray) {
|
|
||||||
actual.size shouldBeEqualTo expected.size
|
|
||||||
|
|
||||||
for (i in actual.indices) {
|
|
||||||
actual[i] shouldBeEqualTo expected[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkHeader(byteArray: ByteArray) {
|
|
||||||
// MATRIX
|
|
||||||
byteArray[0] shouldBeEqualTo 'M'.code.toByte()
|
|
||||||
byteArray[1] shouldBeEqualTo 'A'.code.toByte()
|
|
||||||
byteArray[2] shouldBeEqualTo 'T'.code.toByte()
|
|
||||||
byteArray[3] shouldBeEqualTo 'R'.code.toByte()
|
|
||||||
byteArray[4] shouldBeEqualTo 'I'.code.toByte()
|
|
||||||
byteArray[5] shouldBeEqualTo 'X'.code.toByte()
|
|
||||||
|
|
||||||
// Version
|
|
||||||
byteArray[6] shouldBeEqualTo 2
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkSizeAndTransaction(byteArray: ByteArray) {
|
|
||||||
// Size
|
|
||||||
byteArray[8] shouldBeEqualTo 0
|
|
||||||
byteArray[9] shouldBeEqualTo 13
|
|
||||||
|
|
||||||
// Transaction
|
|
||||||
byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldBeEqualTo "MaTransaction"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.database
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import io.realm.Realm
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
|
|
||||||
class CryptoSanityMigrationTest {
|
|
||||||
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
|
|
||||||
|
|
||||||
lateinit var context: Context
|
|
||||||
var realm: Realm? = null
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
context = InstrumentationRegistry.getInstrumentation().context
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
realm?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun cryptoDatabaseShouldMigrateGracefully() {
|
|
||||||
val realmName = "crypto_store_20.realm"
|
|
||||||
|
|
||||||
val migration = RealmCryptoStoreMigration(
|
|
||||||
object : Clock {
|
|
||||||
override fun epochMillis(): Long {
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val realmConfiguration = configurationFactory.createConfiguration(
|
|
||||||
realmName,
|
|
||||||
"7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca",
|
|
||||||
RealmCryptoStoreModule(),
|
|
||||||
migration.schemaVersion,
|
|
||||||
migration
|
|
||||||
)
|
|
||||||
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
|
|
||||||
|
|
||||||
realm = Realm.getInstance(realmConfiguration)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
|
||||||
import org.matrix.olm.OlmPkMessage
|
|
||||||
|
|
||||||
class BackupRecoveryKey(private val key: ByteArray) : IBackupRecoveryKey {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is BackupRecoveryKey) return false
|
|
||||||
return this.toBase58() == other.toBase58()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return key.contentHashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toBase58() = computeRecoveryKey(key)
|
|
||||||
|
|
||||||
override fun toBase64() = key.toBase64NoPadding()
|
|
||||||
|
|
||||||
override fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String): String = withOlmDecryption {
|
|
||||||
it.setPrivateKey(key)
|
|
||||||
it.decrypt(OlmPkMessage().apply {
|
|
||||||
this.mEphemeralKey = ephemeralKey
|
|
||||||
this.mCipherText = ciphertext
|
|
||||||
this.mMac = mac
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun megolmV1PublicKey() = v1pk
|
|
||||||
|
|
||||||
private val v1pk = object : IMegolmV1PublicKey {
|
|
||||||
override val publicKey: String
|
|
||||||
get() = withOlmDecryption {
|
|
||||||
it.setPrivateKey(key)
|
|
||||||
}
|
|
||||||
override val privateKeySalt: String?
|
|
||||||
get() = null // not use in kotlin sdk
|
|
||||||
override val privateKeyIterations: Int?
|
|
||||||
get() = null // not use in kotlin sdk
|
|
||||||
override val backupAlgorithm: String
|
|
||||||
get() = "" // not use in kotlin sdk
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
|
||||||
|
|
||||||
object BackupUtils {
|
|
||||||
|
|
||||||
fun recoveryKeyFromBase58(base58: String): IBackupRecoveryKey? {
|
|
||||||
return extractCurveKeyFromRecoveryKey(base58)?.let {
|
|
||||||
BackupRecoveryKey(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? {
|
|
||||||
return BackupRecoveryKey(generatePrivateKeyWithPassword(passphrase, null).privateKey)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationAcceptContent(
|
|
||||||
@Json(name = "hash") override val hash: String?,
|
|
||||||
@Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?,
|
|
||||||
@Json(name = "message_authentication_code") override val messageAuthenticationCode: String?,
|
|
||||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
|
|
||||||
@Json(name = "commitment") override var commitment: String? = null
|
|
||||||
) : VerificationInfoAccept {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
|
|
||||||
companion object : VerificationInfoAcceptFactory {
|
|
||||||
|
|
||||||
override fun create(
|
|
||||||
tid: String,
|
|
||||||
keyAgreementProtocol: String,
|
|
||||||
hash: String,
|
|
||||||
commitment: String,
|
|
||||||
messageAuthenticationCode: String,
|
|
||||||
shortAuthenticationStrings: List<String>
|
|
||||||
): VerificationInfoAccept {
|
|
||||||
return MessageVerificationAcceptContent(
|
|
||||||
hash,
|
|
||||||
keyAgreementProtocol,
|
|
||||||
messageAuthenticationCode,
|
|
||||||
shortAuthenticationStrings,
|
|
||||||
RelationDefaultContent(
|
|
||||||
RelationType.REFERENCE,
|
|
||||||
tid
|
|
||||||
),
|
|
||||||
commitment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MessageVerificationCancelContent(
|
|
||||||
@Json(name = "code") override val code: String? = null,
|
|
||||||
@Json(name = "reason") override val reason: String? = null,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
|
||||||
) : VerificationInfoCancel {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent {
|
|
||||||
return MessageVerificationCancelContent(
|
|
||||||
reason.value,
|
|
||||||
reason.humanReadable,
|
|
||||||
RelationDefaultContent(
|
|
||||||
RelationType.REFERENCE,
|
|
||||||
transactionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationDoneContent(
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
|
||||||
) : VerificationInfo<ValidVerificationDone> {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent(): Content? = toContent()
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationDone? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationDone(
|
|
||||||
validTransactionId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ValidVerificationDone(
|
|
||||||
val transactionId: String
|
|
||||||
)
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationKeyContent(
|
|
||||||
/**
|
|
||||||
* The device’s ephemeral public key, as an unpadded base64 string.
|
|
||||||
*/
|
|
||||||
@Json(name = "key") override val key: String? = null,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
|
||||||
) : VerificationInfoKey {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
|
|
||||||
companion object : VerificationInfoKeyFactory {
|
|
||||||
|
|
||||||
override fun create(tid: String, pubKey: String): VerificationInfoKey {
|
|
||||||
return MessageVerificationKeyContent(
|
|
||||||
pubKey,
|
|
||||||
RelationDefaultContent(
|
|
||||||
RelationType.REFERENCE,
|
|
||||||
tid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationMacContent(
|
|
||||||
@Json(name = "mac") override val mac: Map<String, String>? = null,
|
|
||||||
@Json(name = "keys") override val keys: String? = null,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
|
||||||
) : VerificationInfoMac {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
|
|
||||||
companion object : VerificationInfoMacFactory {
|
|
||||||
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
|
|
||||||
return MessageVerificationMacContent(
|
|
||||||
mac,
|
|
||||||
keys,
|
|
||||||
RelationDefaultContent(
|
|
||||||
RelationType.REFERENCE,
|
|
||||||
tid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationReadyContent(
|
|
||||||
@Json(name = "from_device") override val fromDevice: String? = null,
|
|
||||||
@Json(name = "methods") override val methods: List<String>? = null,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
|
||||||
) : VerificationInfoReady {
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
|
|
||||||
companion object : MessageVerificationReadyFactory {
|
|
||||||
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
|
|
||||||
return MessageVerificationReadyContent(
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
methods = methods,
|
|
||||||
relatesTo = RelationDefaultContent(
|
|
||||||
RelationType.REFERENCE,
|
|
||||||
tid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MessageVerificationRequestContent(
|
|
||||||
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
|
|
||||||
@Json(name = "body") override val body: String,
|
|
||||||
@Json(name = "from_device") override val fromDevice: String?,
|
|
||||||
@Json(name = "methods") override val methods: List<String>,
|
|
||||||
@Json(name = "to") val toUserId: String,
|
|
||||||
@Json(name = "timestamp") override val timestamp: Long?,
|
|
||||||
@Json(name = "format") val format: String? = null,
|
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null,
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
|
||||||
// Not parsed, but set after, using the eventId
|
|
||||||
override val transactionId: String? = null
|
|
||||||
) : MessageContent, VerificationInfoRequest {
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class MessageVerificationStartContent(
|
|
||||||
@Json(name = "from_device") override val fromDevice: String?,
|
|
||||||
@Json(name = "hashes") override val hashes: List<String>?,
|
|
||||||
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>?,
|
|
||||||
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
|
|
||||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
|
|
||||||
@Json(name = "method") override val method: String?,
|
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
|
|
||||||
@Json(name = "secret") override val sharedSecret: String?
|
|
||||||
) : VerificationInfoStart {
|
|
||||||
|
|
||||||
override fun toCanonicalJson(): String {
|
|
||||||
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val transactionId: String?
|
|
||||||
get() = relatesTo?.eventId
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 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.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
|
||||||
import org.matrix.android.sdk.internal.database.RealmKeysUtils
|
|
||||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
|
||||||
import org.matrix.android.sdk.internal.di.UserMd5
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.session.cache.ClearCacheTask
|
|
||||||
import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Module
|
|
||||||
internal abstract class CryptoModule {
|
|
||||||
|
|
||||||
@Module
|
|
||||||
companion object {
|
|
||||||
internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@CryptoDatabase
|
|
||||||
@SessionScope
|
|
||||||
fun providesRealmConfiguration(
|
|
||||||
@SessionFilesDirectory directory: File,
|
|
||||||
@UserMd5 userMd5: String,
|
|
||||||
realmKeysUtils: RealmKeysUtils,
|
|
||||||
realmCryptoStoreMigration: RealmCryptoStoreMigration
|
|
||||||
): RealmConfiguration {
|
|
||||||
return RealmConfiguration.Builder()
|
|
||||||
.directory(directory)
|
|
||||||
.apply {
|
|
||||||
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
|
||||||
}
|
|
||||||
.name("crypto_store.realm")
|
|
||||||
.modules(RealmCryptoStoreModule())
|
|
||||||
.allowWritesOnUiThread(true)
|
|
||||||
.schemaVersion(realmCryptoStoreMigration.schemaVersion)
|
|
||||||
.migration(realmCryptoStoreMigration)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@SessionScope
|
|
||||||
fun providesCryptoCoroutineScope(coroutineDispatchers: MatrixCoroutineDispatchers): CoroutineScope {
|
|
||||||
return CoroutineScope(SupervisorJob() + coroutineDispatchers.crypto)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@CryptoDatabase
|
|
||||||
fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask {
|
|
||||||
return RealmClearCacheTask(realmConfiguration)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@SessionScope
|
|
||||||
fun providesCryptoAPI(retrofit: Retrofit): CryptoApi {
|
|
||||||
return retrofit.create(CryptoApi::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Provides
|
|
||||||
@SessionScope
|
|
||||||
fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi {
|
|
||||||
return retrofit.create(RoomKeysApi::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindKeysBackupService(service: DefaultKeysBackupService): KeysBackupService
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindVerificationService(service: DefaultVerificationService): VerificationService
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindInitalizeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class DecryptRoomEventUseCase @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend operator fun invoke(event: Event, requestKeysOnFail: Boolean = true): MXEventDecryptionResult {
|
|
||||||
if (event.roomId.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
|
||||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
|
|
||||||
if (encryptedEventContent.senderKey.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.sessionId.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.ciphertext.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val olmDecryptionResult = olmDevice.decryptGroupMessage(
|
|
||||||
encryptedEventContent.ciphertext,
|
|
||||||
event.roomId,
|
|
||||||
"",
|
|
||||||
eventId = event.eventId.orEmpty(),
|
|
||||||
encryptedEventContent.sessionId,
|
|
||||||
encryptedEventContent.senderKey
|
|
||||||
)
|
|
||||||
if (olmDecryptionResult.payload != null) {
|
|
||||||
return MXEventDecryptionResult(
|
|
||||||
clearEvent = olmDecryptionResult.payload,
|
|
||||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
|
||||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
|
||||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
|
||||||
.orEmpty(),
|
|
||||||
messageVerificationState = olmDecryptionResult.verificationState
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
if (throwable is MXCryptoError.OlmError) {
|
|
||||||
// TODO Check the value of .message
|
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
|
||||||
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
|
||||||
// Check if partially withheld
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
|
||||||
"UNKNOWN_MESSAGE_INDEX",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
|
||||||
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.OLM,
|
|
||||||
reason,
|
|
||||||
detailedReason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (throwable is MXCryptoError.Base) {
|
|
||||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
|
||||||
// Check if it was withheld by sender to enrich error code
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestKeysForEvent(event: Event) {
|
|
||||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun decryptAndSaveResult(event: Event) {
|
|
||||||
tryOrNull(message = "Unable to decrypt the event") {
|
|
||||||
invoke(event)
|
|
||||||
}
|
|
||||||
?.let { result ->
|
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
|
||||||
payload = result.clearEvent,
|
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
|
||||||
verificationState = result.messageVerificationState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,603 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.extensions.measureMetric
|
|
||||||
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// Legacy name: MXDeviceList
|
|
||||||
@SessionScope
|
|
||||||
internal class DeviceListManager @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val syncTokenStore: SyncTokenStore,
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
|
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
|
||||||
coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val clock: Clock,
|
|
||||||
matrixConfiguration: MatrixConfiguration
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val metricPlugins = matrixConfiguration.metricPlugins
|
|
||||||
|
|
||||||
interface UserDevicesUpdateListener {
|
|
||||||
fun onUsersDeviceUpdate(userIds: List<String>)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
|
|
||||||
|
|
||||||
fun addListener(listener: UserDevicesUpdateListener) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.add(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeListener(listener: UserDevicesUpdateListener) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.remove(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchDeviceChange(users: List<String>) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onUsersDeviceUpdate(users)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure, "Failed to dispatch device change")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HS not ready for retry
|
|
||||||
private val notReadyToRetryHS = mutableSetOf<String>()
|
|
||||||
|
|
||||||
private val cryptoCoroutineContext = coroutineDispatchers.crypto
|
|
||||||
|
|
||||||
// Reset in progress status in case of restart
|
|
||||||
suspend fun recover() {
|
|
||||||
withContext(cryptoCoroutineContext) {
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
for ((userId, status) in deviceTrackingStatuses) {
|
|
||||||
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
|
|
||||||
// if a download was in progress when we got shut down, it isn't any more.
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells if the key downloads should be tried.
|
|
||||||
*
|
|
||||||
* @param userId the userId
|
|
||||||
* @return true if the keys download can be retrieved
|
|
||||||
*/
|
|
||||||
private fun canRetryKeysDownload(userId: String): Boolean {
|
|
||||||
var res = false
|
|
||||||
|
|
||||||
if (':' in userId) {
|
|
||||||
try {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the unavailable server lists.
|
|
||||||
*/
|
|
||||||
private fun clearUnavailableServersList() {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
notReadyToRetryHS.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRoomMembersLoadedFor(roomId: String) {
|
|
||||||
cryptoCoroutineScope.launch(cryptoCoroutineContext) {
|
|
||||||
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
|
|
||||||
// It's OK to track also device for invited users
|
|
||||||
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
|
|
||||||
startTrackingDeviceList(userIds)
|
|
||||||
refreshOutdatedDeviceLists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the cached device list for the given user outdated
|
|
||||||
* flag the given user for device-list tracking, if they are not already.
|
|
||||||
*
|
|
||||||
* @param userIds the user ids list
|
|
||||||
*/
|
|
||||||
fun startTrackingDeviceList(userIds: List<String>) {
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
for (userId in userIds) {
|
|
||||||
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
|
|
||||||
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the devices list statuses.
|
|
||||||
*
|
|
||||||
* @param changed the user ids list which have new devices
|
|
||||||
* @param left the user ids list which left a room
|
|
||||||
*/
|
|
||||||
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
|
|
||||||
Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
if (changed.isNotEmpty() || left.isNotEmpty()) {
|
|
||||||
clearUnavailableServersList()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (userId in changed) {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
|
||||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (userId in left) {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
|
||||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will flag each user whose devices we are tracking as in need of an update.
|
|
||||||
*/
|
|
||||||
fun invalidateAllDeviceLists() {
|
|
||||||
handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The keys download failed.
|
|
||||||
*
|
|
||||||
* @param userIds the user ids list
|
|
||||||
*/
|
|
||||||
private fun onKeysDownloadFailed(userIds: List<String>) {
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The keys download succeeded.
|
|
||||||
*
|
|
||||||
* @param userIds the userIds list
|
|
||||||
* @param failures the failure map.
|
|
||||||
*/
|
|
||||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
if (failures != null) {
|
|
||||||
for ((k, value) in failures) {
|
|
||||||
val statusCode = when (val status = value["status"]) {
|
|
||||||
is Double -> status.toInt()
|
|
||||||
is Int -> status.toInt()
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
if (statusCode == 503) {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
notReadyToRetryHS.add(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)
|
|
||||||
if (null == devices) {
|
|
||||||
if (canRetryKeysDownload(userId)) {
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
Timber.e("failed to retry the devices of $userId : retry later")
|
|
||||||
} else {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER
|
|
||||||
Timber.e("failed to retry the devices of $userId : the HS is not available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
|
||||||
// we didn't get any new invalidations since this download started:
|
|
||||||
// this user's device list is now up to date.
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE
|
|
||||||
Timber.v("Device list for $userId now up to date")
|
|
||||||
}
|
|
||||||
// And the response result
|
|
||||||
usersDevicesInfoMap.setObjects(userId, devices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
|
|
||||||
dispatchDeviceChange(userIds)
|
|
||||||
return usersDevicesInfoMap
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the device keys for a list of users and stores the keys in the MXStore.
|
|
||||||
* It must be called in getEncryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param userIds The users to fetch.
|
|
||||||
* @param forceDownload Always download the keys even if cached.
|
|
||||||
*/
|
|
||||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
|
|
||||||
// Map from userId -> deviceId -> MXDeviceInfo
|
|
||||||
val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
|
|
||||||
// List of user ids we need to download keys for
|
|
||||||
val downloadUsers = ArrayList<String>()
|
|
||||||
if (null != userIds) {
|
|
||||||
if (forceDownload) {
|
|
||||||
downloadUsers.addAll(userIds)
|
|
||||||
} else {
|
|
||||||
for (userId in userIds) {
|
|
||||||
val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED)
|
|
||||||
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
|
|
||||||
// not yet retrieved
|
|
||||||
if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) {
|
|
||||||
downloadUsers.add(userId)
|
|
||||||
} else {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)
|
|
||||||
// should always be true
|
|
||||||
if (devices != null) {
|
|
||||||
stored.setObjects(userId, devices)
|
|
||||||
} else {
|
|
||||||
downloadUsers.add(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (downloadUsers.isEmpty()) {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : no new user device")
|
|
||||||
stored
|
|
||||||
} else {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
|
||||||
val t0 = clock.epochMillis()
|
|
||||||
try {
|
|
||||||
val result = doKeyDownloadForUsers(downloadUsers)
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
|
||||||
result.also {
|
|
||||||
it.addEntriesFromMap(stored)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
|
|
||||||
if (forceDownload) {
|
|
||||||
throw failure
|
|
||||||
} else {
|
|
||||||
stored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the devices keys for a set of users.
|
|
||||||
*
|
|
||||||
* @param downloadUsers the user ids list
|
|
||||||
*/
|
|
||||||
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
|
|
||||||
// get the user ids which did not already trigger a keys download
|
|
||||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
|
||||||
if (filteredUsers.isEmpty()) {
|
|
||||||
// trigger nothing
|
|
||||||
return MXUsersDevicesMap()
|
|
||||||
}
|
|
||||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
|
||||||
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
|
|
||||||
|
|
||||||
val response: KeysQueryResponse
|
|
||||||
relevantPlugins.measureMetric {
|
|
||||||
response = try {
|
|
||||||
downloadKeysForUsersTask.execute(params)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
|
||||||
if (throwable is CancellationException) {
|
|
||||||
// the crypto module is getting closed, so we cannot access the DB anymore
|
|
||||||
Timber.w("The crypto module is closed, ignoring this error")
|
|
||||||
} else {
|
|
||||||
onKeysDownloadFailed(filteredUsers)
|
|
||||||
}
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
|
||||||
}
|
|
||||||
|
|
||||||
val userDataToStore = UserDataToStore()
|
|
||||||
|
|
||||||
for (userId in filteredUsers) {
|
|
||||||
// al devices =
|
|
||||||
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
|
|
||||||
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
|
|
||||||
if (!models.isNullOrEmpty()) {
|
|
||||||
val workingCopy = models.toMutableMap()
|
|
||||||
for ((deviceId, deviceInfo) in models) {
|
|
||||||
// Get the potential previously store device keys for this device
|
|
||||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
|
|
||||||
|
|
||||||
// in some race conditions (like unit tests)
|
|
||||||
// the self device must be seen as verified
|
|
||||||
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
|
|
||||||
deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
|
|
||||||
}
|
|
||||||
// Validate received keys
|
|
||||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
|
||||||
// New device keys are not valid. Do not store them
|
|
||||||
workingCopy.remove(deviceId)
|
|
||||||
if (null != previouslyStoredDeviceKeys) {
|
|
||||||
// But keep old validated ones if any
|
|
||||||
workingCopy[deviceId] = previouslyStoredDeviceKeys
|
|
||||||
}
|
|
||||||
} else if (null != previouslyStoredDeviceKeys) {
|
|
||||||
// The verified status is not sync'ed with hs.
|
|
||||||
// This is a client side information, valid only for this client.
|
|
||||||
// So, transfer its previous value
|
|
||||||
workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the store
|
|
||||||
// Note that devices which aren't in the response will be removed from the stores
|
|
||||||
userDataToStore.userDevices[userId] = workingCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
userDataToStore.userIdentities[userId] = UserIdentity(
|
|
||||||
masterKey = masterKey,
|
|
||||||
selfSigningKey = selfSigningKey,
|
|
||||||
userSigningKey = userSigningKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.storeData(userDataToStore)
|
|
||||||
|
|
||||||
// Update devices trust for these users
|
|
||||||
// dispatchDeviceChange(downloadUsers)
|
|
||||||
|
|
||||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate device keys.
|
|
||||||
* This method must called on getEncryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param deviceKeys the device keys to validate.
|
|
||||||
* @param userId the id of the user of the device.
|
|
||||||
* @param deviceId the id of the device.
|
|
||||||
* @param previouslyStoredDeviceKeys the device keys we received before for this device
|
|
||||||
* @return true if succeeds
|
|
||||||
*/
|
|
||||||
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
|
|
||||||
if (null == deviceKeys) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == deviceKeys.keys) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == deviceKeys.signatures) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the user_id and device_id in the received deviceKeys are correct
|
|
||||||
if (deviceKeys.userId != userId) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceKeys.deviceId != deviceId) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signKeyId = "ed25519:" + deviceKeys.deviceId
|
|
||||||
val signKey = deviceKeys.keys[signKeyId]
|
|
||||||
|
|
||||||
if (null == signKey) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signatureMap = deviceKeys.signatures[userId]
|
|
||||||
|
|
||||||
if (null == signatureMap) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signature = signatureMap[signKeyId]
|
|
||||||
|
|
||||||
if (null == signature) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var isVerified = false
|
|
||||||
var errorMessage: String? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature)
|
|
||||||
isVerified = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isVerified) {
|
|
||||||
Timber.e(
|
|
||||||
"## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" +
|
|
||||||
deviceKeys.deviceId + " with error " + errorMessage
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != previouslyStoredDeviceKeys) {
|
|
||||||
if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
|
|
||||||
// This should only happen if the list has been MITMed; we are
|
|
||||||
// best off sticking with the original keys.
|
|
||||||
//
|
|
||||||
// Should we warn the user about it somehow?
|
|
||||||
Timber.e(
|
|
||||||
"## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" +
|
|
||||||
deviceKeys.deviceId + " has changed : " +
|
|
||||||
previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey
|
|
||||||
)
|
|
||||||
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start device queries for any users who sent us an m.new_device recently
|
|
||||||
* This method must be called on getEncryptingThreadHandler() thread.
|
|
||||||
*/
|
|
||||||
suspend fun refreshOutdatedDeviceLists() {
|
|
||||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
|
||||||
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (users.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the statuses
|
|
||||||
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
|
|
||||||
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
runCatching {
|
|
||||||
doKeyDownloadForUsers(users)
|
|
||||||
}.fold(
|
|
||||||
{
|
|
||||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State transition diagram for DeviceList.deviceTrackingStatus.
|
|
||||||
* <pre>
|
|
||||||
*
|
|
||||||
* |
|
|
||||||
* stopTrackingDeviceList V
|
|
||||||
* +---------------------> NOT_TRACKED
|
|
||||||
* | |
|
|
||||||
* +<--------------------+ | startTrackingDeviceList
|
|
||||||
* | | V
|
|
||||||
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
|
|
||||||
* | | ^ | | |
|
|
||||||
* | | restart download | | start download | | invalidateUserDeviceList
|
|
||||||
* | | client failed | | | |
|
|
||||||
* | | | V | |
|
|
||||||
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
|
|
||||||
* | | | |
|
|
||||||
* +<-------------------+ | download successful |
|
|
||||||
* ^ V |
|
|
||||||
* +----------------------- UP_TO_DATE ------------------------+
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
const val TRACKING_STATUS_NOT_TRACKED = -1
|
|
||||||
const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
|
|
||||||
const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
|
|
||||||
const val TRACKING_STATUS_UP_TO_DATE = 3
|
|
||||||
const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,283 +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 kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class EventDecryptor @Inject constructor(
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val clock: Clock,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate limit unwedge attempt, should we persist that?
|
|
||||||
*/
|
|
||||||
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
|
|
||||||
|
|
||||||
data class WedgedDeviceInfo(
|
|
||||||
val userId: String,
|
|
||||||
val senderKey: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
private val wedgedMutex = Mutex()
|
|
||||||
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an event.
|
|
||||||
*
|
|
||||||
* @param event the raw event.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
return internalDecryptEvent(event, timeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an event and save the result in the given event.
|
|
||||||
*
|
|
||||||
* @param event the raw event.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
*/
|
|
||||||
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
|
|
||||||
// event is not encrypted or already decrypted
|
|
||||||
if (event.getClearType() != EventType.ENCRYPTED) return
|
|
||||||
|
|
||||||
tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") {
|
|
||||||
decryptEvent(event, timeline)
|
|
||||||
}
|
|
||||||
?.let { result ->
|
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
|
||||||
payload = result.clearEvent,
|
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
|
||||||
verificationState = result.messageVerificationState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an event asynchronously.
|
|
||||||
*
|
|
||||||
* @param event the raw event.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
* @param callback the callback to return data or null
|
|
||||||
*/
|
|
||||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
|
||||||
// is it needed to do that on the crypto scope??
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
runCatching {
|
|
||||||
internalDecryptEvent(event, timeline)
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an event.
|
|
||||||
*
|
|
||||||
* @param event the raw event.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
* @return the MXEventDecryptionResult data, or null in case of error
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
val eventContent = event.content
|
|
||||||
if (eventContent == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
|
||||||
} else if (event.isRedacted()) {
|
|
||||||
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
|
|
||||||
return MXEventDecryptionResult(
|
|
||||||
clearEvent = mapOf(
|
|
||||||
"room_id" to event.roomId.orEmpty(),
|
|
||||||
"type" to EventType.MESSAGE,
|
|
||||||
"content" to emptyMap<String, Any>(),
|
|
||||||
"unsigned" to event.unsignedData.toContent()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val algorithm = eventContent["algorithm"]?.toString()
|
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
|
||||||
if (alg == null) {
|
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
|
||||||
Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return alg.decryptEvent(event, timeline)
|
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
|
||||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
|
||||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
|
||||||
if (mxCryptoError is MXCryptoError.Base &&
|
|
||||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
|
||||||
// need to find sending device
|
|
||||||
val olmContent = event.content.toModel<OlmEventContent>()
|
|
||||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
|
||||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw mxCryptoError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
|
|
||||||
wedgedMutex.withLock {
|
|
||||||
val info = WedgedDeviceInfo(senderId, senderKey)
|
|
||||||
if (!wedgedDevices.contains(info)) {
|
|
||||||
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
|
|
||||||
wedgedDevices.add(info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coroutineDispatchers.crypto scope
|
|
||||||
suspend fun unwedgeDevicesIfNeeded() {
|
|
||||||
// handle wedged devices
|
|
||||||
// Some olm decryption have failed and some device are wedged
|
|
||||||
// we should force start a new session for those
|
|
||||||
Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
|
|
||||||
// get the one that should be retried according to rate limit
|
|
||||||
val now = clock.epochMillis()
|
|
||||||
val toUnwedge = wedgedMutex.withLock {
|
|
||||||
wedgedDevices.filter {
|
|
||||||
val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
|
|
||||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
|
|
||||||
return@filter false
|
|
||||||
}
|
|
||||||
// let's already mark that we tried now
|
|
||||||
lastNewSessionForcedDates[it] = now
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toUnwedge.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).v("Nothing to unwedge")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
|
|
||||||
|
|
||||||
toUnwedge
|
|
||||||
.chunked(100) // safer to chunk if we ever have lots of wedged devices
|
|
||||||
.forEach { wedgedList ->
|
|
||||||
val groupedByUserId = wedgedList.groupBy { it.userId }
|
|
||||||
// lets download keys if needed
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the matching devices
|
|
||||||
groupedByUserId
|
|
||||||
.map { groupedByUser ->
|
|
||||||
val userId = groupedByUser.key
|
|
||||||
val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
|
|
||||||
val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
|
|
||||||
knownDevices.firstOrNull { it.identityKey() == senderKey }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
.let { deviceList ->
|
|
||||||
try {
|
|
||||||
// force creating new outbound session and mark them as most recent to
|
|
||||||
// be used for next encryption (dummy)
|
|
||||||
val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
|
|
||||||
|
|
||||||
// Now send a dummy message on that session so the other side knows about it.
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.DUMMY
|
|
||||||
)
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sessionToUse.map.values
|
|
||||||
.flatMap { it.values }
|
|
||||||
.map { it.deviceInfo }
|
|
||||||
.forEach { deviceInfo ->
|
|
||||||
Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now let's send that
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceList.values.flatten().forEach { deviceInfo ->
|
|
||||||
wedgedMutex.withLock {
|
|
||||||
wedgedDevices.removeAll {
|
|
||||||
it.senderKey == deviceInfo.identityKey() &&
|
|
||||||
it.userId == deviceInfo.userId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import android.util.LruCache
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal data class InboundGroupSessionHolder(
|
|
||||||
val wrapper: MXInboundMegolmSessionWrapper,
|
|
||||||
val mutex: Mutex = Mutex()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows to cache and batch store operations on inbound group session store.
|
|
||||||
* Because it is used in the decrypt flow, that can be called quite rapidly
|
|
||||||
*/
|
|
||||||
internal class InboundGroupSessionStore @Inject constructor(
|
|
||||||
private val store: IMXCryptoStore,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
|
||||||
) {
|
|
||||||
|
|
||||||
private data class CacheKey(
|
|
||||||
val sessionId: String,
|
|
||||||
val senderKey: String
|
|
||||||
)
|
|
||||||
|
|
||||||
private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
|
|
||||||
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
|
|
||||||
if (oldValue != null) {
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
|
||||||
// store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
|
|
||||||
oldValue.wrapper.session.releaseSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun clear() {
|
|
||||||
sessionCache.evictAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
|
|
||||||
val known = sessionCache[CacheKey(sessionId, senderKey)]
|
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}")
|
|
||||||
return known
|
|
||||||
?: store.getInboundGroupSession(sessionId, senderKey)?.also {
|
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
|
|
||||||
sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
|
|
||||||
}?.let {
|
|
||||||
InboundGroupSessionHolder(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
|
||||||
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
|
||||||
store.removeInboundGroupSession(sessionId, senderKey)
|
|
||||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
|
||||||
|
|
||||||
// release removed session
|
|
||||||
old.wrapper.session.releaseSession()
|
|
||||||
|
|
||||||
internalStoreGroupSession(new, sessionId, senderKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
|
||||||
Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
|
||||||
|
|
||||||
store.storeInboundGroupSessions(
|
|
||||||
listOf(
|
|
||||||
old.wrapper.copy(
|
|
||||||
sessionData = old.wrapper.sessionData.copy(trusted = true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// will release it :/
|
|
||||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
|
||||||
internalStoreGroupSession(holder, sessionId, senderKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
|
|
||||||
|
|
||||||
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
|
||||||
// first time seen, put it in memory cache while waiting for batch insert
|
|
||||||
// If it's already known, no need to update cache it's already there
|
|
||||||
sessionCache.put(CacheKey(sessionId, senderKey), holder)
|
|
||||||
}
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
store.storeInboundGroupSessions(listOf(holder.wrapper))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,465 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class IncomingKeyRequestManager @Inject constructor(
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
|
||||||
val sequencer = SemaphoreCoroutineSequencer()
|
|
||||||
|
|
||||||
private val incomingRequestBuffer = mutableListOf<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.stable,
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class MyDeviceInfoHolder @Inject constructor(
|
|
||||||
// The credentials,
|
|
||||||
credentials: Credentials,
|
|
||||||
// the crypto store
|
|
||||||
cryptoStore: IMXCryptoStore,
|
|
||||||
// Olm device
|
|
||||||
olmDevice: MXOlmDevice
|
|
||||||
) {
|
|
||||||
// Our device keys
|
|
||||||
/**
|
|
||||||
* my device info.
|
|
||||||
*/
|
|
||||||
val myDevice: CryptoDeviceInfo
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
val keys = HashMap<String, String>()
|
|
||||||
|
|
||||||
// TODO it's a bit strange, why not load from DB?
|
|
||||||
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
|
||||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
|
|
||||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
|
||||||
}
|
|
||||||
|
|
||||||
// myDevice.keys = keys
|
|
||||||
//
|
|
||||||
// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
|
||||||
|
|
||||||
// TODO hwo to really check cross signed status?
|
|
||||||
//
|
|
||||||
val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false
|
|
||||||
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
|
|
||||||
|
|
||||||
myDevice = CryptoDeviceInfo(
|
|
||||||
credentials.deviceId,
|
|
||||||
credentials.userId,
|
|
||||||
keys = keys,
|
|
||||||
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
|
|
||||||
trustLevel = DeviceTrustLevel(crossSigned, true)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add our own deviceinfo to the store
|
|
||||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
|
||||||
|
|
||||||
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
|
|
||||||
|
|
||||||
myDevices[myDevice.deviceId] = myDevice
|
|
||||||
|
|
||||||
cryptoStore.storeUserDevices(credentials.userId, myDevices)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class ObjectSigner @Inject constructor(
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val olmDevice: MXOlmDevice
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign Object.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* <pre>
|
|
||||||
* {
|
|
||||||
* "[MY_USER_ID]": {
|
|
||||||
* "ed25519:[MY_DEVICE_ID]": "sign(str)"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param strToSign the String to sign and to include in the Map
|
|
||||||
* @return a Map (see example)
|
|
||||||
*/
|
|
||||||
fun signObject(strToSign: String): Map<String, Map<String, String>> {
|
|
||||||
val result = HashMap<String, Map<String, String>>()
|
|
||||||
|
|
||||||
val content = HashMap<String, String>()
|
|
||||||
|
|
||||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
|
|
||||||
?: "" // null reported by rageshake if happens during logout
|
|
||||||
|
|
||||||
result[credentials.userId] = content
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.olm.OlmSession
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep the used olm session in memory and load them from the data layer when needed.
|
|
||||||
* Access is synchronized for thread safety.
|
|
||||||
*/
|
|
||||||
internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
|
|
||||||
/**
|
|
||||||
* Map of device key to list of olm sessions (it is possible to have several active sessions with a device).
|
|
||||||
*/
|
|
||||||
private val olmSessions = HashMap<String, MutableList<OlmSessionWrapper>>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a session between our own device and another device.
|
|
||||||
* This will be called after the session has been created but also every time it has been used
|
|
||||||
* in order to persist the correct state for next run
|
|
||||||
* @param olmSessionWrapper the end-to-end session.
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
|
|
||||||
// This could be a newly created session or one that was just created
|
|
||||||
// Anyhow we should persist ratchet state for future app lifecycle
|
|
||||||
addNewSessionInCache(olmSessionWrapper, deviceKey)
|
|
||||||
store.storeSession(olmSessionWrapper, deviceKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the Olm Sessions we are sharing with the given device.
|
|
||||||
*
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return A set of sessionId, or empty if device is not known
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun getDeviceSessionIds(deviceKey: String): List<String> {
|
|
||||||
// we need to get the persisted ids first
|
|
||||||
val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
|
|
||||||
.orEmpty()
|
|
||||||
.toMutableList()
|
|
||||||
// Do we have some in cache not yet persisted?
|
|
||||||
olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
|
|
||||||
getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
|
|
||||||
if (!persistedKnownSessions.contains(cachedSessionId)) {
|
|
||||||
// as it's in cache put in on top
|
|
||||||
persistedKnownSessions.add(0, cachedSessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return persistedKnownSessions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an end-to-end session between our own device and another
|
|
||||||
* device.
|
|
||||||
*
|
|
||||||
* @param sessionId the session Id.
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return the session wrapper if found
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
|
||||||
// get from cache or load and add to cache
|
|
||||||
return internalGetSession(sessionId, deviceKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist.
|
|
||||||
*
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return last used sessionId, or null if not found
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun getLastUsedSessionId(deviceKey: String): String? {
|
|
||||||
// We want to avoid to load in memory old session if possible
|
|
||||||
val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
|
|
||||||
var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
|
|
||||||
// we should check if we have one in cache with a higher last message received?
|
|
||||||
olmSessions[deviceKey].orEmpty().forEach { inCache ->
|
|
||||||
if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
|
|
||||||
candidate = inCache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidate?.olmSession?.sessionIdentifier()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release all sessions and clear cache.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun clear() {
|
|
||||||
olmSessions.entries.onEach { entry ->
|
|
||||||
entry.value.onEach { it.olmSession.releaseSession() }
|
|
||||||
}
|
|
||||||
olmSessions.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
|
||||||
return getSessionInCache(sessionId, deviceKey)
|
|
||||||
?: // deserialize from store
|
|
||||||
return store.getDeviceSession(sessionId, deviceKey)?.also {
|
|
||||||
addNewSessionInCache(it, deviceKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
|
||||||
return olmSessions[deviceKey]?.firstOrNull {
|
|
||||||
getSafeSessionIdentifier(it.olmSession) == sessionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSafeSessionIdentifier(session: OlmSession): String? {
|
|
||||||
return try {
|
|
||||||
session.sessionIdentifier()
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
|
|
||||||
val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
|
|
||||||
olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
|
|
||||||
val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
|
|
||||||
it.add(session)
|
|
||||||
// remove and release if was there but with different instance
|
|
||||||
if (existing != null && existing.olmSession != session.olmSession) {
|
|
||||||
// mm not sure when this could happen
|
|
||||||
// anyhow we should remove and release the one known
|
|
||||||
it.remove(existing)
|
|
||||||
existing.olmSession.releaseSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,250 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import org.matrix.olm.OlmAccount
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.floor
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
// The spec recommend a 5mn delay, but due to federation
|
|
||||||
// or server downtime we give it a bit more time (1 hour)
|
|
||||||
private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class OneTimeKeysUploader @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val objectSigner: ObjectSigner,
|
|
||||||
private val uploadKeysTask: UploadKeysTask,
|
|
||||||
private val clock: Clock,
|
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
// tell if there is a OTK check in progress
|
|
||||||
private var oneTimeKeyCheckInProgress = false
|
|
||||||
|
|
||||||
// last OTK check timestamp
|
|
||||||
private var lastOneTimeKeyCheck: Long = 0
|
|
||||||
private var oneTimeKeyCount: Int? = null
|
|
||||||
|
|
||||||
// Simple storage to remember when was uploaded the last fallback key
|
|
||||||
private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the current one_time_key count which will be handled later (in a call of
|
|
||||||
* _onSyncCompleted). The count is e.g. coming from a /sync response.
|
|
||||||
*
|
|
||||||
* @param currentCount the new count
|
|
||||||
*/
|
|
||||||
fun updateOneTimeKeyCount(currentCount: Int) {
|
|
||||||
oneTimeKeyCount = currentCount
|
|
||||||
}
|
|
||||||
|
|
||||||
fun needsNewFallback() {
|
|
||||||
if (olmDevice.generateFallbackKeyIfNeeded()) {
|
|
||||||
// As we generated a new one, it's already forgetting one
|
|
||||||
// so we can clear the last publish time
|
|
||||||
// (in case the network calls fails after to avoid calling forgetKey)
|
|
||||||
saveLastFallbackKeyPublishTime(0L)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the OTK must be uploaded.
|
|
||||||
*/
|
|
||||||
suspend fun maybeUploadOneTimeKeys() {
|
|
||||||
if (oneTimeKeyCheckInProgress) {
|
|
||||||
Timber.v("maybeUploadOneTimeKeys: already in progress")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
|
||||||
// we've done a key upload recently.
|
|
||||||
Timber.v("maybeUploadOneTimeKeys: executed too recently")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oneTimeKeyCheckInProgress = true
|
|
||||||
|
|
||||||
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
|
||||||
?: fetchOtkCount() // we don't have count from sync so get from server
|
|
||||||
?: return Unit.also {
|
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server")
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}")
|
|
||||||
|
|
||||||
lastOneTimeKeyCheck = clock.epochMillis()
|
|
||||||
|
|
||||||
// We then check how many keys we can store in the Account object.
|
|
||||||
val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys()
|
|
||||||
|
|
||||||
// Try to keep at most half that number on the server. This leaves the
|
|
||||||
// rest of the slots free to hold keys that have been claimed from the
|
|
||||||
// server but we haven't received a message for.
|
|
||||||
// If we run out of slots when generating new keys then olm will
|
|
||||||
// discard the oldest private keys first. This will eventually clean
|
|
||||||
// out stale private keys that won't receive a message.
|
|
||||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
|
||||||
|
|
||||||
// We need to keep a pool of one time public keys on the server so that
|
|
||||||
// other devices can start conversations with us. But we can only store
|
|
||||||
// a finite number of private keys in the olm Account object.
|
|
||||||
// To complicate things further then can be a delay between a device
|
|
||||||
// claiming a public one time key from the server and it sending us a
|
|
||||||
// message. We need to keep the corresponding private key locally until
|
|
||||||
// we receive the message.
|
|
||||||
// But that message might never arrive leaving us stuck with duff
|
|
||||||
// private keys clogging up our local storage.
|
|
||||||
// So we need some kind of engineering compromise to balance all of
|
|
||||||
// these factors.
|
|
||||||
tryOrNull("Unable to upload OTK") {
|
|
||||||
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
|
||||||
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
|
||||||
}
|
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
|
|
||||||
// Check if we need to forget a fallback key
|
|
||||||
val latestPublishedTime = getLastFallbackKeyPublishTime()
|
|
||||||
if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) {
|
|
||||||
// This should be called once you are reasonably certain that you will not receive any more messages
|
|
||||||
// that use the old fallback key
|
|
||||||
Timber.d("## forgetFallbackKey()")
|
|
||||||
olmDevice.forgetFallbackKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchOtkCount(): Int? {
|
|
||||||
return tryOrNull("Unable to get OTK count") {
|
|
||||||
val result = uploadKeysTask.execute(UploadKeysTask.Params(KeysUploadBody()))
|
|
||||||
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload some the OTKs.
|
|
||||||
*
|
|
||||||
* @param keyCount the key count
|
|
||||||
* @param keyLimit the limit
|
|
||||||
* @return the number of uploaded keys
|
|
||||||
*/
|
|
||||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
|
|
||||||
if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) {
|
|
||||||
// If we don't need to generate any more keys then we are done.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var keysThisLoop = 0
|
|
||||||
if (keyLimit > keyCount) {
|
|
||||||
// Creating keys can be an expensive operation so we limit the
|
|
||||||
// number we generate in one go to avoid blocking the application
|
|
||||||
// for too long.
|
|
||||||
keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
|
||||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed
|
|
||||||
val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey()
|
|
||||||
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
|
|
||||||
olmDevice.markKeysAsPublished()
|
|
||||||
if (hadUnpublishedFallbackKey) {
|
|
||||||
// It had an unpublished fallback key that was published just now
|
|
||||||
saveLastFallbackKeyPublishTime(clock.epochMillis())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
|
||||||
// Maybe upload other keys
|
|
||||||
return keysThisLoop +
|
|
||||||
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) +
|
|
||||||
(if (hadUnpublishedFallbackKey) 1 else 0)
|
|
||||||
} else {
|
|
||||||
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
|
||||||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveLastFallbackKeyPublishTime(timeMillis: Long) {
|
|
||||||
storage.edit().putLong("last_fb_key_publish", timeMillis).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLastFallbackKeyPublishTime(): Long {
|
|
||||||
return storage.getLong("last_fb_key_publish", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload curve25519 one time keys.
|
|
||||||
*/
|
|
||||||
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
|
|
||||||
val oneTimeJson = mutableMapOf<String, Any>()
|
|
||||||
|
|
||||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
|
||||||
|
|
||||||
curve25519Map.forEach { (key_id, value) ->
|
|
||||||
val k = mutableMapOf<String, Any>()
|
|
||||||
k["key"] = value
|
|
||||||
|
|
||||||
// the key is also signed
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
|
||||||
|
|
||||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
|
||||||
|
|
||||||
oneTimeJson["signed_curve25519:$key_id"] = k
|
|
||||||
}
|
|
||||||
|
|
||||||
val fallbackJson = mutableMapOf<String, Any>()
|
|
||||||
val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
|
||||||
fallbackCurve25519Map.forEach { (key_id, key) ->
|
|
||||||
val k = mutableMapOf<String, Any>()
|
|
||||||
k["key"] = key
|
|
||||||
k["fallback"] = true
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
|
||||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
|
||||||
|
|
||||||
fallbackJson["signed_curve25519:$key_id"] = k
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, we set the device id explicitly, as we may not be using the
|
|
||||||
// same one as used in login.
|
|
||||||
val uploadParams = UploadKeysTask.Params(
|
|
||||||
KeysUploadBody(
|
|
||||||
deviceKeys = null,
|
|
||||||
oneTimeKeys = oneTimeJson,
|
|
||||||
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return uploadKeysTask.executeRetry(uploadParams, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// max number of keys to upload at once
|
|
||||||
// Creating keys can be an expensive operation so we limit the
|
|
||||||
// number we generate in one go to avoid blocking the application
|
|
||||||
// for too long.
|
|
||||||
private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5
|
|
||||||
|
|
||||||
// frequency with which to check & upload one-time keys
|
|
||||||
private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,526 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.Stack
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for sending key requests to other devices when a message failed to decrypt.
|
|
||||||
* It's lifecycle is based on the sync pulse:
|
|
||||||
* - You can post queries for session, or report when you got a session
|
|
||||||
* - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices
|
|
||||||
* If a request failed it will be retried at the end of the next sync
|
|
||||||
*/
|
|
||||||
@SessionScope
|
|
||||||
internal class OutgoingKeyRequestManager @Inject constructor(
|
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
@UserId private val myUserId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
|
||||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
|
||||||
private val sequencer = SemaphoreCoroutineSequencer()
|
|
||||||
|
|
||||||
// We only have one active key request per session, so we don't request if it's already requested
|
|
||||||
// But it could make sense to check more the backup, as it's evolving.
|
|
||||||
// We keep a stack as we consider that the key requested last is more likely to be on screen?
|
|
||||||
private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack<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
|
|
||||||
?.session
|
|
||||||
?.firstKnownIndex
|
|
||||||
}
|
|
||||||
if (knownIndex != null && knownIndex <= request.fromIndex) {
|
|
||||||
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
|
|
||||||
Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request")
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to send the request
|
|
||||||
val toDeviceContent = RoomKeyShareRequest(
|
|
||||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
|
||||||
requestId = request.requestId,
|
|
||||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
|
||||||
body = request.requestBody
|
|
||||||
)
|
|
||||||
val contentMap = MXUsersDevicesMap<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,106 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class RoomDecryptorProvider @Inject constructor(
|
|
||||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
|
||||||
private val megolmDecryptionFactory: MXMegolmDecryptionFactory
|
|
||||||
) {
|
|
||||||
|
|
||||||
// A map from algorithm to MXDecrypting instance, for each room
|
|
||||||
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
|
|
||||||
|
|
||||||
private val newSessionListeners = ArrayList<NewSessionListener>()
|
|
||||||
|
|
||||||
fun addNewSessionListener(listener: NewSessionListener) {
|
|
||||||
if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener) {
|
|
||||||
newSessionListeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a decryptor for a given room and algorithm.
|
|
||||||
* If we already have a decryptor for the given room and algorithm, return
|
|
||||||
* it. Otherwise try to instantiate it.
|
|
||||||
*
|
|
||||||
* @param roomId the room id
|
|
||||||
* @param algorithm the crypto algorithm
|
|
||||||
* @return the decryptor
|
|
||||||
* // TODO Create another method for the case of roomId is null
|
|
||||||
*/
|
|
||||||
fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
|
||||||
// sanity check
|
|
||||||
if (algorithm.isNullOrEmpty()) {
|
|
||||||
Timber.e("## getRoomDecryptor() : null algorithm")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (roomId != null && roomId.isNotEmpty()) {
|
|
||||||
synchronized(roomDecryptors) {
|
|
||||||
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
|
|
||||||
val alg = decryptors[algorithm]
|
|
||||||
if (alg != null) {
|
|
||||||
return alg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
|
|
||||||
if (decryptingClass) {
|
|
||||||
val alg = when (algorithm) {
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply {
|
|
||||||
this.newSessionListener = object : NewSessionListener {
|
|
||||||
override fun onNewSession(roomId: String?, sessionId: String) {
|
|
||||||
// PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor
|
|
||||||
newSessionListeners.toList().forEach {
|
|
||||||
try {
|
|
||||||
it.onNewSession(roomId, sessionId)
|
|
||||||
} catch (ignore: Throwable) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> olmDecryptionFactory.create()
|
|
||||||
}
|
|
||||||
if (!roomId.isNullOrEmpty()) {
|
|
||||||
synchronized(roomDecryptors) {
|
|
||||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return alg
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
|
||||||
if (roomId == null || algorithm == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return roomDecryptors[roomId]?.get(algorithm)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class RoomEncryptorsStore @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
|
||||||
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
|
||||||
) {
|
|
||||||
|
|
||||||
// MXEncrypting instance for each room.
|
|
||||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
|
||||||
|
|
||||||
fun put(roomId: String, alg: IMXEncrypting) {
|
|
||||||
synchronized(roomEncryptors) {
|
|
||||||
roomEncryptors.put(roomId, alg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(roomId: String): IMXEncrypting? {
|
|
||||||
return synchronized(roomEncryptors) {
|
|
||||||
val cache = roomEncryptors[roomId]
|
|
||||||
if (cache != null) {
|
|
||||||
return@synchronized cache
|
|
||||||
} else {
|
|
||||||
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
|
||||||
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
alg?.let { roomEncryptors.put(roomId, it) }
|
|
||||||
return@synchronized alg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,344 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.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.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 ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.requestingDeviceId == credentials.deviceId) {
|
|
||||||
return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.v("handleSecretRequest() : Ignore request from self device")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.toBase64()
|
|
||||||
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.execute(params)
|
|
||||||
}
|
|
||||||
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 requestMissingSecrets() {
|
|
||||||
// quick implementation for backward compatibility with rust, will request all secrets to all own devices
|
|
||||||
val secretNames = listOf(MASTER_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)
|
|
||||||
|
|
||||||
secretNames.forEach { secretName ->
|
|
||||||
val toDeviceContent = SecretShareRequest(
|
|
||||||
requestingDeviceId = credentials.deviceId,
|
|
||||||
secretName = secretName,
|
|
||||||
requestId = createUniqueTxnId()
|
|
||||||
)
|
|
||||||
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
contentMap.setObject(credentials.userId, "*", toDeviceContent)
|
|
||||||
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
eventType = EventType.REQUEST_SECRET,
|
|
||||||
contentMap = contentMap
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(params)
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("Secret request sent for $secretName")
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("Failed to request secret $secretName")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
|
|
||||||
if (!toDevice.isEncrypted()) {
|
|
||||||
// secret send messages must be encrypted
|
|
||||||
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// no need to download keys, after a verification we already forced download
|
|
||||||
val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }
|
|
||||||
if (sendingDevice == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Was that sent by us?
|
|
||||||
if (sendingDevice.userId != credentials.userId) {
|
|
||||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sendingDevice.isVerified) {
|
|
||||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val secretContent = toDevice.getClearContent().toModel<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,182 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val ONE_TIME_KEYS_RETRY_COUNT = 3
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val ensureMutex = Mutex()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We want to synchronize a bit here, because we are iterating to check existing olm session and
|
|
||||||
* also adding some.
|
|
||||||
*/
|
|
||||||
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
|
|
||||||
ensureMutex.withLock {
|
|
||||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
|
||||||
val deviceList = devicesByUser.flatMap { it.value }
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
|
|
||||||
val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
|
|
||||||
if (force) {
|
|
||||||
// we take all devices and will query otk for them
|
|
||||||
devicesToCreateSessionWith.addAll(deviceList)
|
|
||||||
} else {
|
|
||||||
// only peek devices without active session
|
|
||||||
deviceList.forEach { deviceInfo ->
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
val userId = deviceInfo.userId
|
|
||||||
val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// is there a session that as been already used?
|
|
||||||
val sessionId = olmDevice.getSessionId(key)
|
|
||||||
if (sessionId.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
|
|
||||||
devicesToCreateSessionWith.add(deviceInfo)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
|
|
||||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
|
|
||||||
results.setObject(userId, deviceId, olmSessionResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesToCreateSessionWith.isEmpty()) {
|
|
||||||
// no session to create
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
|
|
||||||
devicesToCreateSessionWith.forEach {
|
|
||||||
setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's now claim one time keys
|
|
||||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim.map)
|
|
||||||
val oneTimeKeysForUsers = withContext(coroutineDispatchers.io) {
|
|
||||||
oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
|
|
||||||
}
|
|
||||||
val oneTimeKeys = MXUsersDevicesMap<MXKey>()
|
|
||||||
for ((userId, mapByUserId) in oneTimeKeysForUsers.oneTimeKeys.orEmpty()) {
|
|
||||||
for ((deviceId, deviceKey) in mapByUserId) {
|
|
||||||
val mxKey = MXKey.from(deviceKey)
|
|
||||||
if (mxKey != null) {
|
|
||||||
oneTimeKeys.setObject(userId, deviceId, mxKey)
|
|
||||||
} else {
|
|
||||||
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let now start olm session using the new otks
|
|
||||||
devicesToCreateSessionWith.forEach { deviceInfo ->
|
|
||||||
val userId = deviceInfo.userId
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
// Did we get an OTK
|
|
||||||
val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
|
|
||||||
if (oneTimeKey == null) {
|
|
||||||
Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
|
|
||||||
} else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
|
|
||||||
Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
|
|
||||||
} else {
|
|
||||||
val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
|
||||||
if (olmSessionId != null) {
|
|
||||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
|
|
||||||
results.setObject(userId, deviceId, olmSessionResult)
|
|
||||||
} else {
|
|
||||||
Timber
|
|
||||||
.tag(loggerTag.value)
|
|
||||||
.d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
|
||||||
var sessionId: String? = null
|
|
||||||
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
val signKeyId = "ed25519:$deviceId"
|
|
||||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
|
||||||
|
|
||||||
val fingerprint = deviceInfo.fingerprint()
|
|
||||||
if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) {
|
|
||||||
var isVerified = false
|
|
||||||
var errorMessage: String? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature)
|
|
||||||
isVerified = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).d(
|
|
||||||
e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," +
|
|
||||||
" signature:$signature fingerprint:$fingerprint"
|
|
||||||
)
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " +
|
|
||||||
" - signable json ${oneTimeKey.signalableJSONDictionary()}"
|
|
||||||
)
|
|
||||||
errorMessage = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check one-time key signature
|
|
||||||
if (isVerified) {
|
|
||||||
sessionId = deviceInfo.identityKey()?.let { identityKey ->
|
|
||||||
olmDevice.createOutboundSession(identityKey, oneTimeKey.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionId.isNullOrEmpty()) {
|
|
||||||
// Possibly a bad key
|
|
||||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionId
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class EnsureOlmSessionsForUsersAction @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to make sure we have established olm sessions for the given users.
|
|
||||||
* @param users a list of user ids.
|
|
||||||
*/
|
|
||||||
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
|
|
||||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
|
||||||
val devicesByUser = users.associateWith { userId ->
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
|
|
||||||
devices.filter {
|
|
||||||
// Don't bother setting up session to ourself
|
|
||||||
it.identityKey() != olmDevice.deviceCurve25519Key &&
|
|
||||||
// Don't bother setting up sessions with blocked users
|
|
||||||
!(it.trustLevel?.isVerified() ?: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MegolmSessionDataImporter @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a list of megolm session keys.
|
|
||||||
* Must be call on the crypto coroutine thread
|
|
||||||
*
|
|
||||||
* @param megolmSessionsData megolm sessions.
|
|
||||||
* @param fromBackup true if the imported keys are already backed up on the server.
|
|
||||||
* @param progressListener the progress listener
|
|
||||||
* @return import room keys result
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun handle(
|
|
||||||
megolmSessionsData: List<MegolmSessionData>,
|
|
||||||
fromBackup: Boolean,
|
|
||||||
progressListener: ProgressListener?
|
|
||||||
): ImportRoomKeysResult {
|
|
||||||
val t0 = clock.epochMillis()
|
|
||||||
val importedSession = mutableMapOf<String, MutableMap<String, MutableList<String>>>()
|
|
||||||
|
|
||||||
val totalNumbersOfKeys = megolmSessionsData.size
|
|
||||||
var lastProgress = 0
|
|
||||||
var totalNumbersOfImportedKeys = 0
|
|
||||||
|
|
||||||
progressListener?.onProgress(0, 100)
|
|
||||||
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)
|
|
||||||
|
|
||||||
megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
|
|
||||||
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)
|
|
||||||
|
|
||||||
if (null != decrypting) {
|
|
||||||
try {
|
|
||||||
val sessionId = megolmSessionData.sessionId ?: return@forEachIndexed
|
|
||||||
val senderKey = megolmSessionData.senderKey ?: return@forEachIndexed
|
|
||||||
val roomId = megolmSessionData.roomId ?: return@forEachIndexed
|
|
||||||
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
|
|
||||||
|
|
||||||
importedSession.getOrPut(roomId) { mutableMapOf() }
|
|
||||||
.getOrPut(senderKey) { mutableListOf() }
|
|
||||||
.add(sessionId)
|
|
||||||
totalNumbersOfImportedKeys++
|
|
||||||
|
|
||||||
// cancel any outstanding room key requests for this session
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
|
|
||||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
|
|
||||||
sessionId,
|
|
||||||
roomId,
|
|
||||||
senderKey,
|
|
||||||
tryOrNull {
|
|
||||||
olmInboundGroupSessionWrappers
|
|
||||||
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
|
|
||||||
?.session?.firstKnownIndex
|
|
||||||
?.toInt()
|
|
||||||
} ?: 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Have another go at decrypting events sent with this session
|
|
||||||
when (decrypting) {
|
|
||||||
is MXMegolmDecryption -> {
|
|
||||||
decrypting.onNewSession(megolmSessionData.roomId, senderKey, sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressListener != null) {
|
|
||||||
val progress = 100 * (cpt + 1) / totalNumbersOfKeys
|
|
||||||
|
|
||||||
if (lastProgress != progress) {
|
|
||||||
lastProgress = progress
|
|
||||||
|
|
||||||
progressListener.onProgress(progress, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not back up the key if it comes from a backup recovery
|
|
||||||
if (fromBackup) {
|
|
||||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
|
||||||
}
|
|
||||||
|
|
||||||
val t1 = clock.epochMillis()
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
|
||||||
|
|
||||||
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys, importedSession)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MessageEncrypter @Inject constructor(
|
|
||||||
@UserId
|
|
||||||
private val userId: String,
|
|
||||||
@DeviceId
|
|
||||||
private val deviceId: String?,
|
|
||||||
private val olmDevice: MXOlmDevice
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Encrypt an event payload for a list of devices.
|
|
||||||
* This method must be called from the getCryptoHandler() thread.
|
|
||||||
*
|
|
||||||
* @param payloadFields fields to include in the encrypted payload.
|
|
||||||
* @param deviceInfos list of device infos to encrypt for.
|
|
||||||
* @return the content for an m.room.encrypted event.
|
|
||||||
*/
|
|
||||||
suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
|
|
||||||
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
|
||||||
|
|
||||||
val payloadJson = payloadFields.toMutableMap()
|
|
||||||
|
|
||||||
payloadJson["sender"] = userId
|
|
||||||
payloadJson["sender_device"] = deviceId!!
|
|
||||||
|
|
||||||
// Include the Ed25519 key so that the recipient knows what
|
|
||||||
// device this message came from.
|
|
||||||
// We don't need to include the curve25519 key since the
|
|
||||||
// recipient will already know this from the olm headers.
|
|
||||||
// When combined with the device keys retrieved from the
|
|
||||||
// homeserver signed by the ed25519 key this proves that
|
|
||||||
// the curve25519 key and the ed25519 key are owned by
|
|
||||||
// the same device.
|
|
||||||
payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!)
|
|
||||||
|
|
||||||
val ciphertext = mutableMapOf<String, Any>()
|
|
||||||
|
|
||||||
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
|
|
||||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
|
||||||
|
|
||||||
if (!sessionId.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
|
|
||||||
|
|
||||||
payloadJson["recipient"] = deviceInfo.userId
|
|
||||||
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
|
||||||
|
|
||||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
|
||||||
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EncryptedMessage(
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_OLM,
|
|
||||||
senderKey = olmDevice.deviceCurve25519Key,
|
|
||||||
cipherText = ciphertext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class SetDeviceVerificationAction @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val defaultKeysBackupService: DefaultKeysBackupService
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
|
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
if (null == device) {
|
|
||||||
Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified != trustLevel.isVerified()) {
|
|
||||||
if (userId == this.userId) {
|
|
||||||
// If one of the user's own devices is being marked as verified / unverified,
|
|
||||||
// check the key backup status, since whether or not we use this depends on
|
|
||||||
// whether it has a signature from a verified device
|
|
||||||
defaultKeysBackupService.checkAndStartKeysBackup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.trustLevel != trustLevel) {
|
|
||||||
device.trustLevel = trustLevel
|
|
||||||
cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for decrypting data.
|
|
||||||
*/
|
|
||||||
internal interface IMXDecrypting {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an event.
|
|
||||||
*
|
|
||||||
* @param event the raw event.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
* @return the decryption information, or an error
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a key event.
|
|
||||||
*
|
|
||||||
* @param event the key event.
|
|
||||||
* @param defaultKeysBackupService the keys backup service
|
|
||||||
* @param forceAccept the keys backup service
|
|
||||||
*/
|
|
||||||
suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for encrypting data.
|
|
||||||
*/
|
|
||||||
internal interface IMXEncrypting {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt an event content according to the configuration of the room.
|
|
||||||
*
|
|
||||||
* @param eventContent the content of the event.
|
|
||||||
* @param eventType the type of the event.
|
|
||||||
* @param userIds the room members the event will be sent to.
|
|
||||||
* @return the encrypted content
|
|
||||||
*/
|
|
||||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
|
||||||
|
|
||||||
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
internal interface IMXGroupEncryption {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Megolm, each recipient maintains a record of the ratchet value which allows
|
|
||||||
* them to decrypt any messages sent in the session after the corresponding point
|
|
||||||
* in the conversation. If this value is compromised, an attacker can similarly
|
|
||||||
* decrypt past messages which were encrypted by a key derived from the
|
|
||||||
* compromised or subsequent ratchet values. This gives 'partial' forward
|
|
||||||
* secrecy.
|
|
||||||
*
|
|
||||||
* To mitigate this issue, the application should offer the user the option to
|
|
||||||
* discard historical conversations, by winding forward any stored ratchet values,
|
|
||||||
* or discarding sessions altogether.
|
|
||||||
*/
|
|
||||||
fun discardSessionKey()
|
|
||||||
|
|
||||||
suspend fun preshareKey(userIds: List<String>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-shares a session key with devices if the key has already been
|
|
||||||
* sent to them.
|
|
||||||
*
|
|
||||||
* @param groupSessionId The id of the outbound session to share.
|
|
||||||
* @param userId The id of the user who owns the target device.
|
|
||||||
* @param deviceId The id of the target device.
|
|
||||||
* @param senderKey The key of the originating device for the session.
|
|
||||||
*
|
|
||||||
* @return true in case of success
|
|
||||||
*/
|
|
||||||
suspend fun reshareKey(
|
|
||||||
groupSessionId: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
senderKey: String
|
|
||||||
): Boolean
|
|
||||||
}
|
|
|
@ -1,364 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXMegolmDecryption(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val myUserId: String,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>,
|
|
||||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
|
||||||
private val clock: Clock,
|
|
||||||
) : IMXDecrypting {
|
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
|
||||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
|
||||||
*/
|
|
||||||
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
return decryptEvent(event, timeline, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
|
||||||
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
|
|
||||||
if (event.roomId.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
|
||||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
|
|
||||||
if (encryptedEventContent.senderKey.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.sessionId.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.ciphertext.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
olmDevice.decryptGroupMessage(
|
|
||||||
encryptedEventContent.ciphertext,
|
|
||||||
event.roomId,
|
|
||||||
timeline,
|
|
||||||
eventId = event.eventId.orEmpty(),
|
|
||||||
encryptedEventContent.sessionId,
|
|
||||||
encryptedEventContent.senderKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.fold(
|
|
||||||
{ olmDecryptionResult ->
|
|
||||||
// the decryption succeeds
|
|
||||||
if (olmDecryptionResult.payload != null) {
|
|
||||||
MXEventDecryptionResult(
|
|
||||||
clearEvent = olmDecryptionResult.payload,
|
|
||||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
|
||||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
|
||||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
|
||||||
.orEmpty(),
|
|
||||||
messageVerificationState = olmDecryptionResult.verificationState,
|
|
||||||
).also {
|
|
||||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ throwable ->
|
|
||||||
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
|
|
||||||
if (throwable is MXCryptoError.OlmError) {
|
|
||||||
// TODO Check the value of .message
|
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
|
||||||
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
// Check if partially withheld
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
|
||||||
"UNKNOWN_MESSAGE_INDEX",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
|
||||||
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.OLM,
|
|
||||||
reason,
|
|
||||||
detailedReason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (throwable is MXCryptoError.Base) {
|
|
||||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
|
||||||
// Check if it was withheld by sender to enrich error code
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for the real decryptEvent and for _retryDecryption. If
|
|
||||||
* requestKeysOnFail is true, we'll send an m.room_key_request when we fail
|
|
||||||
* to decrypt the event due to missing megolm keys.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
*/
|
|
||||||
private fun requestKeysForEvent(event: Event) {
|
|
||||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a key event.
|
|
||||||
*
|
|
||||||
* @param event the key event.
|
|
||||||
* @param defaultKeysBackupService the keys backup service
|
|
||||||
* @param forceAccept if true will force to accept the forwarded key
|
|
||||||
*/
|
|
||||||
override suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
|
|
||||||
Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
|
|
||||||
var exportFormat = false
|
|
||||||
val roomKeyContent = event.getDecryptedContent()?.toModel<RoomKeyContent>() ?: return
|
|
||||||
|
|
||||||
val eventSenderKey: String = event.getSenderKey() ?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field")
|
|
||||||
}
|
|
||||||
|
|
||||||
// this device might not been downloaded now?
|
|
||||||
val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey)
|
|
||||||
|
|
||||||
lateinit var sessionInitiatorSenderKey: String
|
|
||||||
val trusted: Boolean
|
|
||||||
|
|
||||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
|
||||||
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
|
||||||
|
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) {
|
|
||||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>()
|
|
||||||
?: return
|
|
||||||
|
|
||||||
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
|
|
||||||
forwardingCurve25519KeyChain.addAll(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
forwardingCurve25519KeyChain.add(eventSenderKey)
|
|
||||||
|
|
||||||
exportFormat = true
|
|
||||||
sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
|
|
||||||
Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
|
|
||||||
|
|
||||||
// checking if was requested once.
|
|
||||||
// should we check if the request is sort of active?
|
|
||||||
val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest(
|
|
||||||
roomId = forwardedRoomKeyContent.roomId.orEmpty(),
|
|
||||||
sessionId = forwardedRoomKeyContent.sessionId.orEmpty(),
|
|
||||||
algorithm = forwardedRoomKeyContent.algorithm.orEmpty(),
|
|
||||||
senderKey = forwardedRoomKeyContent.senderKey.orEmpty(),
|
|
||||||
).isEmpty()
|
|
||||||
|
|
||||||
trusted = false
|
|
||||||
|
|
||||||
if (!forceAccept && wasNotRequested) {
|
|
||||||
// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty()
|
|
||||||
unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis())
|
|
||||||
// Ignore unsolicited
|
|
||||||
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check who sent the request, as we requested we have the device keys (no need to download)
|
|
||||||
val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey)
|
|
||||||
if (sessionThatIsSharing == null) {
|
|
||||||
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isOwnDevice = myUserId == sessionThatIsSharing.userId
|
|
||||||
val isDeviceVerified = sessionThatIsSharing.isVerified
|
|
||||||
val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey
|
|
||||||
|
|
||||||
val isLegitForward = (isOwnDevice && isDeviceVerified) ||
|
|
||||||
(!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator)
|
|
||||||
|
|
||||||
val shouldAcceptForward = forceAccept || isLegitForward
|
|
||||||
|
|
||||||
if (!shouldAcceptForward) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," +
|
|
||||||
" fromInitiator:$isFromSessionInitiator")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a m.room_key so safe
|
|
||||||
trusted = true
|
|
||||||
sessionInitiatorSenderKey = eventSenderKey
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
// inherit the claimed ed25519 key from the setup message
|
|
||||||
keysClaimed = event.getKeysClaimed().toMutableMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
|
||||||
val addSessionResult = olmDevice.addInboundGroupSession(
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
sessionKey = roomKeyContent.sessionKey,
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
senderKey = sessionInitiatorSenderKey,
|
|
||||||
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
|
||||||
keysClaimed = keysClaimed,
|
|
||||||
exportFormat = exportFormat,
|
|
||||||
sharedHistory = roomKeyContent.getSharedKey(),
|
|
||||||
trusted = trusted
|
|
||||||
).also {
|
|
||||||
Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it")
|
|
||||||
}
|
|
||||||
|
|
||||||
when (addSessionResult) {
|
|
||||||
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
|
|
||||||
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
|
|
||||||
else -> null
|
|
||||||
}?.let { index ->
|
|
||||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
|
||||||
outgoingKeyRequestManager.onRoomKeyForwarded(
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
algorithm = roomKeyContent.algorithm ?: "",
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
senderKey = sessionInitiatorSenderKey,
|
|
||||||
fromIndex = index,
|
|
||||||
fromDevice = fromDevice?.deviceId,
|
|
||||||
event = event
|
|
||||||
)
|
|
||||||
|
|
||||||
cryptoStore.saveIncomingForwardKeyAuditTrail(
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
senderKey = sessionInitiatorSenderKey,
|
|
||||||
algorithm = roomKeyContent.algorithm ?: "",
|
|
||||||
userId = event.senderId.orEmpty(),
|
|
||||||
deviceId = fromDevice?.deviceId.orEmpty(),
|
|
||||||
chainIndex = index.toLong()
|
|
||||||
)
|
|
||||||
|
|
||||||
// The index is used to decide if we cancel sent request or if we wait for a better key
|
|
||||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
|
||||||
|
|
||||||
onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
|
|
||||||
*/
|
|
||||||
private fun RoomKeyContent.getSharedKey(): Boolean {
|
|
||||||
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
|
|
||||||
return sharedHistory ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the some messages can be decrypted with a new session.
|
|
||||||
*
|
|
||||||
* @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
|
|
||||||
* @param senderKey the session sender key
|
|
||||||
* @param sessionId the session id
|
|
||||||
*/
|
|
||||||
fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
|
||||||
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
|
||||||
newSessionListener?.onNewSession(roomId, sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
@UserId private val myUserId: String,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val eventsManager: Lazy<StreamEventsManager>,
|
|
||||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
|
||||||
private val mxCryptoConfig: MXCryptoConfig,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(): MXMegolmDecryption {
|
|
||||||
return MXMegolmDecryption(
|
|
||||||
olmDevice = olmDevice,
|
|
||||||
myUserId = myUserId,
|
|
||||||
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
|
||||||
cryptoStore = cryptoStore,
|
|
||||||
liveEventManager = eventsManager,
|
|
||||||
unrequestedForwardManager = unrequestedForwardManager,
|
|
||||||
cryptoConfig = mxCryptoConfig,
|
|
||||||
clock = clock,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,613 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toDebugCount
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toDebugString
|
|
||||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXMegolmEncryption(
|
|
||||||
// The id of the room we will be sending to.
|
|
||||||
private val roomId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val defaultKeysBackupService: DefaultKeysBackupService,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val myUserId: String,
|
|
||||||
private val myDeviceId: String,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val clock: Clock,
|
|
||||||
) : IMXEncrypting, IMXGroupEncryption {
|
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
|
||||||
// that even if this is non-null, it may not be ready for use (in which
|
|
||||||
// case outboundSession.shareOperation will be non-null.)
|
|
||||||
private var outboundSession: MXOutboundSessionInfo? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// restore existing outbound session if any
|
|
||||||
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default rotation periods
|
|
||||||
// TODO Make it configurable via parameters
|
|
||||||
// Session rotation periods
|
|
||||||
private var sessionRotationPeriodMsgs: Int = 100
|
|
||||||
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
|
|
||||||
|
|
||||||
override suspend fun encryptEventContent(
|
|
||||||
eventContent: Content,
|
|
||||||
eventType: String,
|
|
||||||
userIds: List<String>
|
|
||||||
): Content {
|
|
||||||
val ts = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When using in-room messages and the room has encryption enabled,
|
|
||||||
* clients should ensure that encryption does not hinder the verification.
|
|
||||||
* For example, if the verification messages are encrypted, clients must ensure that all the recipient’s
|
|
||||||
* unverified devices receive the keys necessary to decrypt the messages,
|
|
||||||
* even if they would normally not be given the keys to decrypt messages in the room.
|
|
||||||
*/
|
|
||||||
val shouldSendToUnverified = isVerificationEvent(eventType, eventContent)
|
|
||||||
|
|
||||||
val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified)
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
|
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
|
|
||||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
|
||||||
|
|
||||||
return encryptContent(outboundSession, eventType, eventContent)
|
|
||||||
.also {
|
|
||||||
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
|
||||||
// annoyingly we have to serialize again the saved outbound session to store message index :/
|
|
||||||
// if not we would see duplicate message index errors
|
|
||||||
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
|
|
||||||
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isVerificationEvent(eventType: String, eventContent: Content) =
|
|
||||||
EventType.isVerificationEvent(eventType) ||
|
|
||||||
(eventType == EventType.MESSAGE &&
|
|
||||||
eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST)
|
|
||||||
|
|
||||||
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
|
||||||
// offload to computation thread
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
|
||||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
|
||||||
devices.forEach { userId, deviceId, withheldCode ->
|
|
||||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
|
||||||
}
|
|
||||||
}.groupBy(
|
|
||||||
{ it.second },
|
|
||||||
{ it.first }
|
|
||||||
).forEach { (code, targets) ->
|
|
||||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun discardSessionKey() {
|
|
||||||
outboundSession = null
|
|
||||||
olmDevice.discardOutboundGroupSessionForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun preshareKey(userIds: List<String>) {
|
|
||||||
val ts = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...")
|
|
||||||
val devices = getDevicesInRoom(userIds)
|
|
||||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
|
||||||
|
|
||||||
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare a new session.
|
|
||||||
*
|
|
||||||
* @return the session description
|
|
||||||
*/
|
|
||||||
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
|
|
||||||
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
|
|
||||||
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
|
|
||||||
|
|
||||||
val keysClaimedMap = mapOf(
|
|
||||||
"ed25519" to olmDevice.deviceEd25519Key!!
|
|
||||||
)
|
|
||||||
|
|
||||||
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
|
|
||||||
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
|
|
||||||
olmDevice.addInboundGroupSession(
|
|
||||||
sessionId = sessionId!!,
|
|
||||||
sessionKey = olmDevice.getSessionKey(sessionId)!!,
|
|
||||||
roomId = roomId,
|
|
||||||
senderKey = olmDevice.deviceCurve25519Key!!,
|
|
||||||
forwardingCurve25519KeyChain = emptyList(),
|
|
||||||
keysClaimed = keysClaimedMap,
|
|
||||||
exportFormat = false,
|
|
||||||
sharedHistory = sharedHistory,
|
|
||||||
trusted = true
|
|
||||||
)
|
|
||||||
|
|
||||||
cryptoCoroutineScope.launch {
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
return MXOutboundSessionInfo(
|
|
||||||
sessionId = sessionId,
|
|
||||||
sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore),
|
|
||||||
clock = clock,
|
|
||||||
sharedHistory = sharedHistory
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the outbound session.
|
|
||||||
*
|
|
||||||
* @param devicesInRoom the devices list
|
|
||||||
*/
|
|
||||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
|
|
||||||
Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId")
|
|
||||||
var session = outboundSession
|
|
||||||
if (session == null ||
|
|
||||||
// Need to make a brand new session?
|
|
||||||
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
|
|
||||||
// Is there a room history visibility change since the last outboundSession
|
|
||||||
cryptoStore.shouldShareHistory(roomId) != session.sharedHistory ||
|
|
||||||
// Determine if we have shared with anyone we shouldn't have
|
|
||||||
session.sharedWithTooManyDevices(devicesInRoom)) {
|
|
||||||
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
|
|
||||||
session = prepareNewSessionInRoom()
|
|
||||||
outboundSession = session
|
|
||||||
}
|
|
||||||
val safeSession = session
|
|
||||||
val shareMap = HashMap<String, MutableList<CryptoDeviceInfo>>()/* userId */
|
|
||||||
val userIds = devicesInRoom.userIds
|
|
||||||
for (userId in userIds) {
|
|
||||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
|
||||||
for (deviceId in deviceIds!!) {
|
|
||||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
|
||||||
if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, deviceInfo).found) {
|
|
||||||
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
|
||||||
devices.add(deviceInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size }
|
|
||||||
Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})")
|
|
||||||
shareKey(safeSession, shareMap)
|
|
||||||
return safeSession
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share the device key to a list of users.
|
|
||||||
*
|
|
||||||
* @param session the session info
|
|
||||||
* @param devicesByUsers the devices map
|
|
||||||
*/
|
|
||||||
private suspend fun shareKey(
|
|
||||||
session: MXOutboundSessionInfo,
|
|
||||||
devicesByUsers: Map<String, List<CryptoDeviceInfo>>
|
|
||||||
) {
|
|
||||||
// nothing to send, the task is done
|
|
||||||
if (devicesByUsers.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).v("shareKey() : nothing more to do")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
|
|
||||||
val subMap = HashMap<String, List<CryptoDeviceInfo>>()
|
|
||||||
var devicesCount = 0
|
|
||||||
for ((userId, devices) in devicesByUsers) {
|
|
||||||
subMap[userId] = devices
|
|
||||||
devicesCount += devices.size
|
|
||||||
if (devicesCount > 100) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
|
|
||||||
shareUserDevicesKey(session, subMap)
|
|
||||||
val remainingDevices = devicesByUsers - subMap.keys
|
|
||||||
shareKey(session, remainingDevices)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share the device keys of a an user.
|
|
||||||
*
|
|
||||||
* @param sessionInfo the session info
|
|
||||||
* @param devicesByUser the devices map
|
|
||||||
*/
|
|
||||||
private suspend fun shareUserDevicesKey(
|
|
||||||
sessionInfo: MXOutboundSessionInfo,
|
|
||||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>
|
|
||||||
) {
|
|
||||||
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
|
|
||||||
}
|
|
||||||
val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
|
|
||||||
|
|
||||||
val payload = mapOf(
|
|
||||||
"type" to EventType.ROOM_KEY,
|
|
||||||
"content" to mapOf(
|
|
||||||
"algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
"room_id" to roomId,
|
|
||||||
"session_id" to sessionInfo.sessionId,
|
|
||||||
"session_key" to sessionKey,
|
|
||||||
"chain_index" to chainIndex,
|
|
||||||
"org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
var t0 = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
|
|
||||||
|
|
||||||
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
Timber.tag(loggerTag.value).v(
|
|
||||||
"""shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms"""
|
|
||||||
.trimMargin()
|
|
||||||
)
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
var haveTargets = false
|
|
||||||
val userIds = results.userIds
|
|
||||||
val noOlmToNotify = mutableListOf<UserDevice>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devicesToShareWith = devicesByUser[userId]
|
|
||||||
for ((deviceID) in devicesToShareWith!!) {
|
|
||||||
val sessionResult = results.getObject(userId, deviceID)
|
|
||||||
if (sessionResult?.sessionId == null) {
|
|
||||||
// no session with this device, probably because there
|
|
||||||
// were no one-time keys.
|
|
||||||
|
|
||||||
// MSC 2399
|
|
||||||
// send withheld m.no_olm: an olm session could not be established.
|
|
||||||
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld")
|
|
||||||
noOlmToNotify.add(UserDevice(userId, deviceID))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
|
|
||||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
|
|
||||||
haveTargets = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the devices we have shared with to session.sharedWithDevices.
|
|
||||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
|
||||||
// share with), because we don't want to try to claim a one-time-key
|
|
||||||
// for dead devices on every message.
|
|
||||||
for ((_, devicesToShareWith) in devicesByUser) {
|
|
||||||
for (deviceInfo in devicesToShareWith) {
|
|
||||||
sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
|
||||||
// XXX is it needed to add it to the audit trail?
|
|
||||||
// For now decided that no, we are more interested by forward trail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (haveTargets) {
|
|
||||||
t0 = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
|
|
||||||
Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// What to do here...
|
|
||||||
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noOlmToNotify.isNotEmpty()) {
|
|
||||||
// XXX offload?, as they won't read the message anyhow?
|
|
||||||
notifyKeyWithHeld(
|
|
||||||
noOlmToNotify,
|
|
||||||
sessionInfo.sessionId,
|
|
||||||
olmDevice.deviceCurve25519Key,
|
|
||||||
WithHeldCode.NO_OLM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun notifyKeyWithHeld(
|
|
||||||
targets: List<UserDevice>,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String?,
|
|
||||||
code: WithHeldCode
|
|
||||||
) {
|
|
||||||
Timber.tag(loggerTag.value).d(
|
|
||||||
"notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" +
|
|
||||||
" ${targets.joinToString { "${it.userId}|${it.deviceId}" }}"
|
|
||||||
)
|
|
||||||
val withHeldContent = RoomKeyWithHeldContent(
|
|
||||||
roomId = roomId,
|
|
||||||
senderKey = senderKey,
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
sessionId = sessionId,
|
|
||||||
codeString = code.value,
|
|
||||||
fromDevice = myDeviceId
|
|
||||||
)
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
EventType.ROOM_KEY_WITHHELD.stable,
|
|
||||||
MXUsersDevicesMap<Any>().apply {
|
|
||||||
targets.forEach {
|
|
||||||
setObject(it.userId, it.deviceId, withHeldContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(params)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* process the pending encryptions.
|
|
||||||
*/
|
|
||||||
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content {
|
|
||||||
// Everything is in place, encrypt all pending events
|
|
||||||
val payloadJson = HashMap<String, Any>()
|
|
||||||
payloadJson["room_id"] = roomId
|
|
||||||
payloadJson["type"] = eventType
|
|
||||||
payloadJson["content"] = eventContent
|
|
||||||
|
|
||||||
// Get canonical Json from
|
|
||||||
|
|
||||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
|
||||||
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString)
|
|
||||||
|
|
||||||
val map = HashMap<String, Any>()
|
|
||||||
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
map["sender_key"] = olmDevice.deviceCurve25519Key!!
|
|
||||||
map["ciphertext"] = ciphertext!!
|
|
||||||
map["session_id"] = session.sessionId
|
|
||||||
|
|
||||||
// Include our device ID so that recipients can send us a
|
|
||||||
// m.new_device message if they don't have our session key.
|
|
||||||
map["device_id"] = myDeviceId
|
|
||||||
session.useCount++
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of devices which can encrypt data to.
|
|
||||||
* This method must be called in getDecryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param userIds the user ids whose devices must be checked.
|
|
||||||
* @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if
|
|
||||||
* such devices are blocked in crypto settings
|
|
||||||
*/
|
|
||||||
private suspend fun getDevicesInRoom(userIds: List<String>, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo {
|
|
||||||
// We are happy to use a cached version here: we assume that if we already
|
|
||||||
// have a list of the user's devices, then we already share an e2e room
|
|
||||||
// with them, which means that they will have announced any new devices via
|
|
||||||
// an m.new_device.
|
|
||||||
val keys = deviceListManager.downloadKeys(userIds, false)
|
|
||||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
|
|
||||||
cryptoStore.getBlockUnverifiedDevices(roomId)
|
|
||||||
|
|
||||||
val devicesInRoom = DeviceInRoomInfo()
|
|
||||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
|
|
||||||
for (userId in keys.userIds) {
|
|
||||||
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
|
||||||
for (deviceId in deviceIds) {
|
|
||||||
val deviceInfo = keys.getObject(userId, deviceId) ?: continue
|
|
||||||
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
|
|
||||||
// The device is not yet known by the user
|
|
||||||
unknownDevices.setObject(userId, deviceId, deviceInfo)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (deviceInfo.isBlocked) {
|
|
||||||
// Remove any blocked devices
|
|
||||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) {
|
|
||||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
|
|
||||||
// Don't bother sending to ourself
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unknownDevices.isEmpty) {
|
|
||||||
return devicesInRoom
|
|
||||||
} else {
|
|
||||||
throw MXCryptoError.UnknownDevice(unknownDevices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun reshareKey(
|
|
||||||
groupSessionId: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
senderKey: String
|
|
||||||
): Boolean {
|
|
||||||
Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
|
|
||||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
|
|
||||||
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
|
|
||||||
|
|
||||||
// Get the chain index of the key we previously sent this device
|
|
||||||
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
|
|
||||||
if (!wasSessionSharedWithUser.found) {
|
|
||||||
// This session was never shared with this user
|
|
||||||
// Send a room key with held
|
|
||||||
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// if found chain index should not be null
|
|
||||||
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
|
|
||||||
.also {
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: Null chain index")
|
|
||||||
}
|
|
||||||
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
|
||||||
val usersDeviceMap = try {
|
|
||||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
|
|
||||||
if (olmSessionResult?.sessionId == null) {
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
|
|
||||||
|
|
||||||
val sessionHolder = try {
|
|
||||||
olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val export = sessionHolder.mutex.withLock {
|
|
||||||
sessionHolder.wrapper.exportKeys()
|
|
||||||
} ?: return false.also {
|
|
||||||
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
|
||||||
"content" to export
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
return try {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
|
|
||||||
true
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws
|
|
||||||
override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
|
|
||||||
require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" }
|
|
||||||
Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
|
|
||||||
val userId = deviceInfo.userId
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
|
||||||
val usersDeviceMap = try {
|
|
||||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm")
|
|
||||||
// process anyway?
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
|
|
||||||
if (olmSessionResult?.sessionId == null) {
|
|
||||||
Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val export = inboundSessionWrapper.mutex.withLock {
|
|
||||||
inboundSessionWrapper.wrapper.exportKeys()
|
|
||||||
} ?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
|
||||||
"content" to export
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload =
|
|
||||||
withContext(coroutineDispatchers.computation) {
|
|
||||||
messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
}
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeviceInRoomInfo(
|
|
||||||
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
|
||||||
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UserDevice(
|
|
||||||
val userId: String,
|
|
||||||
val deviceId: String
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
|
||||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val defaultKeysBackupService: DefaultKeysBackupService,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
@DeviceId private val deviceId: String?,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
|
||||||
return MXMegolmEncryption(
|
|
||||||
roomId = roomId,
|
|
||||||
olmDevice = olmDevice,
|
|
||||||
defaultKeysBackupService = defaultKeysBackupService,
|
|
||||||
cryptoStore = cryptoStore,
|
|
||||||
deviceListManager = deviceListManager,
|
|
||||||
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,
|
|
||||||
myUserId = userId,
|
|
||||||
myDeviceId = deviceId!!,
|
|
||||||
sendToDeviceTask = sendToDeviceTask,
|
|
||||||
messageEncrypter = messageEncrypter,
|
|
||||||
warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository,
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
|
||||||
cryptoCoroutineScope = cryptoCoroutineScope,
|
|
||||||
clock = clock,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
internal class MXOutboundSessionInfo(
|
|
||||||
// The id of the session
|
|
||||||
val sessionId: String,
|
|
||||||
val sharedWithHelper: SharedWithHelper,
|
|
||||||
private val clock: Clock,
|
|
||||||
// When the session was created
|
|
||||||
private val creationTime: Long = clock.epochMillis(),
|
|
||||||
val sharedHistory: Boolean = false
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Number of times this session has been used
|
|
||||||
var useCount: Int = 0
|
|
||||||
|
|
||||||
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
|
||||||
var needsRotation = false
|
|
||||||
val sessionLifetime = clock.epochMillis() - creationTime
|
|
||||||
|
|
||||||
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
|
|
||||||
Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
|
|
||||||
needsRotation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return needsRotation
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if this session has been shared with devices which it shouldn't have been.
|
|
||||||
*
|
|
||||||
* @param devicesInRoom the devices map
|
|
||||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
|
||||||
*/
|
|
||||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
|
||||||
val sharedWithDevices = sharedWithHelper.sharedWithDevices()
|
|
||||||
val userIds = sharedWithDevices.userIds
|
|
||||||
|
|
||||||
for (userId in userIds) {
|
|
||||||
if (null == devicesInRoom.getUserDeviceIds(userId)) {
|
|
||||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val deviceIds = sharedWithDevices.getUserDeviceIds(userId)
|
|
||||||
|
|
||||||
for (deviceId in deviceIds!!) {
|
|
||||||
if (null == devicesInRoom.getObject(userId, deviceId)) {
|
|
||||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
|
|
||||||
internal class SharedWithHelper(
|
|
||||||
private val roomId: String,
|
|
||||||
private val sessionId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun sharedWithDevices(): MXUsersDevicesMap<Int> {
|
|
||||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markedSessionAsShared(deviceInfo: CryptoDeviceInfo, chainIndex: Int) {
|
|
||||||
cryptoStore.markedSessionAsShared(
|
|
||||||
roomId = roomId,
|
|
||||||
sessionId = sessionId,
|
|
||||||
userId = deviceInfo.userId,
|
|
||||||
deviceId = deviceInfo.deviceId,
|
|
||||||
deviceIdentityKey = deviceInfo.identityKey() ?: "",
|
|
||||||
chainIndex = chainIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class UnRequestedForwardManager @Inject constructor(
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
|
|
||||||
private val sequencer = SemaphoreCoroutineSequencer()
|
|
||||||
|
|
||||||
// For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups?
|
|
||||||
private val forwardedKeysPerRoom = mutableMapOf<String, MutableMap<String, MutableList<ForwardInfo>>>()
|
|
||||||
|
|
||||||
data class InviteInfo(
|
|
||||||
val roomId: String,
|
|
||||||
val fromMxId: String,
|
|
||||||
val timestamp: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ForwardInfo(
|
|
||||||
val event: Event,
|
|
||||||
val timestamp: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
// roomId, local timestamp of invite
|
|
||||||
private val recentInvites = mutableListOf<InviteInfo>()
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
try {
|
|
||||||
scope.cancel("User Terminate")
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.w(failure, "Failed to shutDown UnrequestedForwardManager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) {
|
|
||||||
Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp")
|
|
||||||
scope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) {
|
|
||||||
recentInvites.add(
|
|
||||||
InviteInfo(
|
|
||||||
roomId,
|
|
||||||
fromUserId,
|
|
||||||
localTimeStamp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) {
|
|
||||||
Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp")
|
|
||||||
scope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
val claimSenderId = event.senderId.orEmpty()
|
|
||||||
val senderKey = event.getSenderKey()
|
|
||||||
// we might want to download keys, as this user might not be known yet, cache is ok
|
|
||||||
val ownerMxId =
|
|
||||||
tryOrNull {
|
|
||||||
deviceListManager.downloadKeys(listOf(claimSenderId), false)
|
|
||||||
.map[claimSenderId]
|
|
||||||
?.values
|
|
||||||
?.firstOrNull { it.identityKey() == senderKey }
|
|
||||||
?.userId
|
|
||||||
}
|
|
||||||
// Not sure what to do if the device has been deleted? I can't proove the mxid
|
|
||||||
if (ownerMxId == null || claimSenderId != ownerMxId) {
|
|
||||||
Timber.w("Mismatch senderId between event and olm owner")
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
forwardedKeysPerRoom
|
|
||||||
.getOrPut(roomId) { mutableMapOf() }
|
|
||||||
.getOrPut(ownerMxId) { mutableListOf() }
|
|
||||||
.add(ForwardInfo(event, localTimeStamp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List<Event>) -> Unit) {
|
|
||||||
scope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
// Prune outdated invites
|
|
||||||
recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS }
|
|
||||||
val cleanUpEvents = mutableListOf<Pair<String, String>>()
|
|
||||||
forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) ->
|
|
||||||
senderIdToForwardMap.forEach { (senderId, eventList) ->
|
|
||||||
// is there a matching invite in a valid timewindow?
|
|
||||||
val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId }
|
|
||||||
if (matchingInvite != null) {
|
|
||||||
Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}")
|
|
||||||
|
|
||||||
eventList.filter {
|
|
||||||
abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS
|
|
||||||
}.map {
|
|
||||||
it.event
|
|
||||||
}.takeIf { it.isNotEmpty() }?.let {
|
|
||||||
Timber.w("Re-processing forwarded_room_key_event that was not requested after invite")
|
|
||||||
scope.launch {
|
|
||||||
handleForwards.invoke(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanUpEvents.add(roomId to senderId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanUpEvents.forEach { roomIdToSenderPair ->
|
|
||||||
forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
|
||||||
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXOlmDecryption(
|
|
||||||
// The olm device interface
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
// the matrix userId
|
|
||||||
private val userId: String
|
|
||||||
) :
|
|
||||||
IMXDecrypting {
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
|
||||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cipherText = olmEventContent.ciphertext ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val senderKey = olmEventContent.senderKey ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
|
||||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The message for myUser
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val message = messageAny as JsonDict
|
|
||||||
|
|
||||||
val decryptedPayload = decryptMessage(message, senderKey)
|
|
||||||
|
|
||||||
if (decryptedPayload == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
|
||||||
}
|
|
||||||
val payloadString = convertFromUTF8(decryptedPayload)
|
|
||||||
|
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
|
||||||
val payload = adapter.fromJson(payloadString)
|
|
||||||
|
|
||||||
if (payload == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.recipient.isNullOrBlank()) {
|
|
||||||
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.recipient != userId) {
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"## decryptEvent() : Event ${event.eventId}:" +
|
|
||||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
|
|
||||||
)
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_RECIPIENT,
|
|
||||||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
|
||||||
" property; cannot prevent unknown-key attack"
|
|
||||||
)
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ed25519 = recipientKeys["ed25519"]
|
|
||||||
|
|
||||||
if (ed25519 != olmDevice.deviceEd25519Key) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
|
|
||||||
MXCryptoError.BAD_RECIPIENT_KEY_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.sender.isNullOrBlank()) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.sender != event.senderId) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
|
||||||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.roomId != event.roomId) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_ROOM,
|
|
||||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val keys = olmPayloadContent.keys ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MXEventDecryptionResult(
|
|
||||||
clearEvent = payload,
|
|
||||||
senderCurve25519Key = senderKey,
|
|
||||||
claimedEd25519Key = keys["ed25519"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to decrypt an Olm message.
|
|
||||||
*
|
|
||||||
* @param message message object, with 'type' and 'body' fields.
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
|
|
||||||
* @return payload, if decrypted successfully.
|
|
||||||
*/
|
|
||||||
private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
|
||||||
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
|
||||||
|
|
||||||
val messageBody = message["body"] as? String ?: return null
|
|
||||||
val messageType = when (val typeAsVoid = message["type"]) {
|
|
||||||
is Double -> typeAsVoid.toInt()
|
|
||||||
is Int -> typeAsVoid
|
|
||||||
is Long -> typeAsVoid.toInt()
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try each session in turn
|
|
||||||
// decryptionErrors = {};
|
|
||||||
|
|
||||||
val isPreKey = messageType == 0
|
|
||||||
// we want to synchronize on prekey if not we could end up create two olm sessions
|
|
||||||
// Not very clear but it looks like the js-sdk for consistency
|
|
||||||
return if (isPreKey) {
|
|
||||||
olmDevice.mutex.withLock {
|
|
||||||
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reallyDecryptMessage(sessionIds: List<String>, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
|
|
||||||
Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
|
|
||||||
for (sessionId in sessionIds) {
|
|
||||||
val payload = try {
|
|
||||||
olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
|
|
||||||
} catch (throwable: Exception) {
|
|
||||||
// As we are trying one by one, we don't really care of the error here
|
|
||||||
Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != payload) {
|
|
||||||
Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
|
||||||
return payload
|
|
||||||
} else {
|
|
||||||
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
|
|
||||||
|
|
||||||
if (foundSession) {
|
|
||||||
// Decryption failed, but it was a prekey message matching this
|
|
||||||
// session, so it should have worked.
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageType != 0) {
|
|
||||||
// not a prekey message, so it should have matched an existing session, but it
|
|
||||||
// didn't work.
|
|
||||||
|
|
||||||
if (sessionIds.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// prekey message which doesn't match any existing sessions: make a new
|
|
||||||
// session.
|
|
||||||
// XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
|
|
||||||
Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey")
|
|
||||||
|
|
||||||
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
|
|
||||||
|
|
||||||
if (null == res) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
|
|
||||||
|
|
||||||
return res["payload"]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXOlmDecryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
@UserId private val userId: String
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(): MXOlmDecryption {
|
|
||||||
return MXOlmDecryption(
|
|
||||||
olmDevice,
|
|
||||||
userId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
|
|
||||||
internal class MXOlmEncryption(
|
|
||||||
private val roomId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction
|
|
||||||
) :
|
|
||||||
IMXEncrypting {
|
|
||||||
|
|
||||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
|
|
||||||
// pick the list of recipients based on the membership list.
|
|
||||||
//
|
|
||||||
// TODO there is a race condition here! What if a new user turns up
|
|
||||||
ensureSession(userIds)
|
|
||||||
val deviceInfos = ArrayList<CryptoDeviceInfo>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
for (device in devices) {
|
|
||||||
val key = device.identityKey()
|
|
||||||
if (key == olmDevice.deviceCurve25519Key) {
|
|
||||||
// Don't bother setting up session to ourself
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (device.isBlocked) {
|
|
||||||
// Don't bother setting up sessions with blocked users
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
deviceInfos.add(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageMap = mapOf(
|
|
||||||
"room_id" to roomId,
|
|
||||||
"type" to eventType,
|
|
||||||
"content" to eventContent
|
|
||||||
)
|
|
||||||
|
|
||||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
|
||||||
return messageMap.toContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that the session.
|
|
||||||
*
|
|
||||||
* @param users the user ids list
|
|
||||||
*/
|
|
||||||
private suspend fun ensureSession(users: List<String>) {
|
|
||||||
deviceListManager.downloadKeys(users, false)
|
|
||||||
ensureOlmSessionsForUsersAction.handle(users)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXOlmEncryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(roomId: String): MXOlmEncryption {
|
|
||||||
return MXOlmEncryption(
|
|
||||||
roomId,
|
|
||||||
olmDevice,
|
|
||||||
cryptoStore,
|
|
||||||
messageEncrypter,
|
|
||||||
deviceListManager,
|
|
||||||
ensureOlmSessionsForUsersAction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
|
|
||||||
data class Params(
|
|
||||||
val activeMemberUserIds: List<String>,
|
|
||||||
val isDirectRoom: Boolean
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultComputeTrustTask @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
|
||||||
) : ComputeTrustTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) {
|
|
||||||
// The set of “all users” depends on the type of room:
|
|
||||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
|
||||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
|
||||||
val listToCheck = if (params.isDirectRoom) {
|
|
||||||
params.activeMemberUserIds.filter { it != userId }
|
|
||||||
} else {
|
|
||||||
params.activeMemberUserIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val allTrustedUserIds = listToCheck
|
|
||||||
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
|
|
||||||
|
|
||||||
if (allTrustedUserIds.isEmpty()) {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
} else {
|
|
||||||
// If one of the verified user as an untrusted device -> warning
|
|
||||||
// If all devices of all verified users are trusted -> green
|
|
||||||
// else -> black
|
|
||||||
allTrustedUserIds
|
|
||||||
.mapNotNull { cryptoStore.getUserDeviceList(it) }
|
|
||||||
.flatten()
|
|
||||||
.let { allDevices ->
|
|
||||||
if (getMyCrossSigningKeys() != null) {
|
|
||||||
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
|
||||||
} else {
|
|
||||||
// Legacy method
|
|
||||||
allDevices.any { !it.isVerified }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.let { hasWarning ->
|
|
||||||
if (hasWarning) {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
} else {
|
|
||||||
if (listToCheck.size == allTrustedUserIds.size) {
|
|
||||||
// all users are trusted and all devices are verified
|
|
||||||
RoomEncryptionTrustLevel.Trusted
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getCrossSigningInfo(otherUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getMyCrossSigningInfo()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.olm.OlmPkSigning
|
|
||||||
import org.matrix.olm.OlmUtility
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the OlmPkSigning for cross signing.
|
|
||||||
* Can be injected without having to get the full cross signing service
|
|
||||||
*/
|
|
||||||
@SessionScope
|
|
||||||
internal class CrossSigningOlm @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
) {
|
|
||||||
|
|
||||||
enum class KeyType {
|
|
||||||
SELF,
|
|
||||||
USER,
|
|
||||||
MASTER
|
|
||||||
}
|
|
||||||
|
|
||||||
var olmUtility: OlmUtility = OlmUtility()
|
|
||||||
|
|
||||||
var masterPkSigning: OlmPkSigning? = null
|
|
||||||
var userPkSigning: OlmPkSigning? = null
|
|
||||||
var selfSigningPkSigning: OlmPkSigning? = null
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
olmUtility.releaseUtility()
|
|
||||||
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun signObject(type: KeyType, strToSign: String): Map<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")
|
|
||||||
|
|
||||||
require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" }
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Bob MSK is valid
|
|
||||||
olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,819 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.work.BackoffPolicy
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
|
||||||
import org.matrix.olm.OlmPkSigning
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class DefaultCrossSigningService @Inject constructor(
|
|
||||||
@UserId private val myUserId: String,
|
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
|
||||||
private val uploadSignaturesTask: UploadSignaturesTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val crossSigningOlm: CrossSigningOlm,
|
|
||||||
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
|
||||||
) : CrossSigningService,
|
|
||||||
DeviceListManager.UserDevicesUpdateListener {
|
|
||||||
|
|
||||||
init {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Try to get stored keys if they exist
|
|
||||||
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
|
|
||||||
Timber.i("## CrossSigning - Found Existing self signed keys")
|
|
||||||
Timber.i("## CrossSigning - Checking if private keys are known")
|
|
||||||
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
|
|
||||||
privateKeysInfo.master
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.masterPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public master key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privateKeysInfo.user
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.userPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading User Signing key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public User key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privateKeysInfo.selfSigned
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.selfSigningPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading Self Signing key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recover local trust in case private key are there?
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Mmm this kind of a big issue
|
|
||||||
Timber.e(e, "Failed to initialize Cross Signing")
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceListManager.addListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
crossSigningOlm.release()
|
|
||||||
deviceListManager.removeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun finalize() {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - Make 3 key pairs (MSK, USK, SSK)
|
|
||||||
* - Save the private keys with proper security
|
|
||||||
* - Sign the keys and upload them
|
|
||||||
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures.
|
|
||||||
*/
|
|
||||||
override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) {
|
|
||||||
Timber.d("## CrossSigning initializeCrossSigning")
|
|
||||||
|
|
||||||
val params = InitializeCrossSigningTask.Params(
|
|
||||||
interactiveAuthInterceptor = uiaInterceptor
|
|
||||||
)
|
|
||||||
val data = initializeCrossSigningTask
|
|
||||||
.execute(params)
|
|
||||||
val crossSigningInfo = MXCrossSigningInfo(
|
|
||||||
myUserId,
|
|
||||||
listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
|
||||||
setUserKeysAsTrusted(myUserId, true)
|
|
||||||
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
|
||||||
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
|
||||||
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
|
||||||
crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onSecretMSKGossip(mskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known")
|
|
||||||
}
|
|
||||||
|
|
||||||
mskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.masterPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.masterPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading MSK success")
|
|
||||||
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onSecretSSKGossip(sskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known")
|
|
||||||
}
|
|
||||||
|
|
||||||
sskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.selfSigningPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading SSK success")
|
|
||||||
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onSecretUSKGossip(uskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretUSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ")
|
|
||||||
}
|
|
||||||
|
|
||||||
uskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.userPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.userPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading USK success")
|
|
||||||
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkTrustFromPrivateKeys(
|
|
||||||
masterKeyPrivateKey: String?,
|
|
||||||
uskKeyPrivateKey: String?,
|
|
||||||
sskPrivateKey: String?
|
|
||||||
): UserTrustResult {
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
|
||||||
|
|
||||||
var masterKeyIsTrusted = false
|
|
||||||
var userKeyIsTrusted = false
|
|
||||||
var selfSignedKeyIsTrusted = false
|
|
||||||
|
|
||||||
masterKeyPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.masterPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.masterPkSigning = pkSigning
|
|
||||||
masterKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uskKeyPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.userPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.userPkSigning = pkSigning
|
|
||||||
userKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sskPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
|
|
||||||
crossSigningOlm.selfSigningPkSigning = pkSigning
|
|
||||||
selfSignedKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
|
|
||||||
return UserTrustResult.Failure("Keys not trusted $mxCrossSigningInfo") // UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
|
|
||||||
} else {
|
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
|
||||||
val checkSelfTrust = checkSelfTrust()
|
|
||||||
if (checkSelfTrust.isVerified()) {
|
|
||||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
|
|
||||||
setUserKeysAsTrusted(myUserId, true)
|
|
||||||
}
|
|
||||||
return checkSelfTrust
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶ MSK
|
|
||||||
* │
|
|
||||||
* │ │
|
|
||||||
* │ SSK │
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* └──▶ USK ────────────┘
|
|
||||||
* .
|
|
||||||
*/
|
|
||||||
override suspend fun isUserTrusted(otherUserId: String): Boolean {
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
|
||||||
cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isCrossSigningVerified(): Boolean {
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
|
||||||
checkSelfTrust().isVerified()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will not force a download of the key, but will verify signatures trust chain.
|
|
||||||
*/
|
|
||||||
override suspend fun checkUserTrust(otherUserId: String): UserTrustResult {
|
|
||||||
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
|
||||||
if (otherUserId == myUserId) {
|
|
||||||
return checkSelfTrust()
|
|
||||||
}
|
|
||||||
// I trust a user if I trust his master key
|
|
||||||
// I can trust the master key if it is signed by my user key
|
|
||||||
// TODO what if the master key is signed by a device key that i have verified
|
|
||||||
|
|
||||||
// First let's get my user key
|
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
|
|
||||||
|
|
||||||
return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
|
||||||
val myUserKey = myCrossSigningInfo?.userKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
|
||||||
|
|
||||||
if (!myCrossSigningInfo.isTrusted()) {
|
|
||||||
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's get the other user master key
|
|
||||||
val otherMasterKey = otherInfo?.masterKey()
|
|
||||||
?: return UserTrustResult.Failure("Unknown MSK for ${otherInfo?.userId}") // UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
|
||||||
|
|
||||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
|
||||||
?.get(myUserId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
|
|
||||||
return UserTrustResult.Failure("MSK not signed by my USK $otherMasterKey") // UserTrustResult.KeyNotSigned(otherMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Bob MSK is valid
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
|
||||||
masterKeySignaturesMadeByMyUserKey,
|
|
||||||
myUserKey.unpaddedBase64PublicKey,
|
|
||||||
otherMasterKey.canonicalSignable()
|
|
||||||
)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.Failure("Invalid signature $masterKeySignaturesMadeByMyUserKey") // UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkSelfTrust(): UserTrustResult {
|
|
||||||
// Special case when it's me,
|
|
||||||
// I have to check that MSK -> USK -> SSK
|
|
||||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
|
|
||||||
|
|
||||||
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
|
|
||||||
// Special case when it's me,
|
|
||||||
// I have to check that MSK -> USK -> SSK
|
|
||||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
|
||||||
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
|
||||||
|
|
||||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
|
||||||
|
|
||||||
// Is the master key trusted
|
|
||||||
// 1) check if I know the private key
|
|
||||||
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
|
|
||||||
?.master
|
|
||||||
?.fromBase64()
|
|
||||||
|
|
||||||
var isMaterKeyTrusted = false
|
|
||||||
if (myMasterKey.trustLevel?.locallyVerified == true) {
|
|
||||||
isMaterKeyTrusted = true
|
|
||||||
} else if (masterPrivateKey != null) {
|
|
||||||
// Check if private match public
|
|
||||||
var olmPkSigning: OlmPkSigning? = null
|
|
||||||
try {
|
|
||||||
olmPkSigning = OlmPkSigning()
|
|
||||||
val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey)
|
|
||||||
isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure)
|
|
||||||
}
|
|
||||||
olmPkSigning?.releaseSigning()
|
|
||||||
} else {
|
|
||||||
// Maybe it's signed by a locally trusted device?
|
|
||||||
myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) ->
|
|
||||||
val potentialDeviceId = key.removePrefix("ed25519:")
|
|
||||||
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
|
||||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
|
||||||
// Check signature validity?
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
|
|
||||||
isMaterKeyTrusted = true
|
|
||||||
return@forEach
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// log
|
|
||||||
Timber.w(failure, "Signature not valid?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMaterKeyTrusted) {
|
|
||||||
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
val myUserKey = myCrossSigningInfo.userKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
|
||||||
|
|
||||||
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
|
|
||||||
?.get(myUserId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK")
|
|
||||||
return UserTrustResult.Failure("USK not signed by MSK") // UserTrustResult.KeyNotSigned(myUserKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
|
||||||
userKeySignaturesMadeByMyMasterKey,
|
|
||||||
myMasterKey.unpaddedBase64PublicKey,
|
|
||||||
myUserKey.canonicalSignable()
|
|
||||||
)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.Failure("Invalid MSK signature of USK") // UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mySSKey = myCrossSigningInfo.selfSigningKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
|
|
||||||
|
|
||||||
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
|
|
||||||
?.get(myUserId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK")
|
|
||||||
return UserTrustResult.Failure("SSK not signed by MSK") // UserTrustResult.KeyNotSigned(mySSKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
|
||||||
ssKeySignaturesMadeByMyMasterKey,
|
|
||||||
myMasterKey.unpaddedBase64PublicKey,
|
|
||||||
mySSKey.canonicalSignable()
|
|
||||||
)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.Failure("Invalid signature $ssKeySignaturesMadeByMyMasterKey") // UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
|
||||||
cryptoStore.getCrossSigningInfo(otherUserId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
|
|
||||||
return cryptoStore.getLiveCrossSigningInfo(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
|
||||||
cryptoStore.getMyCrossSigningInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
|
||||||
return withContext(coroutineDispatchers.io) {
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
|
|
||||||
return cryptoStore.getLiveCrossSigningPrivateKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canCrossSign(): Boolean {
|
|
||||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null &&
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allPrivateKeysKnown(): Boolean {
|
|
||||||
return checkSelfTrust().isVerified() &&
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun trustUser(otherUserId: String) {
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
Timber.d("## CrossSigning - Mark user $otherUserId as trusted ")
|
|
||||||
// We should have this user keys
|
|
||||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
|
||||||
if (otherMasterKeys == null) {
|
|
||||||
throw Throwable("## CrossSigning - Other master signing key is not known")
|
|
||||||
}
|
|
||||||
val myKeys = getUserCrossSigningKeys(myUserId)
|
|
||||||
?: throw Throwable("## CrossSigning - CrossSigning is not setup for this account")
|
|
||||||
|
|
||||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
|
||||||
if (userPubKey == null || crossSigningOlm.userPkSigning == null) {
|
|
||||||
throw Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the other MasterKey with our UserSigning key
|
|
||||||
val newSignature = JsonCanonicalizer.getCanonicalJson(
|
|
||||||
Map::class.java,
|
|
||||||
otherMasterKeys.signalableJSONDictionary()
|
|
||||||
).let { crossSigningOlm.userPkSigning?.sign(it) }
|
|
||||||
?: // race??
|
|
||||||
throw Throwable("## CrossSigning - Failed to sign")
|
|
||||||
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
|
||||||
|
|
||||||
Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK")
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery))
|
|
||||||
|
|
||||||
// Local echo for device cross trust, to avoid having to wait for a notification of key change
|
|
||||||
cryptoStore.getUserDeviceList(otherUserId)?.forEach { device ->
|
|
||||||
val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
|
||||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
|
||||||
cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun markMyMasterKeyAsTrusted() {
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
|
||||||
checkSelfTrust()
|
|
||||||
// re-verify all trusts
|
|
||||||
onUsersDeviceUpdate(listOf(myUserId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun trustDevice(deviceId: String) {
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
// This device should be yours
|
|
||||||
val device = cryptoStore.getUserDevice(myUserId, deviceId)
|
|
||||||
?: throw IllegalArgumentException("This device [$deviceId] is not known, or not yours")
|
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(myUserId)
|
|
||||||
?: throw Throwable("CrossSigning is not setup for this account")
|
|
||||||
|
|
||||||
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
|
||||||
if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) {
|
|
||||||
throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign with self signing
|
|
||||||
val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable())
|
|
||||||
?: throw Throwable("Failed to sign")
|
|
||||||
|
|
||||||
val toUpload = device.copy(
|
|
||||||
signatures = mapOf(
|
|
||||||
myUserId
|
|
||||||
to
|
|
||||||
mapOf(
|
|
||||||
"ed25519:$ssPubKey" to newSignature
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withDeviceInfo(toUpload)
|
|
||||||
.build()
|
|
||||||
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun shieldForGroup(userIds: List<String>): RoomEncryptionTrustLevel {
|
|
||||||
// Not used in kotlin SDK?
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
|
|
||||||
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
|
|
||||||
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
|
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(myUserId)
|
|
||||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
|
|
||||||
|
|
||||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
|
||||||
|
|
||||||
val otherKeys = getUserCrossSigningKeys(otherUserId)
|
|
||||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId))
|
|
||||||
|
|
||||||
// TODO should we force verification ?
|
|
||||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
|
||||||
|
|
||||||
// Check if the trust chain is valid
|
|
||||||
/*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶MSK
|
|
||||||
* │
|
|
||||||
* │ │ │
|
|
||||||
* │ SSK │ └──▶ SSK ──────────────────┐
|
|
||||||
* │ │ │
|
|
||||||
* │ │ USK │
|
|
||||||
* └──▶ USK ────────────┘ (not visible by │
|
|
||||||
* Alice) │
|
|
||||||
* ▼
|
|
||||||
* ┌──────────────┐
|
|
||||||
* │ BOB's Device │
|
|
||||||
* └──────────────┘
|
|
||||||
*/
|
|
||||||
|
|
||||||
val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
|
||||||
?: return legacyFallbackTrust(
|
|
||||||
locallyTrusted,
|
|
||||||
DeviceTrustResult.MissingDeviceSignature(
|
|
||||||
otherDeviceId, otherKeys.selfSigningKey()
|
|
||||||
?.unpaddedBase64PublicKey
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check bob's device is signed by bob's SSK
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
|
||||||
otherSSKSignature,
|
|
||||||
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
|
|
||||||
otherDevice.canonicalSignable()
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
|
|
||||||
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
|
|
||||||
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
|
|
||||||
|
|
||||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
|
||||||
|
|
||||||
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
|
|
||||||
|
|
||||||
// TODO should we force verification ?
|
|
||||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
|
||||||
|
|
||||||
// Check if the trust chain is valid
|
|
||||||
/*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶MSK
|
|
||||||
* │
|
|
||||||
* │ │ │
|
|
||||||
* │ SSK │ └──▶ SSK ──────────────────┐
|
|
||||||
* │ │ │
|
|
||||||
* │ │ USK │
|
|
||||||
* └──▶ USK ────────────┘ (not visible by │
|
|
||||||
* Alice) │
|
|
||||||
* ▼
|
|
||||||
* ┌──────────────┐
|
|
||||||
* │ BOB's Device │
|
|
||||||
* └──────────────┘
|
|
||||||
*/
|
|
||||||
|
|
||||||
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
|
||||||
?: return legacyFallbackTrust(
|
|
||||||
locallyTrusted,
|
|
||||||
DeviceTrustResult.MissingDeviceSignature(
|
|
||||||
otherDevice.deviceId, otherKeys.selfSigningKey()
|
|
||||||
?.unpaddedBase64PublicKey
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check bob's device is signed by bob's SSK
|
|
||||||
try {
|
|
||||||
crossSigningOlm.olmUtility.verifyEd25519Signature(
|
|
||||||
otherSSKSignature,
|
|
||||||
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
|
|
||||||
otherDevice.canonicalSignable()
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
|
||||||
return if (locallyTrusted == true) {
|
|
||||||
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
|
||||||
} else {
|
|
||||||
crossSignTrustFail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
|
||||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
|
|
||||||
runBlocking {
|
|
||||||
checkTrustAndAffectedRoomShields(userIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkTrustAndAffectedRoomShields(userIds: List<String>) {
|
|
||||||
Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}")
|
|
||||||
val workerParams = UpdateTrustWorker.Params(
|
|
||||||
sessionId = sessionId,
|
|
||||||
filename = updateTrustWorkerDataRepository.createParam(userIds)
|
|
||||||
)
|
|
||||||
val workerData = WorkerParamsFactory.toData(workerParams)
|
|
||||||
|
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
|
|
||||||
.setInputData(workerData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
workManagerProvider.workManager
|
|
||||||
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
|
||||||
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
|
||||||
// If it's me, recheck trust of all users and devices?
|
|
||||||
val users = ArrayList<String>()
|
|
||||||
if (otherUserId == myUserId && currentTrust != trusted) {
|
|
||||||
// notify key requester
|
|
||||||
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
|
||||||
cryptoStore.updateUsersTrust {
|
|
||||||
users.add(it)
|
|
||||||
// called within a real transaction, has to block
|
|
||||||
runBlocking {
|
|
||||||
checkUserTrust(it).isVerified()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
users.forEach {
|
|
||||||
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
|
||||||
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
|
||||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
|
||||||
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
|
|
||||||
internal fun CryptoDeviceInfo.canonicalSignable(): String {
|
|
||||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun CryptoCrossSigningKey.canonicalSignable(): String {
|
|
||||||
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
|
|
||||||
}
|
|
|
@ -1,390 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.kotlin.where
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.awaitTransaction
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
|
||||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
|
||||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
|
|
||||||
SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class Params(
|
|
||||||
override val sessionId: String,
|
|
||||||
override val lastFailureMessage: String? = null,
|
|
||||||
// Kept for compatibility, but not used anymore (can be used for pending Worker)
|
|
||||||
val updatedUserIds: List<String>? = null,
|
|
||||||
// Passing a long list of userId can break the Work Manager due to data size limitation.
|
|
||||||
// so now we use a temporary file to store the data
|
|
||||||
val filename: String? = null
|
|
||||||
) : SessionWorkerParams
|
|
||||||
|
|
||||||
@Inject lateinit var crossSigningService: CrossSigningService
|
|
||||||
|
|
||||||
// It breaks the crypto store contract, but we need to batch things :/
|
|
||||||
@CryptoDatabase
|
|
||||||
@Inject lateinit var cryptoRealmConfiguration: RealmConfiguration
|
|
||||||
|
|
||||||
@SessionDatabase
|
|
||||||
@Inject lateinit var sessionRealmConfiguration: RealmConfiguration
|
|
||||||
|
|
||||||
@UserId
|
|
||||||
@Inject lateinit var myUserId: String
|
|
||||||
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
|
|
||||||
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
|
||||||
@Inject lateinit var cryptoSessionInfoProvider: CryptoSessionInfoProvider
|
|
||||||
|
|
||||||
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
|
|
||||||
// @Inject lateinit var cryptoStore: IMXCryptoStore
|
|
||||||
|
|
||||||
override fun injectWith(injector: SessionComponent) {
|
|
||||||
injector.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun doSafeWork(params: Params): Result {
|
|
||||||
val sId = myUserId.take(5)
|
|
||||||
Timber.v("## CrossSigning - UpdateTrustWorker started..")
|
|
||||||
val workerParams = params.filename
|
|
||||||
?.let { updateTrustWorkerDataRepository.getParam(it) }
|
|
||||||
?: return Result.success().also {
|
|
||||||
Timber.w("## CrossSigning - UpdateTrustWorker failed to get params")
|
|
||||||
cleanup(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker userIds:${workerParams.userIds.logLimit()}, roomIds:${workerParams.roomIds.orEmpty().logLimit()}")
|
|
||||||
val userList = workerParams.userIds
|
|
||||||
|
|
||||||
// List should not be empty, but let's avoid go further in case of empty list
|
|
||||||
if (userList.isNotEmpty()) {
|
|
||||||
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
|
||||||
// or a new device?) So we check all again :/
|
|
||||||
Timber.v("## CrossSigning [$sId]- Updating trust for users: ${userList.logLimit()}")
|
|
||||||
updateTrust(userList)
|
|
||||||
}
|
|
||||||
|
|
||||||
val roomsToCheck = workerParams.roomIds ?: cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userList)
|
|
||||||
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker roomShield to check:${roomsToCheck.logLimit()}")
|
|
||||||
var myCrossSigningInfo: MXCrossSigningInfo?
|
|
||||||
Realm.getInstance(cryptoRealmConfiguration).use { realm ->
|
|
||||||
myCrossSigningInfo = getCrossSigningInfo(realm, myUserId)
|
|
||||||
}
|
|
||||||
// So Cross Signing keys trust is updated, device trust is updated
|
|
||||||
// We can now update room shields? in the session DB?
|
|
||||||
updateRoomShieldInSummaries(roomsToCheck, myCrossSigningInfo)
|
|
||||||
|
|
||||||
cleanup(params)
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTrust(userListParam: List<String>) {
|
|
||||||
val sId = myUserId.take(5)
|
|
||||||
var userList = userListParam
|
|
||||||
var myCrossSigningInfo: MXCrossSigningInfo?
|
|
||||||
|
|
||||||
// First we check that the users MSK are trusted by mine
|
|
||||||
// After that we check the trust chain for each devices of each users
|
|
||||||
awaitTransaction(cryptoRealmConfiguration) { cryptoRealm ->
|
|
||||||
// By mapping here to model, this object is not live
|
|
||||||
// I should update it if needed
|
|
||||||
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
|
||||||
|
|
||||||
var myTrustResult: UserTrustResult? = null
|
|
||||||
|
|
||||||
if (userList.contains(myUserId)) {
|
|
||||||
Timber.d("## CrossSigning [$sId]- Clear all trust as a change on my user was detected")
|
|
||||||
// i am in the list.. but i don't know exactly the delta of change :/
|
|
||||||
// If it's my cross signing keys we should refresh all trust
|
|
||||||
// do it anyway ?
|
|
||||||
userList = cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.findAll()
|
|
||||||
.mapNotNull { it.userId }
|
|
||||||
|
|
||||||
// check right now my keys and mark it as trusted as other trust depends on it
|
|
||||||
val myDevices = cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, myUserId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
?.map { CryptoMapper.mapToModel(it) }
|
|
||||||
|
|
||||||
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices)
|
|
||||||
updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
|
|
||||||
// update model reference
|
|
||||||
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val otherInfos = userList.associateWith { userId ->
|
|
||||||
getCrossSigningInfo(cryptoRealm, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val trusts = otherInfos.mapValues { entry ->
|
|
||||||
when (entry.key) {
|
|
||||||
myUserId -> myTrustResult
|
|
||||||
else -> {
|
|
||||||
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
|
|
||||||
Timber.v("## CrossSigning [$sId]- user:${entry.key} result:$it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
|
|
||||||
// i have all the new trusts, update DB
|
|
||||||
trusts.forEach {
|
|
||||||
val verified = it.value?.isVerified() == true
|
|
||||||
Timber.v("[$myUserId] ## CrossSigning [$sId]- Updating user trust: ${it.key} to $verified")
|
|
||||||
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok so now we have to check device trust for all these users..
|
|
||||||
Timber.v("## CrossSigning [$sId]- Updating devices cross trust users: ${trusts.keys.logLimit()}")
|
|
||||||
trusts.keys.forEach { userId ->
|
|
||||||
val devicesEntities = cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
|
|
||||||
val trustMap = devicesEntities?.associateWith { device ->
|
|
||||||
runBlocking {
|
|
||||||
crossSigningService.checkDeviceTrust(userId, device.deviceId ?: "", CryptoMapper.mapToModel(device).trustLevel?.locallyVerified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update trust if needed
|
|
||||||
devicesEntities?.forEach { device ->
|
|
||||||
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
|
||||||
Timber.v("## CrossSigning [$sId]- Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
|
||||||
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
|
||||||
Timber.d("## CrossSigning [$sId]- Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
|
||||||
// need to save
|
|
||||||
val trustEntity = device.trustLevelEntity
|
|
||||||
if (trustEntity == null) {
|
|
||||||
device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
|
||||||
it.locallyVerified = false
|
|
||||||
it.crossSignedVerified = crossSignedVerified
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trustEntity.crossSignedVerified = crossSignedVerified
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.v("## CrossSigning [$sId]- Trust unchanged for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateRoomShieldInSummaries(roomList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
|
|
||||||
val sId = myUserId.take(5)
|
|
||||||
Timber.d("## CrossSigning [$sId]- Updating shields for impacted rooms... ${roomList.logLimit()}")
|
|
||||||
awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
|
|
||||||
Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction")
|
|
||||||
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
|
|
||||||
roomList.forEach { roomId ->
|
|
||||||
Timber.v("## CrossSigning [$sId]- Checking room $roomId")
|
|
||||||
RoomSummaryEntity.where(sessionRealm, roomId)
|
|
||||||
// .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
|
|
||||||
.findFirst()
|
|
||||||
?.let { roomSummary ->
|
|
||||||
Timber.v("## CrossSigning [$sId]- Check shield state for room $roomId")
|
|
||||||
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
|
|
||||||
try {
|
|
||||||
val updatedTrust = computeRoomShield(
|
|
||||||
myCrossSigningInfo,
|
|
||||||
cryptoRealm,
|
|
||||||
allActiveRoomMembers,
|
|
||||||
roomSummary
|
|
||||||
)
|
|
||||||
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
|
||||||
Timber.d("## CrossSigning [$sId]- Shield change detected for $roomId -> $updatedTrust")
|
|
||||||
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
|
||||||
} else {
|
|
||||||
Timber.v("## CrossSigning [$sId]- Shield unchanged for $roomId -> $updatedTrust")
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.d("## CrossSigning - Updating shields for impacted rooms - END")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
|
|
||||||
return cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.let { mapCrossSigningInfoEntity(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup(params: Params) {
|
|
||||||
params.filename
|
|
||||||
?.let { updateTrustWorkerDataRepository.delete(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) {
|
|
||||||
cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.let { userKeyInfo ->
|
|
||||||
userKeyInfo
|
|
||||||
.crossSigningKeys
|
|
||||||
.forEach { key ->
|
|
||||||
// optimization to avoid trigger updates when there is no change..
|
|
||||||
if (key.trustLevelEntity?.isVerified() != verified) {
|
|
||||||
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
|
||||||
val level = key.trustLevelEntity
|
|
||||||
if (level == null) {
|
|
||||||
key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
|
||||||
it.locallyVerified = verified
|
|
||||||
it.crossSignedVerified = verified
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
level.locallyVerified = verified
|
|
||||||
level.crossSignedVerified = verified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (verified) {
|
|
||||||
userKeyInfo.wasUserVerifiedOnce = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeRoomShield(
|
|
||||||
myCrossSigningInfo: MXCrossSigningInfo?,
|
|
||||||
cryptoRealm: Realm,
|
|
||||||
activeMemberUserIds: List<String>,
|
|
||||||
roomSummaryEntity: RoomSummaryEntity
|
|
||||||
): RoomEncryptionTrustLevel {
|
|
||||||
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
|
|
||||||
// The set of “all users” depends on the type of room:
|
|
||||||
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
|
|
||||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
|
||||||
val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) {
|
|
||||||
activeMemberUserIds.filter { it != myUserId }
|
|
||||||
} else {
|
|
||||||
activeMemberUserIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val allTrustedUserIds = listToCheck
|
|
||||||
.filter { userId ->
|
|
||||||
getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
|
|
||||||
}
|
|
||||||
|
|
||||||
val resetTrust = listToCheck
|
|
||||||
.filter { userId ->
|
|
||||||
val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId)
|
|
||||||
crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (allTrustedUserIds.isEmpty()) {
|
|
||||||
if (resetTrust.isEmpty()) {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If one of the verified user as an untrusted device -> warning
|
|
||||||
// If all devices of all verified users are trusted -> green
|
|
||||||
// else -> black
|
|
||||||
allTrustedUserIds
|
|
||||||
.mapNotNull { userId ->
|
|
||||||
cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
?.map { CryptoMapper.mapToModel(it) }
|
|
||||||
}
|
|
||||||
.flatten()
|
|
||||||
.let { allDevices ->
|
|
||||||
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}")
|
|
||||||
if (myCrossSigningInfo != null) {
|
|
||||||
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
|
||||||
} else {
|
|
||||||
// Legacy method
|
|
||||||
allDevices.any { !it.isVerified }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.let { hasWarning ->
|
|
||||||
if (hasWarning) {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
} else {
|
|
||||||
if (resetTrust.isEmpty()) {
|
|
||||||
if (listToCheck.size == allTrustedUserIds.size) {
|
|
||||||
// all users are trusted and all devices are verified
|
|
||||||
RoomEncryptionTrustLevel.Trusted
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
|
|
||||||
val userId = xsignInfo.userId ?: ""
|
|
||||||
return MXCrossSigningInfo(
|
|
||||||
userId = userId,
|
|
||||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
|
||||||
crossSigningKeysMapper.map(userId, it)
|
|
||||||
},
|
|
||||||
wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildErrorParams(params: Params, message: String): Params {
|
|
||||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent by Bob to accept a verification from a previously sent m.key.verification.start message.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationAccept(
|
|
||||||
/**
|
|
||||||
* string to identify the transaction.
|
|
||||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
|
|
||||||
* Alice’s device should record this ID and use it in future messages in this transaction.
|
|
||||||
*/
|
|
||||||
@Json(name = "transaction_id")
|
|
||||||
override val transactionId: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
@Json(name = "key_agreement_protocol")
|
|
||||||
override val keyAgreementProtocol: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
@Json(name = "hash")
|
|
||||||
override val hash: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
@Json(name = "message_authentication_code")
|
|
||||||
override val messageAuthenticationCode: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
@Json(name = "short_authentication_string")
|
|
||||||
override val shortAuthenticationStrings: List<String>? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64)
|
|
||||||
* and the canonical JSON representation of the m.key.verification.start message.
|
|
||||||
*/
|
|
||||||
@Json(name = "commitment")
|
|
||||||
override var commitment: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoAccept {
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
|
|
||||||
companion object : VerificationInfoAcceptFactory {
|
|
||||||
override fun create(
|
|
||||||
tid: String,
|
|
||||||
keyAgreementProtocol: String,
|
|
||||||
hash: String,
|
|
||||||
commitment: String,
|
|
||||||
messageAuthenticationCode: String,
|
|
||||||
shortAuthenticationStrings: List<String>
|
|
||||||
): VerificationInfoAccept {
|
|
||||||
return KeyVerificationAccept(
|
|
||||||
transactionId = tid,
|
|
||||||
keyAgreementProtocol = keyAgreementProtocol,
|
|
||||||
hash = hash,
|
|
||||||
commitment = commitment,
|
|
||||||
messageAuthenticationCode = messageAuthenticationCode,
|
|
||||||
shortAuthenticationStrings = shortAuthenticationStrings
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To device event sent by either party to cancel a key verification.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationCancel(
|
|
||||||
/**
|
|
||||||
* the transaction ID of the verification to cancel.
|
|
||||||
*/
|
|
||||||
@Json(name = "transaction_id")
|
|
||||||
override val transactionId: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* machine-readable reason for cancelling, see #CancelCode.
|
|
||||||
*/
|
|
||||||
override val code: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
|
|
||||||
*/
|
|
||||||
override val reason: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoCancel {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel {
|
|
||||||
return KeyVerificationCancel(
|
|
||||||
tid,
|
|
||||||
cancelCode.value,
|
|
||||||
cancelCode.humanReadable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests a key verification with another user's devices.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationDone(
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoDone {
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent by both devices to send their ephemeral Curve25519 public key to the other device.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationKey(
|
|
||||||
/**
|
|
||||||
* The ID of the transaction that the message is part of.
|
|
||||||
*/
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The device’s ephemeral public key, as an unpadded base64 string.
|
|
||||||
*/
|
|
||||||
@Json(name = "key") override val key: String? = null
|
|
||||||
|
|
||||||
) : SendToDeviceObject, VerificationInfoKey {
|
|
||||||
|
|
||||||
companion object : VerificationInfoKeyFactory {
|
|
||||||
override fun create(tid: String, pubKey: String): KeyVerificationKey {
|
|
||||||
return KeyVerificationKey(tid, pubKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent by both devices to send the MAC of their device key to the other device.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationMac(
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null,
|
|
||||||
@Json(name = "mac") override val mac: Map<String, String>? = null,
|
|
||||||
@Json(name = "keys") override val keys: String? = null
|
|
||||||
|
|
||||||
) : SendToDeviceObject, VerificationInfoMac {
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject(): SendToDeviceObject? = this
|
|
||||||
|
|
||||||
companion object : VerificationInfoMacFactory {
|
|
||||||
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
|
|
||||||
return KeyVerificationMac(tid, mac, keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests a key verification with another user's devices.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationReady(
|
|
||||||
@Json(name = "from_device") override val fromDevice: String?,
|
|
||||||
@Json(name = "methods") override val methods: List<String>?,
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoReady {
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests a key verification with another user's devices.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationRequest(
|
|
||||||
@Json(name = "from_device") override val fromDevice: String?,
|
|
||||||
@Json(name = "methods") override val methods: List<String>,
|
|
||||||
@Json(name = "timestamp") override val timestamp: Long?,
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoRequest {
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
|
|
||||||
override fun toEventContent() = toContent()
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent by Alice to initiate an interactive key verification.
|
|
||||||
*/
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class KeyVerificationStart(
|
|
||||||
@Json(name = "from_device") override val fromDevice: String? = null,
|
|
||||||
@Json(name = "method") override val method: String? = null,
|
|
||||||
@Json(name = "transaction_id") override val transactionId: String? = null,
|
|
||||||
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
|
|
||||||
@Json(name = "hashes") override val hashes: List<String>? = null,
|
|
||||||
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null,
|
|
||||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null,
|
|
||||||
// For QR code verification
|
|
||||||
@Json(name = "secret") override val sharedSecret: String? = null
|
|
||||||
) : SendToDeviceObject, VerificationInfoStart {
|
|
||||||
|
|
||||||
override fun toCanonicalJson(): String {
|
|
||||||
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toSendToDeviceObject() = this
|
|
||||||
}
|
|
|
@ -1,498 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.store
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
|
||||||
import org.matrix.olm.OlmAccount
|
|
||||||
import org.matrix.olm.OlmOutboundGroupSession
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The crypto data store.
|
|
||||||
*/
|
|
||||||
internal interface IMXCryptoStore : IMXCommonCryptoStore {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the device id
|
|
||||||
*/
|
|
||||||
fun getDeviceId(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the olm account
|
|
||||||
*/
|
|
||||||
fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T
|
|
||||||
|
|
||||||
fun getOrCreateOlmAccount(): OlmAccount
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the known inbound group sessions.
|
|
||||||
*
|
|
||||||
* @return the list of all known group sessions, to export them.
|
|
||||||
*/
|
|
||||||
fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the known inbound group sessions for the specified room.
|
|
||||||
*
|
|
||||||
* @param roomId The roomId that the sessions will be returned
|
|
||||||
* @return the list of all known group sessions, for the provided roomId
|
|
||||||
*/
|
|
||||||
fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable key gossiping.
|
|
||||||
* Default is true.
|
|
||||||
* If set to false this device won't send key_request nor will accept key forwarded
|
|
||||||
*/
|
|
||||||
fun enableKeyGossiping(enable: Boolean)
|
|
||||||
|
|
||||||
fun isKeyGossipingEnabled(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As per MSC3061.
|
|
||||||
* If true will make it possible to share part of e2ee room history
|
|
||||||
* on invite depending on the room visibility setting.
|
|
||||||
*/
|
|
||||||
fun enableShareKeyOnInvite(enable: Boolean)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As per MSC3061.
|
|
||||||
* If true will make it possible to share part of e2ee room history
|
|
||||||
* on invite depending on the room visibility setting.
|
|
||||||
*/
|
|
||||||
fun isShareKeysOnInviteEnabled(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
|
||||||
*
|
|
||||||
* @return the room Ids list
|
|
||||||
*/
|
|
||||||
fun getRoomsListBlacklistUnverifiedDevices(): List<String>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current keys backup version.
|
|
||||||
*/
|
|
||||||
fun getKeyBackupVersion(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current keys backup version.
|
|
||||||
*
|
|
||||||
* @param keyBackupVersion the keys backup version or null to delete it
|
|
||||||
*/
|
|
||||||
fun setKeyBackupVersion(keyBackupVersion: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current keys backup local data.
|
|
||||||
*/
|
|
||||||
fun getKeysBackupData(): KeysBackupDataEntity?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the keys backup local data.
|
|
||||||
*
|
|
||||||
* @param keysBackupData the keys backup local data, or null to erase data
|
|
||||||
*/
|
|
||||||
fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the devices statuses map (userId -> tracking status)
|
|
||||||
*/
|
|
||||||
fun getDeviceTrackingStatuses(): Map<String, Int>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate if the store contains data for the passed account.
|
|
||||||
*
|
|
||||||
* @return true means that the user enabled the crypto in a previous session
|
|
||||||
*/
|
|
||||||
fun hasData(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the crypto store for the passed credentials.
|
|
||||||
*/
|
|
||||||
fun deleteStore()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the device id.
|
|
||||||
*
|
|
||||||
* @param deviceId the device id
|
|
||||||
*/
|
|
||||||
fun storeDeviceId(deviceId: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the end to end account for the logged-in user.
|
|
||||||
*/
|
|
||||||
fun saveOlmAccount()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a device for a user.
|
|
||||||
*
|
|
||||||
* @param userId the user's id.
|
|
||||||
* @param deviceId the device id.
|
|
||||||
* @return the device
|
|
||||||
*/
|
|
||||||
fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a device by its identity key.
|
|
||||||
*
|
|
||||||
* @param identityKey the device identity key (`MXDeviceInfo.identityKey`)
|
|
||||||
* @return the device or null if not found
|
|
||||||
*/
|
|
||||||
fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the known devices for a user.
|
|
||||||
*
|
|
||||||
* @param userId The user's id.
|
|
||||||
* @param devices A map from device id to 'MXDevice' object for the device.
|
|
||||||
*/
|
|
||||||
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the cross signing keys for the user userId.
|
|
||||||
*/
|
|
||||||
fun storeUserIdentity(
|
|
||||||
userId: String,
|
|
||||||
userIdentity: UserIdentity
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the known devices for a user.
|
|
||||||
*
|
|
||||||
* @param userId The user's id.
|
|
||||||
* @return The devices map if some devices are known, else null
|
|
||||||
*/
|
|
||||||
fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>?
|
|
||||||
|
|
||||||
fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>?
|
|
||||||
|
|
||||||
// fun getUserDeviceListFlow(userId: String): Flow<List<CryptoDeviceInfo>>
|
|
||||||
|
|
||||||
fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>>
|
|
||||||
|
|
||||||
fun getLiveDeviceList(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
|
|
||||||
|
|
||||||
// TODO temp
|
|
||||||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
|
||||||
|
|
||||||
fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the crypto algorithm for a room.
|
|
||||||
*
|
|
||||||
* @param roomId the id of the room.
|
|
||||||
* @param algorithm the algorithm.
|
|
||||||
*/
|
|
||||||
fun storeRoomAlgorithm(roomId: String, algorithm: String?)
|
|
||||||
|
|
||||||
fun shouldShareHistory(roomId: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a session between the logged-in user and another device.
|
|
||||||
*
|
|
||||||
* @param olmSessionWrapper the end-to-end session.
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
*/
|
|
||||||
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all end-to-end session ids between our own device and another
|
|
||||||
* device.
|
|
||||||
*
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return A set of sessionId, or null if device is not known
|
|
||||||
*/
|
|
||||||
fun getDeviceSessionIds(deviceKey: String): List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an end-to-end session between our own device and another
|
|
||||||
* device.
|
|
||||||
*
|
|
||||||
* @param sessionId the session Id.
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return The Base64 end-to-end session, or null if not found
|
|
||||||
*/
|
|
||||||
fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist.
|
|
||||||
*
|
|
||||||
* @param deviceKey the public key of the other device.
|
|
||||||
* @return last used sessionId, or null if not found
|
|
||||||
*/
|
|
||||||
fun getLastUsedSessionId(deviceKey: String): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store inbound group sessions.
|
|
||||||
*
|
|
||||||
* @param sessions the inbound group sessions to store.
|
|
||||||
*/
|
|
||||||
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an inbound group session, filtering shared history.
|
|
||||||
*
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
|
||||||
* @param sharedHistory filter inbound session with respect to shared history field
|
|
||||||
* @return an inbound group session.
|
|
||||||
*/
|
|
||||||
fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current outbound group session for this encrypted room.
|
|
||||||
*/
|
|
||||||
fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current outbound group session for this encrypted room.
|
|
||||||
*/
|
|
||||||
fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an inbound group session.
|
|
||||||
*
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
|
||||||
*/
|
|
||||||
fun removeInboundGroupSession(sessionId: String, senderKey: String)
|
|
||||||
|
|
||||||
/* ==========================================================================================
|
|
||||||
* Keys backup
|
|
||||||
* ========================================================================================== */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark all inbound group sessions as not backed up.
|
|
||||||
*/
|
|
||||||
fun resetBackupMarkers()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark inbound group sessions as backed up on the user homeserver.
|
|
||||||
*
|
|
||||||
* @param olmInboundGroupSessionWrappers the sessions
|
|
||||||
*/
|
|
||||||
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve inbound group sessions that are not yet backed up.
|
|
||||||
*
|
|
||||||
* @param limit the maximum number of sessions to return.
|
|
||||||
* @return an array of non backed up inbound group sessions.
|
|
||||||
*/
|
|
||||||
fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of stored inbound group sessions.
|
|
||||||
*
|
|
||||||
* @param onlyBackedUp if true, count only session marked as backed up.
|
|
||||||
* @return a count.
|
|
||||||
*/
|
|
||||||
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the device statuses.
|
|
||||||
*
|
|
||||||
* @param deviceTrackingStatuses the device tracking statuses
|
|
||||||
*/
|
|
||||||
fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the tracking status of a specified userId devices.
|
|
||||||
*
|
|
||||||
* @param userId the user id
|
|
||||||
* @param defaultValue the default value
|
|
||||||
* @return the tracking status
|
|
||||||
*/
|
|
||||||
fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look for an existing outgoing room key request, and if none is found, return null.
|
|
||||||
*
|
|
||||||
* @param requestBody the request body
|
|
||||||
* @return an OutgoingRoomKeyRequest instance or null
|
|
||||||
*/
|
|
||||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
|
|
||||||
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
|
|
||||||
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
|
||||||
*
|
|
||||||
* @param requestBody the request
|
|
||||||
* @param recipients list of recipients
|
|
||||||
* @param fromIndex start index
|
|
||||||
* @return either the same instance as passed in, or the existing one.
|
|
||||||
*/
|
|
||||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<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 deleteOutgoingRoomKeyRequest(requestId: String)
|
|
||||||
fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState)
|
|
||||||
|
|
||||||
fun saveIncomingKeyRequestAuditTrail(
|
|
||||||
requestId: String,
|
|
||||||
roomId: String,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String,
|
|
||||||
algorithm: String,
|
|
||||||
fromUser: String,
|
|
||||||
fromDevice: String
|
|
||||||
)
|
|
||||||
|
|
||||||
fun saveWithheldAuditTrail(
|
|
||||||
roomId: String,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String,
|
|
||||||
algorithm: String,
|
|
||||||
code: WithHeldCode,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String
|
|
||||||
)
|
|
||||||
|
|
||||||
fun saveForwardKeyAuditTrail(
|
|
||||||
roomId: String,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String,
|
|
||||||
algorithm: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
chainIndex: Long?
|
|
||||||
)
|
|
||||||
|
|
||||||
fun saveIncomingForwardKeyAuditTrail(
|
|
||||||
roomId: String,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String,
|
|
||||||
algorithm: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
chainIndex: Long?
|
|
||||||
)
|
|
||||||
|
|
||||||
fun addNewSessionListener(listener: NewSessionListener)
|
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
|
||||||
|
|
||||||
// =============================================
|
|
||||||
// CROSS SIGNING
|
|
||||||
// =============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current crosssigning info.
|
|
||||||
*/
|
|
||||||
fun getMyCrossSigningInfo(): MXCrossSigningInfo?
|
|
||||||
|
|
||||||
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
|
|
||||||
|
|
||||||
fun getCrossSigningInfo(userId: String): MXCrossSigningInfo?
|
|
||||||
fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>>
|
|
||||||
|
|
||||||
// fun getCrossSigningInfoFlow(userId: String): Flow<Optional<MXCrossSigningInfo>>
|
|
||||||
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
|
|
||||||
|
|
||||||
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
|
|
||||||
|
|
||||||
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
|
|
||||||
fun storeMSKPrivateKey(msk: String?)
|
|
||||||
fun storeSSKPrivateKey(ssk: String?)
|
|
||||||
fun storeUSKPrivateKey(usk: String?)
|
|
||||||
|
|
||||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
|
||||||
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
|
|
||||||
// fun getCrossSigningPrivateKeysFlow(): Flow<Optional<PrivateKeysInfo>>
|
|
||||||
|
|
||||||
fun getGlobalCryptoConfig(): GlobalCryptoConfig
|
|
||||||
|
|
||||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
|
||||||
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
|
|
||||||
|
|
||||||
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
|
|
||||||
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?)
|
|
||||||
|
|
||||||
fun clearOtherUserTrust()
|
|
||||||
|
|
||||||
fun updateUsersTrust(check: (String) -> Boolean)
|
|
||||||
|
|
||||||
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
|
|
||||||
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
|
|
||||||
|
|
||||||
fun markedSessionAsShared(
|
|
||||||
roomId: String?,
|
|
||||||
sessionId: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
deviceIdentityKey: String,
|
|
||||||
chainIndex: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query for information on this session sharing history.
|
|
||||||
* @return SharedSessionResult
|
|
||||||
* if found is true then this session was initialy shared with that user|device,
|
|
||||||
* in this case chainIndex is not nullindicates the ratchet position.
|
|
||||||
* In found is false, chainIndex is null
|
|
||||||
*/
|
|
||||||
fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): SharedSessionResult
|
|
||||||
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
|
|
||||||
|
|
||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
|
||||||
// Dev tools
|
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
|
||||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
|
||||||
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
|
||||||
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
|
|
||||||
fun getGossipingEvents(): List<AuditTrail>
|
|
||||||
|
|
||||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
|
||||||
fun areDeviceKeysUploaded(): Boolean
|
|
||||||
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a bunch of data related to the users. @See [UserDataToStore].
|
|
||||||
*/
|
|
||||||
fun storeData(userDataToStore: UserDataToStore)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.store.db
|
|
||||||
|
|
||||||
import io.realm.DynamicRealm
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo001Legacy
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo002Legacy
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo003RiotX
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo004
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo005
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo006
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo007
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo008
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo009
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo010
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo011
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo020
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo021
|
|
||||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema version history:
|
|
||||||
* 0, 1, 2: legacy Riot-Android;
|
|
||||||
* 3: migrate to RiotX schema;
|
|
||||||
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6).
|
|
||||||
*/
|
|
||||||
internal class RealmCryptoStoreMigration @Inject constructor(
|
|
||||||
private val clock: Clock,
|
|
||||||
) : MatrixRealmMigration(
|
|
||||||
dbName = "Crypto",
|
|
||||||
schemaVersion = 21L,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Forces all RealmCryptoStoreMigration instances to be equal.
|
|
||||||
* Avoids Realm throwing when multiple instances of the migration are set.
|
|
||||||
*/
|
|
||||||
override fun equals(other: Any?) = other is RealmCryptoStoreMigration
|
|
||||||
override fun hashCode() = 5000
|
|
||||||
|
|
||||||
override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
|
|
||||||
if (oldVersion < 1) MigrateCryptoTo001Legacy(realm).perform()
|
|
||||||
if (oldVersion < 2) MigrateCryptoTo002Legacy(realm).perform()
|
|
||||||
if (oldVersion < 3) MigrateCryptoTo003RiotX(realm).perform()
|
|
||||||
if (oldVersion < 4) MigrateCryptoTo004(realm).perform()
|
|
||||||
if (oldVersion < 5) MigrateCryptoTo005(realm).perform()
|
|
||||||
if (oldVersion < 6) MigrateCryptoTo006(realm).perform()
|
|
||||||
if (oldVersion < 7) MigrateCryptoTo007(realm).perform()
|
|
||||||
if (oldVersion < 8) MigrateCryptoTo008(realm, clock).perform()
|
|
||||||
if (oldVersion < 9) MigrateCryptoTo009(realm).perform()
|
|
||||||
if (oldVersion < 10) MigrateCryptoTo010(realm).perform()
|
|
||||||
if (oldVersion < 11) MigrateCryptoTo011(realm).perform()
|
|
||||||
if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
|
|
||||||
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
|
||||||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
|
||||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
|
||||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
|
||||||
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
|
||||||
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
|
|
||||||
if (oldVersion < 19) MigrateCryptoTo019(realm).perform()
|
|
||||||
if (oldVersion < 20) MigrateCryptoTo020(realm).perform()
|
|
||||||
if (oldVersion < 21) MigrateCryptoTo021(realm).perform()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
|
|
||||||
import org.matrix.android.sdk.api.session.uia.UiaResult
|
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
|
||||||
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.olm.OlmPkSigning
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
|
||||||
data class Params(
|
|
||||||
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Result(
|
|
||||||
val masterKeyPK: String,
|
|
||||||
val userKeyPK: String,
|
|
||||||
val selfSigningKeyPK: String,
|
|
||||||
val masterKeyInfo: CryptoCrossSigningKey,
|
|
||||||
val userKeyInfo: CryptoCrossSigningKey,
|
|
||||||
val selfSignedKeyInfo: CryptoCrossSigningKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
|
||||||
private val uploadSigningKeysTask: UploadSigningKeysTask,
|
|
||||||
private val uploadSignaturesTask: UploadSignaturesTask
|
|
||||||
) : InitializeCrossSigningTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result {
|
|
||||||
var masterPkOlm: OlmPkSigning? = null
|
|
||||||
var userSigningPkOlm: OlmPkSigning? = null
|
|
||||||
var selfSigningPkOlm: OlmPkSigning? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
// =================
|
|
||||||
// MASTER KEY
|
|
||||||
// =================
|
|
||||||
|
|
||||||
masterPkOlm = OlmPkSigning()
|
|
||||||
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// USER KEY
|
|
||||||
// =================
|
|
||||||
userSigningPkOlm = OlmPkSigning()
|
|
||||||
val uskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
|
|
||||||
|
|
||||||
// Sign userSigningKey with master
|
|
||||||
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.build()
|
|
||||||
.canonicalSignable()
|
|
||||||
.let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// SELF SIGNING KEY
|
|
||||||
// =================
|
|
||||||
selfSigningPkOlm = OlmPkSigning()
|
|
||||||
val sskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
|
|
||||||
|
|
||||||
// Sign selfSigningKey with master
|
|
||||||
val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.build()
|
|
||||||
.canonicalSignable()
|
|
||||||
.let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// I need to upload the keys
|
|
||||||
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
|
|
||||||
.key(masterPublicKey)
|
|
||||||
.build()
|
|
||||||
val uploadSigningKeysParams = UploadSigningKeysTask.Params(
|
|
||||||
masterKey = mskCrossSigningKeyInfo,
|
|
||||||
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedUSK)
|
|
||||||
.build(),
|
|
||||||
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedSSK)
|
|
||||||
.build(),
|
|
||||||
userAuthParam = null
|
|
||||||
// userAuthParam = params.authParams
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
if (params.interactiveAuthInterceptor == null ||
|
|
||||||
handleUIA(
|
|
||||||
failure = failure,
|
|
||||||
interceptor = params.interactiveAuthInterceptor,
|
|
||||||
retryBlock = { authUpdate ->
|
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
|
|
||||||
}
|
|
||||||
) != UiaResult.SUCCESS
|
|
||||||
) {
|
|
||||||
Timber.d("## UIA: propagate failure")
|
|
||||||
throw failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the current device with SSK
|
|
||||||
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
|
||||||
|
|
||||||
val myDevice = myDeviceInfoHolder.get().myDevice
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
|
|
||||||
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
|
|
||||||
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
|
|
||||||
.also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
|
||||||
}
|
|
||||||
myDevice.copy(signatures = updateSignatures).let {
|
|
||||||
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign MSK with device key (migration) and upload signatures
|
|
||||||
val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
|
|
||||||
olmDevice.signMessage(message)?.let { sign ->
|
|
||||||
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
|
|
||||||
?: HashMap()).also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
|
|
||||||
}
|
|
||||||
mskCrossSigningKeyInfo.copy(
|
|
||||||
signatures = mskUpdatedSignatures
|
|
||||||
).let {
|
|
||||||
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should we ignore failure of that?
|
|
||||||
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build()))
|
|
||||||
|
|
||||||
return InitializeCrossSigningTask.Result(
|
|
||||||
masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(),
|
|
||||||
userKeyPK = uskPrivateKey.toBase64NoPadding(),
|
|
||||||
selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(),
|
|
||||||
masterKeyInfo = uploadSigningKeysParams.masterKey,
|
|
||||||
userKeyInfo = uploadSigningKeysParams.userKey,
|
|
||||||
selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
masterPkOlm?.releaseSigning()
|
|
||||||
userSigningPkOlm?.releaseSigning()
|
|
||||||
selfSigningPkOlm?.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString
|
|
||||||
|
|
||||||
internal class KotlinQRVerification(
|
|
||||||
private val channel: Channel<VerificationIntent>,
|
|
||||||
var qrCodeData: QrCodeData?,
|
|
||||||
override val method: VerificationMethod,
|
|
||||||
override val transactionId: String,
|
|
||||||
override val otherUserId: String,
|
|
||||||
override val otherDeviceId: String?,
|
|
||||||
override val isIncoming: Boolean,
|
|
||||||
var state: QRCodeVerificationState,
|
|
||||||
val isToDevice: Boolean
|
|
||||||
) : QrCodeVerificationTransaction {
|
|
||||||
|
|
||||||
override fun state() = state
|
|
||||||
|
|
||||||
override val qrCodeText: String?
|
|
||||||
get() = qrCodeData?.toEncodedString()
|
|
||||||
//
|
|
||||||
// var userMSKKeyToTrust: String? = null
|
|
||||||
// var deviceKeysToTrust = mutableListOf<String>()
|
|
||||||
|
|
||||||
// override suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) {
|
|
||||||
// TODO("Not yet implemented")
|
|
||||||
// }
|
|
||||||
|
|
||||||
override suspend fun otherUserScannedMyQrCode() {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionConfirmCodeWasScanned(otherUserId, transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun otherUserDidNotScannedMyQrCode() {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
// TODO what cancel code??
|
|
||||||
VerificationIntent.ActionCancel(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancel() {
|
|
||||||
cancel(CancelCode.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancel(code: CancelCode) {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionCancel(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isToDeviceTransport() = isToDevice
|
|
||||||
}
|
|
|
@ -1,476 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V1
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V2
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_AGREEMENT_PROTOCOLS
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_HASHES
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_MACS
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_SHORT_CODES
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256_LONGKDF
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
|
||||||
import org.matrix.olm.OlmSAS
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
internal class KotlinSasTransaction(
|
|
||||||
private val channel: Channel<VerificationIntent>,
|
|
||||||
override val transactionId: String,
|
|
||||||
override val otherUserId: String,
|
|
||||||
private val myUserId: String,
|
|
||||||
private val myTrustedMSK: String?,
|
|
||||||
override var otherDeviceId: String?,
|
|
||||||
private val myDeviceId: String,
|
|
||||||
private val myDeviceFingerprint: String,
|
|
||||||
override val isIncoming: Boolean,
|
|
||||||
val startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null,
|
|
||||||
val isToDevice: Boolean,
|
|
||||||
var state: SasTransactionState,
|
|
||||||
val olmSAS: OlmSAS,
|
|
||||||
) : SasVerificationTransaction {
|
|
||||||
|
|
||||||
override val method: VerificationMethod
|
|
||||||
get() = VerificationMethod.SAS
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun sasStart(inRoom: Boolean, fromDevice: String, requestId: String): VerificationInfoStart {
|
|
||||||
return if (inRoom) {
|
|
||||||
MessageVerificationStartContent(
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
hashes = KNOWN_HASHES,
|
|
||||||
keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS,
|
|
||||||
messageAuthenticationCodes = KNOWN_MACS,
|
|
||||||
shortAuthenticationStrings = KNOWN_SHORT_CODES,
|
|
||||||
method = VERIFICATION_METHOD_SAS,
|
|
||||||
relatesTo = RelationDefaultContent(
|
|
||||||
type = RelationType.REFERENCE,
|
|
||||||
eventId = requestId
|
|
||||||
),
|
|
||||||
sharedSecret = null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
KeyVerificationStart(
|
|
||||||
fromDevice,
|
|
||||||
VERIFICATION_METHOD_SAS,
|
|
||||||
requestId,
|
|
||||||
KNOWN_AGREEMENT_PROTOCOLS,
|
|
||||||
KNOWN_HASHES,
|
|
||||||
KNOWN_MACS,
|
|
||||||
KNOWN_SHORT_CODES,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sasAccept(
|
|
||||||
inRoom: Boolean,
|
|
||||||
requestId: String,
|
|
||||||
keyAgreementProtocol: String,
|
|
||||||
hash: String,
|
|
||||||
commitment: String,
|
|
||||||
messageAuthenticationCode: String,
|
|
||||||
shortAuthenticationStrings: List<String>,
|
|
||||||
): VerificationInfoAccept {
|
|
||||||
return if (inRoom) {
|
|
||||||
MessageVerificationAcceptContent.create(
|
|
||||||
requestId,
|
|
||||||
keyAgreementProtocol,
|
|
||||||
hash,
|
|
||||||
commitment,
|
|
||||||
messageAuthenticationCode,
|
|
||||||
shortAuthenticationStrings
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
KeyVerificationAccept.create(
|
|
||||||
requestId,
|
|
||||||
keyAgreementProtocol,
|
|
||||||
hash,
|
|
||||||
commitment,
|
|
||||||
messageAuthenticationCode,
|
|
||||||
shortAuthenticationStrings
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sasReady(
|
|
||||||
inRoom: Boolean,
|
|
||||||
requestId: String,
|
|
||||||
methods: List<String>,
|
|
||||||
fromDevice: String,
|
|
||||||
): VerificationInfoReady {
|
|
||||||
return if (inRoom) {
|
|
||||||
MessageVerificationReadyContent.create(
|
|
||||||
requestId,
|
|
||||||
methods,
|
|
||||||
fromDevice,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
KeyVerificationReady(
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
methods = methods,
|
|
||||||
transactionId = requestId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sasKeyMessage(
|
|
||||||
inRoom: Boolean,
|
|
||||||
requestId: String,
|
|
||||||
pubKey: String,
|
|
||||||
): VerificationInfoKey {
|
|
||||||
return if (inRoom) {
|
|
||||||
MessageVerificationKeyContent.create(tid = requestId, pubKey = pubKey)
|
|
||||||
} else {
|
|
||||||
KeyVerificationKey.create(tid = requestId, pubKey = pubKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sasMacMessage(
|
|
||||||
inRoom: Boolean,
|
|
||||||
requestId: String,
|
|
||||||
validVerificationInfoMac: ValidVerificationInfoMac
|
|
||||||
): VerificationInfoMac {
|
|
||||||
return if (inRoom) {
|
|
||||||
MessageVerificationMacContent.create(
|
|
||||||
tid = requestId,
|
|
||||||
keys = validVerificationInfoMac.keys,
|
|
||||||
mac = validVerificationInfoMac.mac
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
KeyVerificationMac.create(
|
|
||||||
tid = requestId,
|
|
||||||
keys = validVerificationInfoMac.keys,
|
|
||||||
mac = validVerificationInfoMac.mac
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "KotlinSasTransaction(transactionId=$transactionId, state=$state, otherUserId=$otherUserId, otherDeviceId=$otherDeviceId, isToDevice=$isToDevice)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// To override finalize(), all you need to do is simply declare it, without using the override keyword:
|
|
||||||
protected fun finalize() {
|
|
||||||
releaseSAS()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseSAS() {
|
|
||||||
// finalization logic
|
|
||||||
olmSAS.releaseSas()
|
|
||||||
}
|
|
||||||
|
|
||||||
var accepted: ValidVerificationInfoAccept? = null
|
|
||||||
var otherKey: String? = null
|
|
||||||
var shortCodeBytes: ByteArray? = null
|
|
||||||
var myMac: ValidVerificationInfoMac? = null
|
|
||||||
var theirMac: ValidVerificationInfoMac? = null
|
|
||||||
var verifiedSuccessInfo: MacVerificationResult.Success? = null
|
|
||||||
|
|
||||||
override fun state() = this.state
|
|
||||||
|
|
||||||
// override fun supportsEmoji(): Boolean {
|
|
||||||
// return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun getEmojiCodeRepresentation(): List<EmojiRepresentation> {
|
|
||||||
return shortCodeBytes?.getEmojiCodeRepresentation().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDecimalCodeRepresentation(): String? {
|
|
||||||
return shortCodeBytes?.getDecimalCodeRepresentation()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun userHasVerifiedShortCode() {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionSASCodeMatches(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun acceptVerification() {
|
|
||||||
// nop
|
|
||||||
// as we are using verification request accept is automatic
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun shortCodeDoesNotMatch() {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionSASCodeDoesNotMatch(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancel() {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionCancel(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancel(code: CancelCode) {
|
|
||||||
val deferred = CompletableDeferred<Unit>()
|
|
||||||
channel.send(
|
|
||||||
VerificationIntent.ActionCancel(transactionId, deferred)
|
|
||||||
)
|
|
||||||
deferred.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isToDeviceTransport() = isToDevice
|
|
||||||
|
|
||||||
fun calculateSASBytes(otherKey: String) {
|
|
||||||
this.otherKey = otherKey
|
|
||||||
olmSAS.setTheirPublicKey(otherKey)
|
|
||||||
shortCodeBytes = when (accepted!!.keyAgreementProtocol) {
|
|
||||||
KEY_AGREEMENT_V1 -> {
|
|
||||||
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
|
|
||||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
|
||||||
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
|
|
||||||
// - the Matrix ID of the user who sent the m.key.verification.start message,
|
|
||||||
// - the device ID of the device that sent the m.key.verification.start message,
|
|
||||||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
|
||||||
// - he device ID of the device that sent the m.key.verification.accept message
|
|
||||||
// - the transaction ID.
|
|
||||||
val sasInfo = buildString {
|
|
||||||
append("MATRIX_KEY_VERIFICATION_SAS")
|
|
||||||
if (isIncoming) {
|
|
||||||
append(otherUserId)
|
|
||||||
append(otherDeviceId)
|
|
||||||
append(myUserId)
|
|
||||||
append(myDeviceId)
|
|
||||||
append(olmSAS.publicKey)
|
|
||||||
} else {
|
|
||||||
append(myUserId)
|
|
||||||
append(myDeviceId)
|
|
||||||
append(otherUserId)
|
|
||||||
append(otherDeviceId)
|
|
||||||
}
|
|
||||||
append(transactionId)
|
|
||||||
}
|
|
||||||
// decimal: generate five bytes by using HKDF.
|
|
||||||
// emoji: generate six bytes by using HKDF.
|
|
||||||
olmSAS.generateShortCode(sasInfo, 6)
|
|
||||||
}
|
|
||||||
KEY_AGREEMENT_V2 -> {
|
|
||||||
val sasInfo = buildString {
|
|
||||||
append("MATRIX_KEY_VERIFICATION_SAS|")
|
|
||||||
if (isIncoming) {
|
|
||||||
append(otherUserId).append('|')
|
|
||||||
append(otherDeviceId).append('|')
|
|
||||||
append(otherKey).append('|')
|
|
||||||
append(myUserId).append('|')
|
|
||||||
append(myDeviceId).append('|')
|
|
||||||
append(olmSAS.publicKey).append('|')
|
|
||||||
} else {
|
|
||||||
append(myUserId).append('|')
|
|
||||||
append(myDeviceId).append('|')
|
|
||||||
append(olmSAS.publicKey).append('|')
|
|
||||||
append(otherUserId).append('|')
|
|
||||||
append(otherDeviceId).append('|')
|
|
||||||
append(otherKey).append('|')
|
|
||||||
}
|
|
||||||
append(transactionId)
|
|
||||||
}
|
|
||||||
olmSAS.generateShortCode(sasInfo, 6)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Protocol has been checked earlier
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun computeMyMac(): ValidVerificationInfoMac {
|
|
||||||
val baseInfo = buildString {
|
|
||||||
append("MATRIX_KEY_VERIFICATION_MAC")
|
|
||||||
append(myUserId)
|
|
||||||
append(myDeviceId)
|
|
||||||
append(otherUserId)
|
|
||||||
append(otherDeviceId)
|
|
||||||
append(transactionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key.
|
|
||||||
// It should now contain both the device key and the MSK.
|
|
||||||
// So when Alice and Bob verify with SAS, the verification will verify the MSK.
|
|
||||||
|
|
||||||
val keyMap = HashMap<String, String>()
|
|
||||||
|
|
||||||
val keyId = "ed25519:$myDeviceId"
|
|
||||||
val macString = macUsingAgreedMethod(myDeviceFingerprint, baseInfo + keyId)
|
|
||||||
|
|
||||||
if (macString.isNullOrBlank()) {
|
|
||||||
// Should not happen
|
|
||||||
Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.")
|
|
||||||
throw IllegalStateException("Invalid mac for transaction ${transactionId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyMap[keyId] = macString
|
|
||||||
|
|
||||||
if (myTrustedMSK != null) {
|
|
||||||
val crossSigningKeyId = "ed25519:$myTrustedMSK"
|
|
||||||
macUsingAgreedMethod(myTrustedMSK, baseInfo + crossSigningKeyId)?.let { mskMacString ->
|
|
||||||
keyMap[crossSigningKeyId] = mskMacString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS")
|
|
||||||
|
|
||||||
if (keyStrings.isNullOrBlank()) {
|
|
||||||
// Should not happen
|
|
||||||
Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.")
|
|
||||||
throw IllegalStateException("Invalid key mac for transaction ${transactionId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidVerificationInfoMac(
|
|
||||||
transactionId,
|
|
||||||
keyMap,
|
|
||||||
keyStrings
|
|
||||||
).also {
|
|
||||||
myMac = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class MacVerificationResult {
|
|
||||||
|
|
||||||
object MismatchKeys : MacVerificationResult()
|
|
||||||
data class MismatchMacDevice(val deviceId: String) : MacVerificationResult()
|
|
||||||
object MismatchMacCrossSigning : MacVerificationResult()
|
|
||||||
object NoDevicesVerified : MacVerificationResult()
|
|
||||||
|
|
||||||
data class Success(val verifiedDeviceId: List<String>, val otherMskTrusted: Boolean) : MacVerificationResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyMacs(
|
|
||||||
theirMacSafe: ValidVerificationInfoMac,
|
|
||||||
otherUserKnownDevices: List<CryptoDeviceInfo>,
|
|
||||||
otherMasterKey: String?
|
|
||||||
): MacVerificationResult {
|
|
||||||
Timber.v("## SAS verifying macs for id:$transactionId")
|
|
||||||
|
|
||||||
// Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID),
|
|
||||||
// as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message.
|
|
||||||
// Bob’s device compares these with the HMAC values given in the m.key.verification.mac message.
|
|
||||||
// If everything matches, then consider Alice’s device keys as verified.
|
|
||||||
val baseInfo = buildString {
|
|
||||||
append("MATRIX_KEY_VERIFICATION_MAC")
|
|
||||||
append(otherUserId)
|
|
||||||
append(otherDeviceId)
|
|
||||||
append(myUserId)
|
|
||||||
append(myDeviceId)
|
|
||||||
append(transactionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",")
|
|
||||||
|
|
||||||
val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS")
|
|
||||||
if (theirMacSafe.keys != keyStrings) {
|
|
||||||
// WRONG!
|
|
||||||
return MacVerificationResult.MismatchKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
val verifiedDevices = ArrayList<String>()
|
|
||||||
|
|
||||||
// cannot be empty because it has been validated
|
|
||||||
theirMacSafe.mac.keys.forEach { entry ->
|
|
||||||
val keyIDNoPrefix = entry.removePrefix("ed25519:")
|
|
||||||
val otherDeviceKey = otherUserKnownDevices
|
|
||||||
.firstOrNull { it.deviceId == keyIDNoPrefix }
|
|
||||||
?.fingerprint()
|
|
||||||
if (otherDeviceKey == null) {
|
|
||||||
Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
|
||||||
// just ignore and continue
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + entry)
|
|
||||||
if (mac != theirMacSafe.mac[entry]) {
|
|
||||||
// WRONG!
|
|
||||||
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
|
|
||||||
// cancel(CancelCode.MismatchedKeys)
|
|
||||||
return MacVerificationResult.MismatchMacDevice(keyIDNoPrefix)
|
|
||||||
}
|
|
||||||
verifiedDevices.add(keyIDNoPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
var otherMasterKeyIsVerified = false
|
|
||||||
if (otherMasterKey != null) {
|
|
||||||
// Did the user signed his master key
|
|
||||||
theirMacSafe.mac.keys.forEach {
|
|
||||||
val keyIDNoPrefix = it.removePrefix("ed25519:")
|
|
||||||
if (keyIDNoPrefix == otherMasterKey) {
|
|
||||||
// Check the signature
|
|
||||||
val mac = macUsingAgreedMethod(otherMasterKey, baseInfo + it)
|
|
||||||
if (mac != theirMacSafe.mac[it]) {
|
|
||||||
// WRONG!
|
|
||||||
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
|
||||||
return MacVerificationResult.MismatchMacCrossSigning
|
|
||||||
} else {
|
|
||||||
otherMasterKeyIsVerified = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if none of the keys could be verified, then error because the app
|
|
||||||
// should be informed about that
|
|
||||||
if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) {
|
|
||||||
Timber.e("## SAS Verification: No devices verified")
|
|
||||||
return MacVerificationResult.NoDevicesVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
return MacVerificationResult.Success(
|
|
||||||
verifiedDevices,
|
|
||||||
otherMasterKeyIsVerified
|
|
||||||
).also {
|
|
||||||
// store and will persist when transaction is actually done
|
|
||||||
verifiedSuccessInfo = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun macUsingAgreedMethod(message: String, info: String): String? {
|
|
||||||
return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) {
|
|
||||||
SAS_MAC_SHA256_LONGKDF -> olmSAS.calculateMacLongKdf(message, info)
|
|
||||||
SAS_MAC_SHA256 -> olmSAS.calculateMac(message, info)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString
|
|
||||||
|
|
||||||
internal class KotlinVerificationRequest(
|
|
||||||
val requestId: String,
|
|
||||||
val incoming: Boolean,
|
|
||||||
val otherUserId: String,
|
|
||||||
var state: EVerificationState,
|
|
||||||
val ageLocalTs: Long
|
|
||||||
) {
|
|
||||||
|
|
||||||
var roomId: String? = null
|
|
||||||
var qrCodeData: QrCodeData? = null
|
|
||||||
var targetDevices: List<String>? = null
|
|
||||||
var requestInfo: ValidVerificationInfoRequest? = null
|
|
||||||
var readyInfo: ValidVerificationInfoReady? = null
|
|
||||||
var cancelCode: CancelCode? = null
|
|
||||||
|
|
||||||
// fun requestId() = requestId
|
|
||||||
//
|
|
||||||
// fun incoming() = incoming
|
|
||||||
//
|
|
||||||
// fun otherUserId() = otherUserId
|
|
||||||
//
|
|
||||||
// fun roomId() = roomId
|
|
||||||
//
|
|
||||||
// fun targetDevices() = targetDevices
|
|
||||||
//
|
|
||||||
// fun state() = state
|
|
||||||
//
|
|
||||||
// fun ageLocalTs() = ageLocalTs
|
|
||||||
|
|
||||||
fun otherDeviceId(): String? {
|
|
||||||
return if (incoming) {
|
|
||||||
requestInfo?.fromDevice
|
|
||||||
} else {
|
|
||||||
readyInfo?.fromDevice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelCode(): CancelCode? = cancelCode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SAS is supported if I support it and the other party support it.
|
|
||||||
*/
|
|
||||||
private fun isSasSupported(): Boolean {
|
|
||||||
return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() &&
|
|
||||||
readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Other can show QR code if I can scan QR code and other can show QR code.
|
|
||||||
*/
|
|
||||||
private fun otherCanShowQrCode(): Boolean {
|
|
||||||
return if (incoming) {
|
|
||||||
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() &&
|
|
||||||
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse()
|
|
||||||
} else {
|
|
||||||
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() &&
|
|
||||||
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Other can scan QR code if I can show QR code and other can scan QR code.
|
|
||||||
*/
|
|
||||||
private fun otherCanScanQrCode(): Boolean {
|
|
||||||
return if (incoming) {
|
|
||||||
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() &&
|
|
||||||
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse()
|
|
||||||
} else {
|
|
||||||
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() &&
|
|
||||||
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun qrCodeText() = qrCodeData?.toEncodedString()
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return toPendingVerificationRequest().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toPendingVerificationRequest(): PendingVerificationRequest {
|
|
||||||
return PendingVerificationRequest(
|
|
||||||
ageLocalTs = ageLocalTs,
|
|
||||||
state = state,
|
|
||||||
isIncoming = incoming,
|
|
||||||
otherUserId = otherUserId,
|
|
||||||
roomId = roomId,
|
|
||||||
transactionId = requestId,
|
|
||||||
cancelConclusion = cancelCode,
|
|
||||||
isFinished = isFinished(),
|
|
||||||
handledByOtherSession = state == EVerificationState.HandledByOtherSession,
|
|
||||||
targetDevices = targetDevices,
|
|
||||||
qrCodeText = qrCodeText(),
|
|
||||||
isSasSupported = isSasSupported(),
|
|
||||||
weShouldShowScanOption = otherCanShowQrCode(),
|
|
||||||
weShouldDisplayQRCode = otherCanScanQrCode(),
|
|
||||||
otherDeviceId = otherDeviceId()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isFinished() = state == EVerificationState.Cancelled || state == EVerificationState.Done
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility
|
|
||||||
import org.matrix.olm.OlmSAS
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// Mainly for testing purpose to ease mocking
|
|
||||||
internal class VerificationCryptoPrimitiveProvider @Inject constructor() {
|
|
||||||
|
|
||||||
fun provideOlmSas(): OlmSAS {
|
|
||||||
return OlmSAS()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sha256(toHash: String): String {
|
|
||||||
return withOlmUtility {
|
|
||||||
it.sha256(toHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
|
|
||||||
internal interface VerificationInfo<ValidObjectType> {
|
|
||||||
fun toEventContent(): Content? = null
|
|
||||||
fun toSendToDeviceObject(): SendToDeviceObject? = null
|
|
||||||
|
|
||||||
fun asValidObject(): ValidObjectType?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* String to identify the transaction.
|
|
||||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
|
|
||||||
* Alice’s device should record this ID and use it in future messages in this transaction.
|
|
||||||
*/
|
|
||||||
val transactionId: String?
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
internal interface VerificationInfoAccept : VerificationInfo<ValidVerificationInfoAccept> {
|
|
||||||
/**
|
|
||||||
* The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
val keyAgreementProtocol: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
val hash: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
val messageAuthenticationCode: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device.
|
|
||||||
*/
|
|
||||||
val shortAuthenticationStrings: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64)
|
|
||||||
* and the canonical JSON representation of the m.key.verification.start message.
|
|
||||||
*/
|
|
||||||
var commitment: String?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoAccept? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationInfoAccept(
|
|
||||||
validTransactionId,
|
|
||||||
validKeyAgreementProtocol,
|
|
||||||
validHash,
|
|
||||||
validMessageAuthenticationCode,
|
|
||||||
validShortAuthenticationStrings,
|
|
||||||
validCommitment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface VerificationInfoAcceptFactory {
|
|
||||||
|
|
||||||
fun create(
|
|
||||||
tid: String,
|
|
||||||
keyAgreementProtocol: String,
|
|
||||||
hash: String,
|
|
||||||
commitment: String,
|
|
||||||
messageAuthenticationCode: String,
|
|
||||||
shortAuthenticationStrings: List<String>
|
|
||||||
): VerificationInfoAccept
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ValidVerificationInfoAccept(
|
|
||||||
val transactionId: String,
|
|
||||||
val keyAgreementProtocol: String,
|
|
||||||
val hash: String,
|
|
||||||
val messageAuthenticationCode: String,
|
|
||||||
val shortAuthenticationStrings: List<String>,
|
|
||||||
var commitment: String?
|
|
||||||
)
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
internal interface VerificationInfoCancel : VerificationInfo<ValidVerificationInfoCancel> {
|
|
||||||
/**
|
|
||||||
* machine-readable reason for cancelling, see [CancelCode].
|
|
||||||
*/
|
|
||||||
val code: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
|
|
||||||
*/
|
|
||||||
val reason: String?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoCancel? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validCode = code?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationInfoCancel(
|
|
||||||
validTransactionId,
|
|
||||||
validCode,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ValidVerificationInfoCancel(
|
|
||||||
val transactionId: String,
|
|
||||||
val code: String,
|
|
||||||
val reason: String?
|
|
||||||
)
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone
|
|
||||||
|
|
||||||
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationDone> {
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationDone? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
return ValidVerificationDone(validTransactionId)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent by both devices to send their ephemeral Curve25519 public key to the other device.
|
|
||||||
*/
|
|
||||||
internal interface VerificationInfoKey : VerificationInfo<ValidVerificationInfoKey> {
|
|
||||||
/**
|
|
||||||
* The device’s ephemeral public key, as an unpadded base64 string.
|
|
||||||
*/
|
|
||||||
val key: String?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoKey? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validKey = key?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationInfoKey(
|
|
||||||
validTransactionId,
|
|
||||||
validKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface VerificationInfoKeyFactory {
|
|
||||||
fun create(tid: String, pubKey: String): VerificationInfoKey
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ValidVerificationInfoKey(
|
|
||||||
val transactionId: String,
|
|
||||||
val key: String
|
|
||||||
)
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
internal interface VerificationInfoMac : VerificationInfo<ValidVerificationInfoMac> {
|
|
||||||
/**
|
|
||||||
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key.
|
|
||||||
*/
|
|
||||||
val mac: Map<String, String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MAC of the comma-separated, sorted list of key IDs given in the mac property,
|
|
||||||
* as an unpadded base64 string, calculated using the MAC key.
|
|
||||||
* For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will
|
|
||||||
* give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”.
|
|
||||||
*/
|
|
||||||
val keys: String?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoMac? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationInfoMac(
|
|
||||||
validTransactionId,
|
|
||||||
validMac,
|
|
||||||
validKeys
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface VerificationInfoMacFactory {
|
|
||||||
fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class ValidVerificationInfoMac(
|
|
||||||
val transactionId: String,
|
|
||||||
val mac: Map<String, String>,
|
|
||||||
val keys: String
|
|
||||||
)
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A new event type is added to the key verification framework: m.key.verification.ready,
|
|
||||||
* which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event.
|
|
||||||
*
|
|
||||||
* The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly
|
|
||||||
* with a m.key.verification.start event instead.
|
|
||||||
*/
|
|
||||||
|
|
||||||
internal interface VerificationInfoReady : VerificationInfo<ValidVerificationInfoReady> {
|
|
||||||
/**
|
|
||||||
* The ID of the device that sent the m.key.verification.ready message.
|
|
||||||
*/
|
|
||||||
val fromDevice: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of verification methods that the device supports.
|
|
||||||
*/
|
|
||||||
val methods: List<String>?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoReady? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null.also {
|
|
||||||
Timber.e("## SAS Invalid room ready content invalid transaction id $transactionId")
|
|
||||||
}
|
|
||||||
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null.also {
|
|
||||||
Timber.e("## SAS Invalid room ready content invalid fromDevice $fromDevice")
|
|
||||||
}
|
|
||||||
val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null.also {
|
|
||||||
Timber.e("## SAS Invalid room ready content invalid methods $methods")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidVerificationInfoReady(
|
|
||||||
validTransactionId,
|
|
||||||
validFromDevice,
|
|
||||||
validMethods
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface MessageVerificationReadyFactory {
|
|
||||||
fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
|
||||||
|
|
||||||
internal interface VerificationInfoRequest : VerificationInfo<ValidVerificationInfoRequest> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required. The device ID which is initiating the request.
|
|
||||||
*/
|
|
||||||
val fromDevice: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required. The verification methods supported by the sender.
|
|
||||||
*/
|
|
||||||
val methods: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The POSIX timestamp in milliseconds for when the request was made.
|
|
||||||
* If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
|
||||||
* the message should be ignored by the receiver.
|
|
||||||
*/
|
|
||||||
val timestamp: Long?
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoRequest? {
|
|
||||||
// FIXME No check on Timestamp?
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return ValidVerificationInfoRequest(
|
|
||||||
validTransactionId,
|
|
||||||
validFromDevice,
|
|
||||||
validMethods,
|
|
||||||
timestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
|
||||||
|
|
||||||
internal interface VerificationInfoStart : VerificationInfo<ValidVerificationInfoStart> {
|
|
||||||
|
|
||||||
val method: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alice’s device ID.
|
|
||||||
*/
|
|
||||||
val fromDevice: String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of key agreement protocols that Alice’s client understands.
|
|
||||||
* Must include “curve25519”.
|
|
||||||
* Other methods may be defined in the future
|
|
||||||
*/
|
|
||||||
val keyAgreementProtocols: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of hashes that Alice’s client understands.
|
|
||||||
* Must include “sha256”. Other methods may be defined in the future.
|
|
||||||
*/
|
|
||||||
val hashes: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of message authentication codes that Alice’s client understands.
|
|
||||||
* Must include “hkdf-hmac-sha256”.
|
|
||||||
* Other methods may be defined in the future.
|
|
||||||
*/
|
|
||||||
val messageAuthenticationCodes: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of short authentication string methods that Alice’s client (and Alice) understands.
|
|
||||||
* Must include “decimal”.
|
|
||||||
* This document also describes the “emoji” method.
|
|
||||||
* Other methods may be defined in the future
|
|
||||||
*/
|
|
||||||
val shortAuthenticationStrings: List<String>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared secret, when starting verification with QR code.
|
|
||||||
*/
|
|
||||||
val sharedSecret: String?
|
|
||||||
|
|
||||||
fun toCanonicalJson(): String
|
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoStart? {
|
|
||||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
return when (method) {
|
|
||||||
VERIFICATION_METHOD_SAS -> {
|
|
||||||
val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null
|
|
||||||
val validMessageAuthenticationCodes = messageAuthenticationCodes
|
|
||||||
?.takeIf {
|
|
||||||
it.contains(SasVerificationTransaction.SAS_MAC_SHA256) ||
|
|
||||||
it.contains(SasVerificationTransaction.SAS_MAC_SHA256_LONGKDF)
|
|
||||||
}
|
|
||||||
?: return null
|
|
||||||
val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null
|
|
||||||
|
|
||||||
ValidVerificationInfoStart.SasVerificationInfoStart(
|
|
||||||
validTransactionId,
|
|
||||||
validFromDevice,
|
|
||||||
validKeyAgreementProtocols,
|
|
||||||
validHashes,
|
|
||||||
validMessageAuthenticationCodes,
|
|
||||||
validShortAuthenticationStrings,
|
|
||||||
canonicalJson = toCanonicalJson()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
VERIFICATION_METHOD_RECIPROCATE -> {
|
|
||||||
val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null
|
|
||||||
|
|
||||||
ValidVerificationInfoStart.ReciprocateVerificationInfoStart(
|
|
||||||
validTransactionId,
|
|
||||||
validFromDevice,
|
|
||||||
validSharedSecret
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ValidVerificationInfoStart(
|
|
||||||
open val transactionId: String,
|
|
||||||
open val fromDevice: String
|
|
||||||
) {
|
|
||||||
data class SasVerificationInfoStart(
|
|
||||||
override val transactionId: String,
|
|
||||||
override val fromDevice: String,
|
|
||||||
val keyAgreementProtocols: List<String>,
|
|
||||||
val hashes: List<String>,
|
|
||||||
val messageAuthenticationCodes: List<String>,
|
|
||||||
val shortAuthenticationStrings: List<String>,
|
|
||||||
val canonicalJson: String
|
|
||||||
) : ValidVerificationInfoStart(transactionId, fromDevice)
|
|
||||||
|
|
||||||
data class ReciprocateVerificationInfoStart(
|
|
||||||
override val transactionId: String,
|
|
||||||
override val fromDevice: String,
|
|
||||||
val sharedSecret: String
|
|
||||||
) : ValidVerificationInfoStart(transactionId, fromDevice)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue