mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 09:55:40 +03:00
Merge branch 'release/1.6.8' into main
This commit is contained in:
commit
552b143f8c
234 changed files with 905 additions and 24003 deletions
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
@ -57,7 +57,7 @@ jobs:
|
|||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- 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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
@ -79,7 +79,7 @@ jobs:
|
|||
- name: Execute exodus-standalone
|
||||
uses: docker://exodusprivacy/exodus-standalone:latest
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
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
|
||||
- name: Build and upload Gplay Nightly APK
|
||||
run: |
|
||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
env:
|
||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||
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
|
||||
# Not always, if ktlint or detekt fail, avoid running the long lint check.
|
||||
run: |
|
||||
./gradlew vector-app:lintGplayKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
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
|
||||
# emulator-build: 7425822
|
||||
script: |
|
||||
./gradlew gatherGplayKotlinCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
||||
|
|
18
CHANGES.md
18
CHANGES.md
|
@ -1,3 +1,21 @@
|
|||
Changes in Element v1.6.8 (2023-11-28)
|
||||
======================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Stop incoming call ringing if the call is cancelled or answered on another session. ([#4066](https://github.com/vector-im/element-android/issues/4066))
|
||||
- Ensure the incoming call will not ring forever, in case the call is not ended by another way. ([#8178](https://github.com/vector-im/element-android/issues/8178))
|
||||
- Unified Push: Ignore the potential SSL error when the custom gateway is testing locally ([#8683](https://github.com/vector-im/element-android/issues/8683))
|
||||
- Fix issue with timeline message view reuse while rich text editor is enabled ([#8688](https://github.com/vector-im/element-android/issues/8688))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Remove unused WebRTC dependency ([#8658](https://github.com/vector-im/element-android/issues/8658))
|
||||
- Take into account boolean "io.element.disable_network_constraint" from the .well-known file. ([#8662](https://github.com/vector-im/element-android/issues/8662))
|
||||
- Update regex for email address to be aligned on RFC 5322 ([#8671](https://github.com/vector-im/element-android/issues/8671))
|
||||
- Bump crypto sdk bindings to v0.3.16 ([#8679](https://github.com/vector-im/element-android/issues/8679))
|
||||
|
||||
|
||||
Changes in Element v1.6.6 (2023-10-05)
|
||||
======================================
|
||||
|
||||
|
|
|
@ -312,7 +312,7 @@ tasks.register("recordScreenshots", GradleBuild) {
|
|||
|
||||
tasks.register("verifyScreenshots", GradleBuild) {
|
||||
startParameter.projectProperties.screenshot = ""
|
||||
tasks = [':vector:verifyPaparazziRustCryptoDebug']
|
||||
tasks = [':vector:verifyPaparazziDebug']
|
||||
}
|
||||
|
||||
ext.initScreenshotTests = { project ->
|
||||
|
|
|
@ -87,11 +87,5 @@ task unitTestsWithCoverage(type: GradleBuild) {
|
|||
task instrumentationTestsWithCoverage(type: GradleBuild) {
|
||||
startParameter.projectProperties.coverage = "true"
|
||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
||||
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
|
||||
}
|
||||
|
||||
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']
|
||||
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ def lifecycle = "2.5.1"
|
|||
def flowBinding = "1.2.0"
|
||||
def flipper = "0.190.0"
|
||||
def epoxy = "5.0.0"
|
||||
def mavericks = "3.0.2"
|
||||
def mavericks = "3.0.7"
|
||||
def glide = "4.15.1"
|
||||
def bigImageViewer = "1.8.1"
|
||||
def jjwt = "0.11.5"
|
||||
|
@ -101,7 +101,7 @@ ext.libs = [
|
|||
],
|
||||
element : [
|
||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||
'wysiwyg' : "io.element.android:wysiwyg:2.2.2"
|
||||
'wysiwyg' : "io.element.android:wysiwyg:2.14.1"
|
||||
],
|
||||
squareup : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
|
|
|
@ -48,7 +48,7 @@ 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
|
||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
```
|
||||
|
||||
Then you can reset the change on the codebase.
|
||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40106080.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40106080.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: Bugfixes.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -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\""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret
|
|||
|
||||
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
||||
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
||||
android.experimental.lint.version=8.0.0-alpha10
|
||||
android.experimental.lint.version=8.3.0-alpha12
|
||||
|
|
|
@ -3,7 +3,6 @@ plugins {
|
|||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
apply from: '../flavor.gradle'
|
||||
|
||||
android {
|
||||
namespace "org.matrix.android.sdk.flow"
|
||||
|
|
|
@ -41,7 +41,6 @@ dokkaHtml {
|
|||
}
|
||||
}
|
||||
}
|
||||
apply from: '../flavor.gradle'
|
||||
|
||||
android {
|
||||
namespace "org.matrix.android.sdk"
|
||||
|
@ -63,7 +62,7 @@ android {
|
|||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.6.6\""
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.6.8\""
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||
|
@ -158,7 +157,7 @@ dependencies {
|
|||
// implementation libs.androidx.appCompat
|
||||
implementation libs.androidx.core
|
||||
|
||||
rustCryptoImplementation libs.androidx.lifecycleLivedata
|
||||
implementation libs.androidx.lifecycleLivedata
|
||||
|
||||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleCommon
|
||||
|
@ -216,8 +215,8 @@ dependencies {
|
|||
|
||||
implementation libs.google.phonenumber
|
||||
|
||||
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.15")
|
||||
// rustCryptoApi project(":library:rustCrypto")
|
||||
implementation("org.matrix.rustcomponents:crypto-android:0.3.16")
|
||||
// api project(":library:rustCrypto")
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
|
|
|
@ -261,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||
return MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
||||
authData = createFakeMegolmBackupAuthData(),
|
||||
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")!!
|
||||
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,14 +28,12 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -197,7 +195,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
|||
|
||||
@Test
|
||||
fun testNeedsRotationFromSharedToWorldReadable() {
|
||||
Assume.assumeTrue("Test is flacky on legacy crypto", BuildConfig.FLAVOR == "rustCrypto")
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
|
||||
}
|
||||
|
||||
|
|
|
@ -542,7 +542,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertFails {
|
||||
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
|
||||
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key")!!,
|
||||
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key"),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -680,7 +680,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
assertFailsWith<InvalidParameterException> {
|
||||
keysBackupService.restoreKeysWithRecoveryKey(
|
||||
keysBackupService.keysBackupVersion!!,
|
||||
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")!!,
|
||||
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
|
@ -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 (deviceId == 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?
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue