Merge pull request #7628 from vector-im/feature/bca/rust_flavor

Merging Element R in Element Android as a new flavor
This commit is contained in:
Valere 2023-04-20 14:13:06 +02:00 committed by GitHub
commit d4d9a1068a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
441 changed files with 24718 additions and 12601 deletions

1
.gitattributes vendored
View File

@ -1,2 +1,3 @@
**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text
**/src/androidTest/assets/*.realm filter=lfs diff=lfs merge=lfs -text
**/matrix-rust-sdk-crypto.aar filter=lfs diff=lfs merge=lfs -text

View File

@ -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 }}Debug $CI_GRADLE_ARG_PROPERTIES
run: ./gradlew assemble${{ matrix.target }}KotlinCryptoDebug $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 assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
run: ./gradlew clean assembleGplayKotlinCryptoRelease $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/gplay/release/vector-gplay-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
args: /github/workspace/gplayKotlinCrypto/release/vector-gplay-kotlinCrypto-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 Normal file
View File

@ -0,0 +1,37 @@
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

View File

@ -34,7 +34,7 @@ jobs:
yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly APK
run: |
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
./gradlew assembleGplayKotlinCryptoNightly appDistributionUploadGplayKotlinCryptoNightly $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 Normal file
View File

@ -0,0 +1,46 @@
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" -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 }}

View File

@ -49,8 +49,10 @@ jobs:
- name: Run lint
# Not always, if ktlint or detekt fail, avoid running the long lint check.
run: |
./gradlew vector-app:lintGplayRelease $CI_GRADLE_ARG_PROPERTIES
./gradlew vector-app:lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES
./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
- name: Upload reports
if: always()
uses: actions/upload-artifact@v3

View File

@ -73,7 +73,7 @@ jobs:
disable-animations: true
# emulator-build: 7425822
script: |
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
./gradlew gatherGplayKotlinCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES

10
.gitignore vendored
View File

@ -12,6 +12,9 @@
/benchmark-out
/captures
.externalNativeBuild
rust-sdk/target/*
rust-sdk/src/uniffi/*
Cargo.lock
/tmp
/fastlane/private
@ -24,3 +27,10 @@
/yarn.lock
/node_modules
**/out/failures
# For manual dependency to rust crypto sdk
matrix-sdk-android/src/main/jniLibs/
matrix-sdk-android/libs/crypto-android-release.aar
matrix-sdk-android/libs/matrix-rust-sdk-crypto.aar

View File

@ -121,6 +121,15 @@ allprojects {
groups.jcenter.group.each { includeGroup it }
}
}
maven {
url 'https://s01.oss.sonatype.org/content/repositories/snapshots'
content {
groups.mavenSnapshots.regex.each { includeGroupByRegex it }
groups.mavenSnapshots.group.each { includeGroup it }
}
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
@ -314,7 +323,7 @@ tasks.register("recordScreenshots", GradleBuild) {
tasks.register("verifyScreenshots", GradleBuild) {
startParameter.projectProperties.screenshot = ""
tasks = [':vector:verifyPaparazziDebug']
tasks = [':vector:verifyPaparazziKotlinCryptoDebug']
}
ext.initScreenshotTests = { project ->

View File

@ -87,5 +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:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
}

View File

@ -1,5 +1,5 @@
ext.groups = [
jitpack : [
jitpack : [
regex: [
],
group: [
@ -15,7 +15,7 @@ ext.groups = [
'com.github.Zhuinden',
]
],
jitsi : [
jitsi : [
regex: [
],
group: [
@ -24,7 +24,7 @@ ext.groups = [
'org.webkit',
]
],
google : [
google : [
regex: [
'androidx\\..*',
'com\\.android\\.tools\\..*',
@ -44,6 +44,13 @@ ext.groups = [
group: [
]
],
mavenSnapshots: [
regex: [
],
group: [
'org.matrix.rustcomponents'
]
],
mavenCentral: [
regex: [
],
@ -204,6 +211,7 @@ ext.groups = [
'org.jvnet.staxex',
'org.maplibre.gl',
'org.matrix.android',
'org.matrix.rustcomponents',
'org.mockito',
'org.mongodb',
'org.objenesis',
@ -223,7 +231,7 @@ ext.groups = [
'xml-apis',
]
],
jcenter : [
jcenter : [
regex: [
],
group: [

View File

@ -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 assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
./gradlew assembleGplayKotlinCryptoNightly appDistributionUploadGplayKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
```
Then you can reset the change on the codebase.

View File

@ -0,0 +1,63 @@
## Overview
Until the final migration to [rust crypto sdk](https://github.com/matrix-org/matrix-rust-components-kotlin), the Element Android project will support two
different SDK as a product flavor.
The `matrix-sdk-android` module is defining a new flavor dimension `crypto`, with two flavors `kotlinCrypto` and `rustCrypto`.
The crypto module cannot be changed at runtime, it's a build time configuration. The app supports migration from kotlinCrypto to rustCrypto but not the other
way around.
The code that is not shared between the flavors is located in dedicated source sets (`src/kotlinCrypto/`, `src/rustCrypto/`). Some tests are also extracted
in different source sets because they were accessing internal API and won't work with the rust crypto sdk.
## Noticeable changes
As a general rule, if you stick to the `kotlinCrypto` the app should behave as it was before the integration of favours.
There is a noticeable exception though:
In order to integrate the rust crypto several APIs had to be migrated from callback code to suspendable code. This change
impacted a lot the key verification engine (user and device verification), so this part has been refactored for `kotlinCrypto`. The UI is also impacted,
the verification flows now match the web experience.
TLDR; Verification UI and engine has been refactored.
## Testing with a local rust aar
In order to run a custom rust SDK branch you can follow the direction in the [bindings repository](https://github.com/matrix-org/matrix-rust-components-kotlin)
in order to build the `matrix-rust-sdk-crypto.aar`.
Copy this lib in `library/rustCrypto/`, and rename it `matrix-rust-sdk-crypto.aar`.
Then go to `matrix-sdk-android/build.gradle` and toggle the comments between the following lines.
````
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.1")
// rustCryptoApi project(":library:rustCrypto")
````
## Changes in CI
The workflow files have been updated to use the `kotlinCrypto` flavor, e.g
`assembleGplayNightly` => `assembleGplayKotlinCryptoNightly`
So building the unsigned release kotlin crypto apk is now:
`> ./gradlew assembleGplayKotlinCryptoRelease`
An additional workflow has been added to build the `rustCrypto` flavor (elementr.yml, ` Build debug APKs ER`).
## Database migration from kotlin to rust
With the kotlin flavor, the crypto information are persisted in the crypto realm database.
With the rust flavor, the crypto information are in a sqllite database.
The migration is handled when injecting `@SessionRustFilesDirectory` in the olmMachine.
When launching the first time after migration, the app will detect that there is no rust data repository and it will
create one. If there is an existing realm database, the data will then migrated to rust. See `ExtractMigrationDataUseCase`.
This will extract your device keys, account secrets, active olm and megolm sessions.
There is no inverse migration for now, as there is not yet rust pickle to olm pickle support in the sdk.
If you migrate your app to rust, and want to revert to kotlin you have to logout then login again.

20
flavor.gradle Normal file
View File

@ -0,0 +1,20 @@
android {
flavorDimensions "crypto"
productFlavors {
kotlinCrypto {
dimension "crypto"
isDefault = true
// versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"JC\""
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"KotlinCrypto\""
}
rustCrypto {
dimension "crypto"
// // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"RC\""
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"RustCrypto\""
}
}
}

View File

@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('matrix-rust-sdk-crypto.aar'))

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c025a7047c3276b09f8cfaddc6323688b4c0174385148aa20f21080ba74d236d
size 32306804

View File

@ -413,7 +413,9 @@
<string name="action_disconnect">Disconnect</string>
<string name="action_play">Play</string>
<string name="action_dismiss">Dismiss</string>
<string name="action_reset">Reset</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="action_reset">Reset</string>
<string name="action_proceed_to_reset">Proceed to reset</string>
<string name="action_learn_more">Learn more</string>
<string name="action_next">Next</string>
<string name="action_got_it">Got it</string>
@ -1644,7 +1646,8 @@
<string name="sas_got_it">Got it</string>
<string name="sas_incoming_request_notif_title">Verification Request</string>
<string name="sas_incoming_request_notif_content">%s wants to verify your session</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="sas_incoming_request_notif_content">%s wants to verify your session</string>
<!-- SAS Errors -->
<string name="sas_error_unknown">Unknown Error</string>
@ -2323,8 +2326,10 @@
<string name="verification_no_scan_emoji_title">Verify by comparing emojis</string>
<string name="verification_verify_user">Verify %s</string>
<string name="verification_verified_user">Verified %s</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verification_verified_user">Verified %s</string>
<string name="verification_request_waiting_for">Waiting for %s…</string>
<string name="verification_request_waiting_for_recovery">Verifying from Secure Key or Phrase…</string>
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
<string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string>
<string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
@ -2394,9 +2399,11 @@
<string name="verification_request_start_notice">To be secure, do this in person or use another way to communicate.</string>
<string name="verification_emoji_notice">Compare the unique emoji, ensuring they appear in the same order.</string>
<string name="verification_code_notice">Compare the code with the one displayed on the other user\'s screen.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verification_code_notice">Compare the code with the one displayed on the other user\'s screen.</string>
<string name="verification_conclusion_ok_notice">Messages with this user are end-to-end encrypted and can\'t be read by third parties.</string>
<string name="verification_conclusion_ok_self_notice">Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verification_conclusion_ok_self_notice">Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.</string>
<string name="encryption_information_cross_signing_state">Cross-Signing</string>
<string name="encryption_information_dg_xsigning_complete">Cross-Signing is enabled\nPrivate Keys on device.</string>
@ -2435,7 +2442,10 @@
<string name="crosssigning_cannot_verify_this_session">Unable to verify this device</string>
<string name="crosssigning_cannot_verify_this_session_desc">You wont be able to access encrypted message history. Reset your Secure Message Backup and verification keys to start fresh.</string>
<string name="verification_open_other_to_verify">Use an existing session to verify this one, granting it access to encrypted messages.</string>
<string name="verification_verify_with_another_device">Verify with another device</string>
<string name="verification_verify_identity">Verify your identity to access encrypted messages and prove your identity to others.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verification_open_other_to_verify">Use an existing session to verify this one, granting it access to encrypted messages.</string>
<string name="verification_profile_verify">Verify</string>
<string name="verification_profile_verified">Verified</string>
@ -2509,20 +2519,28 @@
<string name="new_session">New login. Was this you?</string>
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
<string name="verification_request_was_sent">A verification request has been sent. Open one of your other sessions to accept and start the verification.</string>
<string name="verify_new_session_was_not_me">This wasnt me</string>
<string name="verify_new_session_compromized">Your account may be compromised</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verify_new_session_compromized">Your account may be compromised</string>
<string name="_resume">Resume</string>
<string name="verify_cancel_self_verification_from_untrusted">If you cancel, you wont be able to read encrypted messages on this device, and other users wont trust it</string>
<string name="verify_cancel_self_verification_from_trusted">If you cancel, you wont be able to read encrypted messages on your new device, and other users wont trust it</string>
<string name="verify_cancel_other">You wont verify %1$s (%2$s) if you cancel now. Start again in their user profile.</string>
<string name="verify_not_me_self_verification">
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verify_not_me_self_verification">
One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password &amp; recovery key in Settings immediately.
</string>
<string name="verify_cancelled_notice">Verification has been canceled. You can start verification again.</string>
<string name="verify_invalid_qr_notice">This QR code looks malformed. Please try to verify with another method.</string>
<string name="verification_cancelled">Verification Canceled</string>
<string name="verification_not_found">The verification request was not found. It may have been cancelled, or handled by another session.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verify_invalid_qr_notice">This QR code looks malformed. Please try to verify with another method.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="verification_cancelled">Verification Canceled</string>
<string name="recovery_passphrase">Recovery Passphrase</string>
<string name="message_key">Message Key</string>
@ -2650,8 +2668,12 @@
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
<string name="secure_backup_reset_all">Reset everything</string>
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
<string name="secure_backup_reset_all_no_other_devices_long">Resetting your verification keys cannot be undone. After resetting, you won\'t have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
<string name="secure_backup_reset_danger_warning">Please only proceed if you\'re sure you\'ve lost all of your other devices and your security key.</string>
<plurals name="secure_backup_reset_devices_you_can_verify">
<item quantity="one">Show the device you can verify with now</item>
<item quantity="other">Show %d devices you can verify with now</item>
@ -2666,6 +2688,7 @@
<string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="encrypted_by_deleted">Encrypted by a deleted device</string>
<string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string>
<string name="review_unverified_sessions_title">You have unverified sessions</string>
<string name="review_unverified_sessions_description">Review to ensure your account is safe</string>
@ -2673,7 +2696,8 @@
<string name="verify_this_session">Verify the new login accessing your account: %1$s</string>
<string name="cross_signing_verify_by_text">Manually Verify by Text</string>
<string name="crosssigning_verify_session">Verify login</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="crosssigning_verify_session">Verify login</string>
<string name="cross_signing_verify_by_emoji">Interactively Verify by Emoji</string>
<string name="confirm_your_identity">Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.</string>
<string name="confirm_your_identity_quad_s">Confirm your identity by verifying this login, granting it access to encrypted messages.</string>

View File

@ -3,6 +3,7 @@ plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
apply from: '../flavor.gradle'
android {
namespace "org.matrix.android.sdk.flow"
@ -30,11 +31,23 @@ android {
kotlinOptions {
jvmTarget = "11"
}
// publishNonDefault true
}
//configurations {
// kotlinCryptoDebugImplementation
// kotlinCryptoReleaseImplementation
// rustCryptoDebugImplementation
// rustCryptoReleaseImplementation
//}
dependencies {
implementation project(":matrix-sdk-android")
// kotlinCryptoDebugImplementation project(path: ":matrix-sdk-android", configuration :"kotlinCryptoDebug")
// kotlinCryptoReleaseImplementation project(path: ":matrix-sdk-android", configuration :"kotlinCryptoRelease")
// rustCryptoDebugImplementation project(path: ":matrix-sdk-android", configuration :"rustCryptoDebug")
// rustCryptoReleaseImplementation project(path: ":matrix-sdk-android", configuration :"rustCryptoDebug")
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
implementation libs.androidx.lifecycleLivedata

View File

@ -41,6 +41,7 @@ dokkaHtml {
}
}
}
apply from: '../flavor.gradle'
android {
namespace "org.matrix.android.sdk"
@ -75,7 +76,7 @@ android {
testOptions {
// Comment to run on Android 12
// execution 'ANDROIDX_TEST_ORCHESTRATOR'
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
buildTypes {
@ -124,6 +125,7 @@ android {
java.srcDirs += "src/sharedTest/java"
}
}
}
static def gitRevision() {
@ -141,12 +143,23 @@ static def gitRevisionDate() {
return cmd.execute().text.trim()
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid
// implementation(name: 'crypto-android-release', ext: 'aar')
implementation 'net.java.dev.jna:jna:5.10.0@aar'
// implementation libs.androidx.appCompat
implementation libs.androidx.core
rustCryptoImplementation libs.androidx.lifecycleLivedata
// Lifecycle
implementation libs.androidx.lifecycleCommon
implementation libs.androidx.lifecycleProcess
@ -203,6 +216,9 @@ dependencies {
implementation libs.google.phonenumber
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.1")
// rustCryptoApi project(":library:rustCrypto")
testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
testImplementation libs.mockk.mockk

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59b4957aa2f9cdc17b14ec8546e144537fac9dee050c6eb173f56fa8602c2736
size 2097152

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -44,6 +45,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoomSummary
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.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
@ -82,7 +84,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
}
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context, cryptoConfig)
val cryptoTestHelper = CryptoTestHelper(testHelper)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
@ -181,6 +183,110 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
return sentEvents
}
suspend fun sendMessageInRoom(room: Room, text: String): String {
Log.v("#E2E TEST", "sendMessageInRoom room:${room.roomId} <$text>")
room.sendService().sendTextMessage(text)
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
val messageSent = CompletableDeferred<String>()
timeline.addListener(object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also { list ->
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
Log.v("#E2E TEST", "Timeline snapshot is $message")
}
.filter { it.root.sendState == SendState.SYNCED }
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
if (decryptedMsg != null) {
timeline.dispose()
messageSent.complete(decryptedMsg.eventId)
}
}
})
return messageSent.await().also {
Log.v("#E2E TEST", "Message <${text}> sent and synced with id $it")
}
// return withTimeout(TestConstants.timeOutMillis) { messageSent.await() }
}
suspend fun ensureMessage(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)) {
Log.v("#E2E TEST", "ensureMessage room:${room.roomId} <$eventId>")
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60, buildReadReceipts = false))
// check if not already there?
val existing = withContext(Dispatchers.Main) {
room.getTimelineEvent(eventId)
}
if (existing != null && block(existing)) return Unit.also {
Log.v("#E2E TEST", "Already received")
}
val messageSent = CompletableDeferred<Unit>()
timeline.addListener(object : Timeline.Listener {
override fun onNewTimelineEvents(eventIds: List<String>) {
Log.v("#E2E TEST", "onNewTimelineEvents snapshot is $eventIds")
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val success = timeline.getSnapshot()
// .filter { it.root.getClearType() == EventType.MESSAGE }
.also { list ->
val message = list.joinToString(",", "[", "]") {
"${it.eventId}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}"
}
Log.v("#E2E TEST", "Timeline snapshot is $message")
}
.firstOrNull { it.eventId == eventId }
?.let {
block(it)
} ?: false
if (success) {
messageSent.complete(Unit)
timeline.dispose()
}
}
})
timeline.start()
return messageSent.await()
// withTimeout(TestConstants.timeOutMillis) {
// messageSent.await()
// }
}
fun ensureMessagePromise(room: Room, eventId: String, block: ((event: TimelineEvent) -> Boolean)): CompletableDeferred<Unit> {
val timeline = room.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
val messageSent = CompletableDeferred<Unit>()
timeline.addListener(object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val success = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also { list ->
val message = list.joinToString(",", "[", "]") {
"${it.root.type}|${it.root.getClearType()}|${it.root.sendState}|${it.root.mxDecryptionResult?.verificationState}"
}
Log.v("#E2E TEST", "Promise Timeline snapshot is $message")
}
.firstOrNull { it.eventId == eventId }
?.let {
block(it)
} ?: false
if (success) {
messageSent.complete(Unit)
timeline.dispose()
}
}
})
return messageSent
}
/**
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
*/
@ -239,18 +345,18 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
}
suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
retryPeriodically {
retryWithBackoff {
val roomSummary = otherSession.getRoomSummary(roomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("# TEST", "${otherSession.myUserId} can see the invite")
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite")
}
}
}
// not sure why it's taking so long :/
wrapWithTimeout(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
Log.v("#E2E TEST", "${otherSession.myUserId.take(10)} tries to join room $roomID")
try {
otherSession.roomService().joinRoom(roomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
@ -259,7 +365,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
retryPeriodically {
retryWithBackoff {
val roomSummary = otherSession.getRoomSummary(roomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
@ -432,6 +538,31 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
}
}
private val backoff = listOf(60L, 75L, 100L, 300L, 300L, 500L, 1_000L, 1_000L, 1_500L, 1_500L, 3_000L)
suspend fun retryWithBackoff(
timeout: Long = TestConstants.timeOutMillis,
// we use on fail to let caller report a proper error that will show nicely in junit test result with correct line
// just call fail with your message
onFail: (() -> Unit)? = null,
predicate: suspend () -> Boolean,
) {
var backoffTry = 0
val now = System.currentTimeMillis()
while (!predicate()) {
Timber.v("## retryWithBackoff Trial nb $backoffTry")
withContext(Dispatchers.IO) {
delay(backoff[backoffTry])
}
backoffTry++
if (backoffTry >= backoff.size) backoffTry = 0
if (System.currentTimeMillis() - now > timeout) {
Timber.v("## retryWithBackoff Trial fail")
onFail?.invoke()
return
}
}
}
suspend fun <T> waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
return wrapWithTimeout(timeout) {
suspendCoroutine { continuation ->

View File

@ -37,4 +37,10 @@ data class CryptoTestData(
testHelper.signOutAndClose(it)
}
}
suspend fun initializeCrossSigning(testHelper: CryptoTestHelper) {
sessions.forEach {
testHelper.initializeCrossSigning(it)
}
}
}

View File

@ -17,6 +17,13 @@
package org.matrix.android.sdk.common
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.launch
import org.amshove.kluent.fail
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -33,18 +40,23 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
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.VerificationTxState
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -52,7 +64,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@ -121,6 +132,82 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession))
}
suspend fun inviteNewUsersAndWaitForThemToJoin(session: Session, roomId: String, usernames: List<String>): List<Session> {
val newSessions = usernames.map { username ->
testHelper.createAccount(username, SessionTestParams(true)).also {
if (it.cryptoService().supportsDisablingKeyGossiping()) {
it.cryptoService().enableKeyGossiping(false)
}
}
}
val room = session.getRoom(roomId)!!
Log.v("#E2E TEST", "accounts for ${usernames.joinToString(",") { it.take(10) }} created")
// we want to invite them in the room
newSessions.forEach { newSession ->
Log.v("#E2E TEST", "${session.myUserId.take(10)} invites ${newSession.myUserId.take(10)}")
room.membershipService().invite(newSession.myUserId)
}
// All user should accept invite
newSessions.forEach { newSession ->
waitForAndAcceptInviteInRoom(newSession, roomId)
Log.v("#E2E TEST", "${newSession.myUserId.take(10)} joined room $roomId")
}
ensureMembersHaveJoined(session, newSessions, roomId)
return newSessions
}
private suspend fun ensureMembersHaveJoined(session: Session, invitedUserSessions: List<Session>, roomId: String) {
testHelper.retryWithBackoff(
onFail = {
fail("Members ${invitedUserSessions.map { it.myUserId.take(10) }} should have join from the pov of ${session.myUserId.take(10)}")
}
) {
invitedUserSessions.map { invitedUserSession ->
session.roomService().getRoomMember(invitedUserSession.myUserId, roomId)?.membership?.also {
Log.v("#E2E TEST", "${invitedUserSession.myUserId.take(10)} membership is $it")
}
}.all {
it == Membership.JOIN
}
}
}
private suspend fun waitForAndAcceptInviteInRoom(session: Session, roomId: String) {
testHelper.retryWithBackoff(
onFail = {
fail("${session.myUserId} cannot see the invite from ${session.myUserId.take(10)}")
}
) {
val roomSummary = session.getRoomSummary(roomId)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("#E2E TEST", "${session.myUserId.take(10)} can see the invite from ${roomSummary?.inviterId}")
}
}
}
// not sure why it's taking so long :/
Log.v("#E2E TEST", "${session.myUserId.take(10)} tries to join room $roomId")
try {
session.roomService().joinRoom(roomId)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
Log.v("#E2E TEST", "${session.myUserId} waiting for join echo ...")
testHelper.retryWithBackoff(
onFail = {
fail("${session.myUserId.take(10)} cannot see the join echo for ${roomId}")
}
) {
val roomSummary = session.getRoomSummary(roomId)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
/**
* @return Alice and Bob sessions
*/
@ -137,37 +224,22 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
// Alice sends a message
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1).first().eventId.let { sentEventId ->
// ensure bob got it
ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
}
ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromAlicePOV, messagesFromAlice[0]), bobSession, true)
// Bob send 3 messages
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
}
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
}
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
for (msg in messagesFromBob) {
ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromBobPOV, msg), aliceSession, true)
}
// Alice sends a message
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1).first().eventId.let { sentEventId ->
// ensure bob got it
ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
}
ensureEventReceived(aliceRoomId, testHelper.sendMessageInRoom(roomFromAlicePOV, messagesFromAlice[1]), bobSession, true)
return cryptoTestData
}
private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
Log.d("#E2E", "ensureEventReceived $eventId => ${timeLineEvent?.senderInfo?.userId}| ${timeLineEvent?.root?.getClearType()}")
if (andCanDecrypt) {
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -189,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
authData = createFakeMegolmBackupAuthData(),
recoveryKey = "fake"
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")!!
)
}
@ -221,7 +293,6 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
}
suspend fun initializeCrossSigning(session: Session) {
testHelper.waitForCallback<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
@ -234,9 +305,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
)
)
}
}, it
)
}
})
}
/**
@ -272,16 +341,13 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
)
// set up megolm backup
val creationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = testHelper.waitForCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
val creationInfo = session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null)
val version = session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo)
// Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
creationInfo.recoveryKey.toBase64().let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
@ -291,82 +357,262 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
}
suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
val scope = CoroutineScope(SupervisorJob())
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService()
val localId = UUID.randomUUID().toString()
aliceVerificationService.requestKeyVerificationInDMs(
localId = localId,
val bobSeesVerification = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request != null) {
bobSeesVerification.complete(request)
return@collect cancel()
}
}
}
val aliceReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
aliceReady.complete(request)
return@collect cancel()
}
}
}
val bobReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
bobReady.complete(request)
return@collect cancel()
}
}
}
val requestID = aliceVerificationService.requestKeyVerificationInDMs(
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
otherUserId = bob.myUserId,
roomId = roomId
).transactionId
testHelper.retryPeriodically {
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
} != null
}
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
}
bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, incomingRequest.transactionId!!)
bobSeesVerification.await()
bobVerificationService.readyPendingVerification(
listOf(VerificationMethod.SAS),
alice.myUserId,
requestID
)
aliceReady.await()
bobReady.await()
var requestID: String? = null
// wait for it to be readied
testHelper.retryPeriodically {
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
.firstOrNull { it.localId == localId }
if (outgoingRequest?.isReady == true) {
requestID = outgoingRequest.transactionId!!
true
} else {
false
}
val bobCode = CompletableDeferred<SasVerificationTransaction>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val transaction = it.getTransaction()
Log.v("#E2E TEST", "#TEST flow ${bob.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val tx = transaction as? SasVerificationTransaction
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
Log.v("#E2E TEST", "COMPLETE BOB CODE")
bobCode.complete(tx)
return@collect cancel()
}
if (it.getRequest()?.state == EVerificationState.Cancelled) {
Log.v("#E2E TEST", "EXCEPTION BOB CODE")
bobCode.completeExceptionally(AssertionError("Request as been cancelled"))
return@collect cancel()
}
}
}
aliceVerificationService.beginKeyVerificationInDMs(
val aliceCode = CompletableDeferred<SasVerificationTransaction>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val transaction = it.getTransaction()
Log.v("#E2E TEST", "#TEST flow ${alice.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val tx = transaction as? SasVerificationTransaction
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
Log.v("#E2E TEST", "COMPLETE ALICE CODE")
aliceCode.complete(tx)
return@collect cancel()
}
if (it.getRequest()?.state == EVerificationState.Cancelled) {
Log.v("#E2E TEST", "EXCEPTION ALICE CODE")
aliceCode.completeExceptionally(AssertionError("Request as been cancelled"))
return@collect cancel()
}
}
}
Log.v("#E2E TEST", "#TEST let alice start the verification")
val id = aliceVerificationService.startKeyVerification(
VerificationMethod.SAS,
requestID!!,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!
requestID,
)
Log.v("#E2E TEST", "#TEST alice started: $id")
val bobTx = bobCode.await()
val aliceTx = aliceCode.await()
Log.v("#E2E TEST", "#TEST Alice code ${aliceTx.getDecimalCodeRepresentation()}")
Log.v("#E2E TEST", "#TEST Bob code ${bobTx.getDecimalCodeRepresentation()}")
assertEquals("SAS code do not match", aliceTx.getDecimalCodeRepresentation()!!, bobTx.getDecimalCodeRepresentation())
val aliceDone = CompletableDeferred<Unit>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val transaction = it.getTransaction()
Log.v("#E2E TEST", "#TEST flow ${alice.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val request = it.getRequest()
Log.v("#E2E TEST", "#TEST flow request ${alice.myUserId.take(5)} ${request?.transactionId}|${request?.state}")
if (request?.state == EVerificationState.Done || request?.state == EVerificationState.WaitingForDone) {
aliceDone.complete(Unit)
return@collect cancel()
}
}
}
val bobDone = CompletableDeferred<Unit>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val transaction = it.getTransaction()
Log.v("#E2E TEST", "#TEST flow ${bob.myUserId.take(5)} ${transaction?.transactionId}|${transaction?.dbgState()}")
val request = it.getRequest()
Log.v("#E2E TEST", "#TEST flow request ${bob.myUserId.take(5)} ${request?.transactionId}|${request?.state}")
if (request?.state == EVerificationState.Done || request?.state == EVerificationState.WaitingForDone) {
bobDone.complete(Unit)
return@collect cancel()
}
}
}
Log.v("#E2E TEST", "#TEST Bob confirm sas code")
bobTx.userHasVerifiedShortCode()
Log.v("#E2E TEST", "#TEST Alice confirm sas code")
aliceTx.userHasVerifiedShortCode()
Log.v("#E2E TEST", "#TEST Waiting for Done..")
bobDone.await()
aliceDone.await()
Log.v("#E2E TEST", "#TEST .. ok")
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
scope.cancel()
}
suspend fun verifyNewSession(oldDevice: Session, newDevice: Session) {
val scope = CoroutineScope(SupervisorJob())
assertTrue(oldDevice.cryptoService().crossSigningService().canCrossSign())
val verificationServiceOld = oldDevice.cryptoService().verificationService()
val verificationServiceNew = newDevice.cryptoService().verificationService()
val oldSeesVerification = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
verificationServiceOld.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
Log.d("#E2E", "Verification request received: $request")
if (request != null) {
oldSeesVerification.complete(request)
return@collect cancel()
}
}
}
val newReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
verificationServiceNew.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
Log.d("#E2E", "new state: ${request?.state}")
if (request?.state == EVerificationState.Ready) {
newReady.complete(request)
return@collect cancel()
}
}
}
val txId = verificationServiceNew.requestSelfKeyVerification(listOf(VerificationMethod.SAS)).transactionId
oldSeesVerification.await()
verificationServiceOld.readyPendingVerification(
listOf(VerificationMethod.SAS),
oldDevice.myUserId,
txId
)
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
newReady.await()
testHelper.retryPeriodically {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
// wait for alice to get the ready
testHelper.retryPeriodically {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
}
bobPovTx?.state == VerificationTxState.ShortCodeReady
val newConfirmed = CompletableDeferred<Unit>()
scope.launch(Dispatchers.IO) {
verificationServiceNew.requestEventFlow()
.cancellable()
.collect {
val tx = it.getTransaction() as? SasVerificationTransaction
Log.d("#E2E", "new tx state: ${tx?.state()}")
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
tx.userHasVerifiedShortCode()
newConfirmed.complete(Unit)
return@collect cancel()
}
}
}
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
bobPovTx!!.userHasVerifiedShortCode()
alicePovTx!!.userHasVerifiedShortCode()
testHelper.retryPeriodically {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
val oldConfirmed = CompletableDeferred<Unit>()
scope.launch(Dispatchers.IO) {
verificationServiceOld.requestEventFlow()
.cancellable()
.collect {
val tx = it.getTransaction() as? SasVerificationTransaction
Log.d("#E2E", "old tx state: ${tx?.state()}")
if (tx?.state() == SasTransactionState.SasShortCodeReady) {
tx.userHasVerifiedShortCode()
oldConfirmed.complete(Unit)
return@collect cancel()
}
}
}
verificationServiceNew.startKeyVerification(VerificationMethod.SAS, newDevice.myUserId, txId)
newConfirmed.await()
oldConfirmed.await()
testHelper.retryPeriodically {
bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
oldDevice.cryptoService().crossSigningService().isCrossSigningVerified()
}
Log.d("#E2E", "New session is trusted")
}
suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {
@ -393,9 +639,9 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root
?: return@retryPeriodically false
?: return@retryWithBackoff false
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
@ -403,13 +649,13 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
verificationState = result.messageVerificationState
)
}
} catch (error: MXCryptoError) {
// nop
}
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
Log.v("#E2E TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
event.getClearType() == EventType.MESSAGE &&
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
}

View File

@ -46,7 +46,7 @@ class DecryptRedactedEventTest : InstrumentedTest {
roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
// get the event from bob
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
}

View File

@ -20,6 +20,7 @@ import android.util.Log
import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -27,11 +28,8 @@ import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -62,15 +60,15 @@ class E2EShareKeysConfigTest : InstrumentedTest {
enableEncryption()
})
commonTestHelper.retryPeriodically {
commonTestHelper.retryWithBackoff {
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
// send some messages
val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
val withSession1 = commonTestHelper.sendMessageInRoom(roomAlice, "Hello")
aliceSession.cryptoService().discardOutboundSession(roomId)
val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
val withSession2 = commonTestHelper.sendMessageInRoom(roomAlice, "World")
// Create bob account
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
@ -82,7 +80,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
// Bob has join but should not be able to decrypt history
cryptoTestHelper.ensureCannotDecrypt(
withSession1.map { it.eventId } + withSession2.map { it.eventId },
listOf(withSession1, withSession2),
bobSession,
roomId
)
@ -90,44 +88,53 @@ class E2EShareKeysConfigTest : InstrumentedTest {
// We don't need bob anymore
commonTestHelper.signOutAndClose(bobSession)
// Now let's enable history key sharing on alice side
aliceSession.cryptoService().enableShareKeyOnInvite(true)
if (aliceSession.cryptoService().supportsShareKeysOnInvite()) {
// Now let's enable history key sharing on alice side
aliceSession.cryptoService().enableShareKeyOnInvite(true)
// let's add a new message first
val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1)
// let's add a new message first
val afterFlagOn = commonTestHelper.sendMessageInRoom(roomAlice, "After")
// Worth nothing to check that the session was rotated
Assert.assertNotEquals(
"Session should have been rotated",
withSession2.first().root.content?.get("session_id")!!,
afterFlagOn.first().root.content?.get("session_id")!!
)
// Worth nothing to check that the session was rotated
Assert.assertNotEquals(
"Session should have been rotated",
aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(withSession1)?.root?.content?.get("session_id")!!,
aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(afterFlagOn)?.root?.content?.get("session_id")!!
)
// Invite a new user
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Invite a new user
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam
roomAlice.membershipService().invite(samSession.myUserId)
// Let alice invite sam
roomAlice.membershipService().invite(samSession.myUserId)
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
cryptoTestHelper.ensureCannotDecrypt(
withSession1.map { it.eventId } + withSession2.map { it.eventId },
samSession,
roomId
)
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
cryptoTestHelper.ensureCannotDecrypt(
listOf(withSession1, withSession2),
samSession,
roomId
)
cryptoTestHelper.ensureCanDecrypt(
afterFlagOn.map { it.eventId },
samSession,
roomId,
afterFlagOn.map { it.root.getClearContent()?.get("body") as String })
cryptoTestHelper.ensureCanDecrypt(
listOf(afterFlagOn),
samSession,
roomId,
listOf(aliceSession.roomService().getRoom(roomId)?.getTimelineEvent(afterFlagOn)?.root?.getClearContent()?.get("body") as String)
)
}
}
@Test
fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
Assume.assumeTrue("Shared key on invite needed to test this",
testData.firstSession.cryptoService().supportsShareKeysOnInvite()
)
val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(false)
}
@ -155,6 +162,11 @@ class E2EShareKeysConfigTest : InstrumentedTest {
@Test
fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
Assume.assumeTrue("Shared key on invite needed to test this",
testData.firstSession.cryptoService().supportsShareKeysOnInvite()
)
val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
@ -197,6 +209,11 @@ class E2EShareKeysConfigTest : InstrumentedTest {
@Test
fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
Assume.assumeTrue("Shared key on invite needed to test this",
aliceSession.cryptoService().supportsShareKeysOnInvite()
)
aliceSession.cryptoService().enableShareKeyOnInvite(false)
val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = RoomHistoryVisibility.SHARED
@ -204,75 +221,85 @@ class E2EShareKeysConfigTest : InstrumentedTest {
enableEncryption()
})
commonTestHelper.retryPeriodically {
commonTestHelper.retryWithBackoff {
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
// send some messages
val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
val notSharableMessage = commonTestHelper.sendMessageInRoom(roomAlice, "Hello")
aliceSession.cryptoService().enableShareKeyOnInvite(true)
val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
val sharableMessage = commonTestHelper.sendMessageInRoom(roomAlice, "World")
Log.v("#E2E TEST", "Create and start key backup for bob ...")
val keysBackupService = aliceSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = commonTestHelper.waitForCallback<KeysVersion> {
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
commonTestHelper.waitForCallback<Unit> {
keysBackupService.backupAllGroupSessions(null, it)
val megolmBackupCreationInfo = keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null)
val version = keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo)
Log.v("#E2E TEST", "... Backup created.")
commonTestHelper.retryPeriodically {
Log.v("#E2E TEST", "Backup status ${keysBackupService.getTotalNumbersOfBackedUpKeys()}/${keysBackupService.getTotalNumbersOfKeys()}")
keysBackupService.getTotalNumbersOfKeys() == keysBackupService.getTotalNumbersOfBackedUpKeys()
}
val aliceId = aliceSession.myUserId
// signout
Log.v("#E2E TEST", "Sign out alice")
commonTestHelper.signOutAndClose(aliceSession)
val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
Log.v("#E2E TEST", "Sign in a new alice device")
val newAliceSession = commonTestHelper.logIntoAccount(aliceId, SessionTestParams(true))
newAliceSession.cryptoService().enableShareKeyOnInvite(true)
newAliceSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = commonTestHelper.waitForCallback<KeysVersionResult?> {
kbs.getVersion(version.version, it)
}
val keyVersionResult = kbs.getVersion(version.version)
val importedResult = commonTestHelper.waitForCallback<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword(
Log.v("#E2E TEST", "Restore new backup")
val importedResult = kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
null,
it
)
}
assertEquals(2, importedResult.totalNumberOfKeys)
}
// Now let's invite sam
// Invite a new user
Log.v("#E2E TEST", "Create Sam account")
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam
Log.v("#E2E TEST", "Let alice invite sam")
newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
cryptoTestHelper.ensureCannotDecrypt(
notSharableMessage.map { it.eventId },
listOf(notSharableMessage),
samSession,
roomId
)
cryptoTestHelper.ensureCanDecrypt(
sharableMessage.map { it.eventId },
listOf(sharableMessage),
samSession,
roomId,
sharableMessage.map { it.root.getClearContent()?.get("body") as String })
listOf(newAliceSession.getRoom(roomId)!!
.getTimelineEvent(sharableMessage)
?.root
?.getClearContent()
?.get("body") as String
)
)
}
}

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
@ -52,43 +53,46 @@ class E2eeConfigTest : InstrumentedTest {
val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
val sentMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you are blocked")
val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
// ensure other received
testHelper.retryPeriodically {
roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
}
testHelper.ensureMessage(roomBobPOV, sentMessage) { true }
cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId)
cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage), cryptoTestData.secondSession!!, cryptoTestData.roomId)
}
@Test
fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
Log.v("#E2E TEST", "Initializing cross signing for alice and bob...")
cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession)
cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!)
Log.v("#E2E TEST", "... Initialized")
Log.v("#E2E TEST", "Start User Verification")
cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId)
cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
Log.v("#E2E TEST", "Send message in room")
val sentMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you can read")
val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
// ensure other received
testHelper.retryPeriodically {
roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
}
testHelper.ensureMessage(roomBobPOV, sentMessage) { true }
cryptoTestHelper.ensureCanDecrypt(
listOf(sentMessage.eventId),
listOf(sentMessage),
cryptoTestData.secondSession!!,
cryptoTestData.roomId,
listOf(sentMessage.getLastMessageContent()!!.body)
listOf(
roomBobPOV.timelineService().getTimelineEvent(sentMessage)?.getLastMessageContent()!!.body
)
)
}
@ -98,32 +102,34 @@ class E2eeConfigTest : InstrumentedTest {
val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
val beforeMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you can read")
val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
// ensure other received
testHelper.retryPeriodically {
roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null
}
Log.v("#E2E TEST", "Wait for bob to get the message")
testHelper.ensureMessage(roomBobPOV, beforeMessage) { true }
Log.v("#E2E TEST", "ensure bob Can Decrypt first message")
cryptoTestHelper.ensureCanDecrypt(
listOf(beforeMessage.eventId),
listOf(beforeMessage),
cryptoTestData.secondSession!!,
cryptoTestData.roomId,
listOf(beforeMessage.getLastMessageContent()!!.body)
listOf("you can read")
)
Log.v("#E2E TEST", "setRoomBlockUnverifiedDevices true")
cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true)
val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
Log.v("#E2E TEST", "let alice send the message")
val afterMessage = testHelper.sendMessageInRoom(roomAlicePOV, "you are blocked")
// ensure received
testHelper.retryPeriodically {
cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null
}
Log.v("#E2E TEST", "Ensure bob received second message")
testHelper.ensureMessage(roomBobPOV, afterMessage) { true }
cryptoTestHelper.ensureCannotDecrypt(
listOf(afterMessage.eventId),
listOf(afterMessage),
cryptoTestData.secondSession!!,
cryptoTestData.roomId,
MXCryptoError.ErrorType.KEYS_WITHHELD

View File

@ -18,12 +18,7 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
@ -40,27 +35,13 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
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.Room
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
@ -92,79 +73,56 @@ class E2eeSanityTests : InstrumentedTest {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
// we want to disable key gossiping to just check initial sending of keys
aliceSession.cryptoService().enableKeyGossiping(false)
cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false)
if (aliceSession.cryptoService().supportsDisablingKeyGossiping()) {
aliceSession.cryptoService().enableKeyGossiping(false)
}
if (cryptoTestData.secondSession?.cryptoService()?.supportsDisablingKeyGossiping() == true) {
cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false)
}
// add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true)).also {
it.cryptoService().enableKeyGossiping(false)
}
.let {
cryptoTestHelper.inviteNewUsersAndWaitForThemToJoin(aliceSession, e2eRoomID, it)
}
Log.v("#E2E TEST", "All accounts created")
// we want to invite them in the room
otherAccounts.forEach {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.membershipService().invite(it.myUserId)
}
// All user should accept invite
otherAccounts.forEach { otherSession ->
testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
// check that alice see them as joined (not really necessary?)
ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID)
Log.v("#E2E TEST", "All users have joined the room")
Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my message"
val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text)
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null)
val sentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, text)
Log.v("#E2E TEST", "Alice just sent message with id:$sentEventId")
// All should be able to decrypt
otherAccounts.forEach { otherSession ->
testHelper.retryPeriodically {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
timeLineEvent.root.mxDecryptionResult?.isSafe == true
val room = otherSession.getRoom(e2eRoomID)!!
testHelper.ensureMessage(room, sentEventId) {
it.isEncrypted() &&
it.root.getClearType() == EventType.MESSAGE &&
it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
}
}
Log.v("#E2E TEST", "Everybody received the encrypted message and could decrypt")
// Add a new user to the room, and check that he can't decrypt
Log.v("#E2E TEST", "Create some new accounts and invite them")
val newAccount = listOf("adam") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
.let {
cryptoTestHelper.inviteNewUsersAndWaitForThemToJoin(aliceSession, e2eRoomID, it)
}
newAccount.forEach {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.membershipService().invite(it.myUserId)
}
newAccount.forEach {
testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
}
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit
delay(3_000)
// check that messages are encrypted (uisi)
newAccount.forEach { otherSession ->
testHelper.retryPeriodically {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
testHelper.retryWithBackoff(
onFail = {
fail("New Users shouldn't be able to decrypt history")
}
) {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId).also {
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timelineEvent != null &&
@ -177,12 +135,17 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message"
val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage)
val secondSentEventId: String = testHelper.sendMessageInRoom(aliceRoomPOV, secondMessage)
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
testHelper.retryPeriodically {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
// ("${otherSession.myUserId} should be able to decrypt")
testHelper.retryWithBackoff(
onFail = {
fail("New user ${otherSession.myUserId.take(10)} should be able to decrypt the second message")
}
) {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId).also {
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timelineEvent != null &&
@ -223,13 +186,10 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Create and start key backup for bob ...")
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = testHelper.waitForCallback<KeysVersion> {
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
Log.v("#E2E TEST", "... Key backup started and enabled for bob")
val megolmBackupCreationInfo = bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null)
val version = bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo)
Log.v("#E2E TEST", "... Key backup started and enabled for bob: version:$version")
// Bob session should now have
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
@ -238,11 +198,15 @@ class E2eeSanityTests : InstrumentedTest {
val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also {
sentEventIds.add(it)
}
testHelper.retryPeriodically {
testHelper.retryWithBackoff(
onFail = {
fail("Bob should be able to decrypt all messages")
}
) {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -256,7 +220,14 @@ class E2eeSanityTests : InstrumentedTest {
// Let's wait a bit to be sure that bob has backed up the session
Log.v("#E2E TEST", "Force key backup for Bob...")
testHelper.waitForCallback<Unit> { bobKeysBackupService.backupAllGroupSessions(null, it) }
testHelper.retryWithBackoff(
onFail = {
fail("All keys should be backedup")
}
) {
Log.v("#E2E TEST", "backedUp=${ bobKeysBackupService.getTotalNumbersOfBackedUpKeys()}, known=${bobKeysBackupService.getTotalNumbersOfKeys()}")
bobKeysBackupService.getTotalNumbersOfBackedUpKeys() == bobKeysBackupService.getTotalNumbersOfKeys()
}
Log.v("#E2E TEST", "... Key backup done for Bob")
// Now lets logout both alice and bob to ensure that we won't have any gossiping
@ -276,7 +247,7 @@ class E2eeSanityTests : InstrumentedTest {
// check that bob can't currently decrypt
Log.v("#E2E TEST", "check that bob can't currently decrypt")
sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
}
@ -284,37 +255,41 @@ class E2eeSanityTests : InstrumentedTest {
}
}
// after initial sync events are not decrypted, so we have to try manually
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// TODO CHANGE WHEN AVAILABLE FROM RUST
cryptoTestHelper.ensureCannotDecrypt(
sentEventIds,
newBobSession,
e2eRoomID,
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
) // MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// Let's now import keys from backup
Log.v("#E2E TEST", "Restore backup for the new session")
newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.waitForCallback<KeysVersionResult?> {
kbs.getVersion(version.version, it)
}
val keyVersionResult = kbs.getVersion(version.version)
val importedResult = testHelper.waitForCallback<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
null,
it
)
}
val importedResult = kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
null,
)
assertEquals(3, importedResult.totalNumberOfKeys)
}
// ensure bob can now decrypt
Log.v("#E2E TEST", "Check that bob can decrypt now")
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
// Check key trust
Log.v("#E2E TEST", "Check key safety")
sentEventIds.forEach { sentEventId ->
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!!
val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
assertEquals("Keys from history should be deniable", false, result.isSafe)
assertEquals("Keys from history should be deniable", MessageVerificationState.UNSAFE_SOURCE, result.messageVerificationState)
}
}
@ -338,11 +313,15 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
val sentEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text).also {
sentEventIds.add(it)
}
testHelper.retryPeriodically {
testHelper.retryWithBackoff(
onFail = {
fail("${bobSession.myUserId.take(10)} should be able to decrypt message sent by alice}")
}
) {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -358,52 +337,40 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Create a new session for Bob")
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// ensure first session is aware of the new one
bobSession.cryptoService().downloadKeysIfNeeded(listOf(bobSession.myUserId), true)
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Try to request
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
newBobSession.cryptoService().requestRoomKeyForEvent(event)
}
// Ensure that new bob still can't decrypt (keys must have been withheld)
//
// Log.v("#E2E TEST", "Let bob re-request")
// sentEventIds.forEach { sentEventId ->
// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
// .getTimelineEvent(sentEventId)!!
// .root.content.toModel<EncryptedEventContent>()!!.sessionId
// testHelper.retryPeriodically {
// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .first {
// it.sessionId == megolmSessionId &&
// it.roomId == e2eRoomID
// }
// .results.also {
// Log.w("##TEST", "result list is $it")
// }
// .firstOrNull { it.userId == aliceSession.myUserId }
// ?.result
// aliceReply != null &&
// aliceReply is RequestResult.Failure &&
// WithHeldCode.UNAUTHORISED == aliceReply.code
// }
// val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
// newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
// }
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
//
// Log.v("#E2E TEST", "Should not be able to decrypt as not verified")
// cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Now mark new bob session as verified
bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
Log.v("#E2E TEST", "Mark all as verified")
bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId)
newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId)
// now let new session re-request
Log.v("#E2E TEST", "Re-request")
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
}
Log.v("#E2E TEST", "Now should be able to decrypt")
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
}
@ -429,9 +396,9 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages")
firstMessage.let { text ->
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
firstEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text)
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -455,9 +422,9 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages")
secondMessage.let { text ->
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
secondEventId = testHelper.sendMessageInRoom(aliceRoomPOV, text)
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -488,11 +455,11 @@ class E2eeSanityTests : InstrumentedTest {
// Now let's verify bobs session, and re-request keys
bobSessionWithBetterKey.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId)
newBobSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId)
// now let new session request
newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
@ -501,7 +468,7 @@ class E2eeSanityTests : InstrumentedTest {
// old session should have shared the key at earliest known index now
// we should be able to decrypt both
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val canDecryptFirst = try {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
true
@ -518,101 +485,79 @@ class E2eeSanityTests : InstrumentedTest {
}
}
private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
var sentEventId: String? = null
aliceRoomPOV.sendService().sendTextMessage(text)
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodically {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also { list ->
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
Log.v("#E2E TEST", "Timeline snapshot is $message")
}
.filter { it.root.sendState == SendState.SYNCED }
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
}
timeline.dispose()
return sentEventId
}
/**
* Test that if a better key is forwared (lower index, it is then used)
*/
@Test
fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
cryptoTestHelper.bootstrapSecurity(aliceSession)
// now let's create a new login from alice
val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId)
val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId)
// initiate self verification
aliceSession.cryptoService().verificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceNewSession.myUserId,
listOf(aliceNewSession.sessionParams.deviceId!!)
)
val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode)
assertEquals("Decimal code should have matched", oldCode, newCode)
// Assert that devices are verified
val newDeviceFromOldPov: CryptoDeviceInfo? =
aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
val oldDeviceFromNewPov: CryptoDeviceInfo? =
aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
// wait for secret gossiping to happen
testHelper.retryPeriodically {
aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
}
testHelper.retryPeriodically {
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
}
assertEquals(
"MSK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master
)
assertEquals(
"USK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user
)
assertEquals(
"SSK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned
)
// Let's check that we have the megolm backup key
assertEquals(
"Megolm key should be the same",
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey
)
assertEquals(
"Megolm version should be the same",
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
)
}
// @Test
// fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
//
// val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
// cryptoTestHelper.bootstrapSecurity(aliceSession)
//
// // now let's create a new login from alice
//
// val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
//
// val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId)
// val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId)
// // initiate self verification
// aliceSession.cryptoService().verificationService().requestSelfKeyVerification(
// listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
// // aliceNewSession.myUserId,
// // listOf(aliceNewSession.sessionParams.deviceId!!)
// )
//
// val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode)
//
// assertEquals("Decimal code should have matched", oldCode, newCode)
//
// // Assert that devices are verified
// val newDeviceFromOldPov: CryptoDeviceInfo? =
// aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
// val oldDeviceFromNewPov: CryptoDeviceInfo? =
// aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
//
// Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
// Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
//
// // wait for secret gossiping to happen
// testHelper.retryPeriodically {
// aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
// }
//
// testHelper.retryPeriodically {
// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
// }
//
// assertEquals(
// "MSK Private parts should be the same",
// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master,
// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master
// )
// assertEquals(
// "USK Private parts should be the same",
// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user,
// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user
// )
//
// assertEquals(
// "SSK Private parts should be the same",
// aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned,
// aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned
// )
//
// // Let's check that we have the megolm backup key
// assertEquals(
// "Megolm key should be the same",
// aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey,
// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey
// )
// assertEquals(
// "Megolm version should be the same",
// aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
// aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
// )
// }
@Test
fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
@ -625,26 +570,23 @@ class E2eeSanityTests : InstrumentedTest {
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
)
val bobAuthParams = UserPasswordAuth(
user = bobSession!!.myUserId,
password = TestConstants.PASSWORD
)
testHelper.waitForCallback {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
})
testHelper.waitForCallback {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it)
}
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
})
// add a second session for bob but not cross signed
@ -656,15 +598,15 @@ class E2eeSanityTests : InstrumentedTest {
val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!!
Timber.v("#TEST: Send a first message that should be withheld")
val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!!
val sentEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "Hello")
// wait for it to be synced back the other side
Timber.v("#TEST: Wait for message to be synced back")
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
}
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
}
@ -679,13 +621,13 @@ class E2eeSanityTests : InstrumentedTest {
Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt")
val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!!
val secondEvent = testHelper.sendMessageInRoom(roomFromAlicePOV, "World")
Timber.v("#TEST: Wait for message to be synced back")
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
}
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
}
@ -693,104 +635,94 @@ class E2eeSanityTests : InstrumentedTest {
cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId)
}
private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
return scope.async {
suspendCancellableCoroutine { continuation ->
var oldCode: String? = null
val listener = object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
val readyInfo = pr.readyInfo
if (readyInfo != null) {
beginKeyVerification(
VerificationMethod.SAS,
userId,
readyInfo.fromDevice,
readyInfo.transactionId
)
}
}
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "exitsingPov: $tx")
val sasTx = tx as OutgoingSasVerificationTransaction
when (sasTx.uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
// for the test we just accept?
oldCode = sasTx.getDecimalCodeRepresentation()
sasTx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
removeListener(this)
// we can release this latch?
continuation.resume(oldCode!!)
}
else -> Unit
}
}
}
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
}
}
}
private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
return scope.async {
suspendCancellableCoroutine { continuation ->
var newCode: String? = null
val listener = object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// let's ready
readyPendingVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
userId,
pr.transactionId!!
)
}
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "newPov: $tx")
val sasTx = tx as IncomingSasVerificationTransaction
when (sasTx.uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
// no need to accept as there was a request first it will auto accept
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
sasTx.userHasVerifiedShortCode()
newCode = sasTx.getDecimalCodeRepresentation()
matchOnce = false
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
removeListener(this)
continuation.resume(newCode!!)
}
else -> Unit
}
}
}
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
}
}
}
private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.retryPeriodically {
otherAccounts.map {
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
it == Membership.JOIN
}
}
}
// private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
// return scope.async {
// suspendCancellableCoroutine { continuation ->
// var oldCode: String? = null
// val listener = object : VerificationService.Listener {
//
// override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
// val readyInfo = pr.readyInfo
// if (readyInfo != null) {
// beginKeyVerification(
// VerificationMethod.SAS,
// userId,
// readyInfo.fromDevice,
// readyInfo.transactionId
//
// )
// }
// }
//
// override fun transactionUpdated(tx: VerificationTransaction) {
// Log.d("##TEST", "exitsingPov: $tx")
// val sasTx = tx as OutgoingSasVerificationTransaction
// when (sasTx.uxState) {
// OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
// // for the test we just accept?
// oldCode = sasTx.getDecimalCodeRepresentation()
// sasTx.userHasVerifiedShortCode()
// }
// OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
// removeListener(this)
// // we can release this latch?
// continuation.resume(oldCode!!)
// }
// else -> Unit
// }
// }
// }
// addListener(listener)
// continuation.invokeOnCancellation { removeListener(listener) }
// }
// }
// }
//
// private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
// return scope.async {
// suspendCancellableCoroutine { continuation ->
// var newCode: String? = null
//
// val listener = object : VerificationService.Listener {
//
// override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// // let's ready
// readyPendingVerification(
// listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
// userId,
// pr.transactionId!!
// )
// }
//
// var matchOnce = true
// override fun transactionUpdated(tx: VerificationTransaction) {
// Log.d("##TEST", "newPov: $tx")
//
// val sasTx = tx as IncomingSasVerificationTransaction
// when (sasTx.uxState) {
// IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
// // no need to accept as there was a request first it will auto accept
// }
// IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
// if (matchOnce) {
// sasTx.userHasVerifiedShortCode()
// newCode = sasTx.getDecimalCodeRepresentation()
// matchOnce = false
// }
// }
// IncomingSasVerificationTransaction.UxState.VERIFIED -> {
// removeListener(this)
// continuation.resume(newCode!!)
// }
// else -> Unit
// }
// }
// }
// addListener(listener)
// continuation.invokeOnCancellation { removeListener(listener) }
// }
// }
// }
private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
sentEventIds.forEach { sentEventId ->

View File

@ -18,9 +18,11 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertNotEquals
import org.junit.Assert
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -42,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.wrapWithTimeout
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -79,9 +80,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceMessageText = "Hello Bob, I am Alice!"
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
Assume.assumeTrue(cryptoTestData.firstSession.cryptoService().supportsShareKeysOnInvite())
// Alice
val aliceSession = cryptoTestData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
@ -99,19 +100,26 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper)
Assert.assertTrue("Message should be sent", aliceMessageId != null)
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
Log.v("#E2E TEST", "Alice has sent message to roomId: $e2eRoomID")
// Bob should be able to decrypt the message
testHelper.retryPeriodically {
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == true).also {
if (it) {
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
testHelper.retryWithBackoff(
onFail = {
fail("Bob should be able to decrypt $aliceMessageId")
}
) {
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)?.also {
Log.v("#E2E TEST", "Bob sees ${it.root.getClearType()}|${it.root.mxDecryptionResult?.verificationState}")
}
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
// && timelineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
).also {
if (it) {
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
}
}
// Create a new user
@ -135,23 +143,31 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
null
-> {
// Aris should be able to decrypt the message
testHelper.retryPeriodically {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == false
).also {
if (it) {
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
testHelper.retryWithBackoff(
onFail = {
fail("Aris should be able to decrypt $aliceMessageId")
}
) {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE // &&
// timelineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
).also {
if (it) {
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
}
}
}
RoomHistoryVisibility.INVITED,
RoomHistoryVisibility.JOINED -> {
// Aris should not even be able to get the message
testHelper.retryPeriodically {
testHelper.retryWithBackoff(
onFail = {
fail("Aris should not even be able to get the message")
}
) {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
@ -160,7 +176,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
testHelper.signOutAndClose(arisSession)
cryptoTestData.cleanUp(testHelper)
}
@ -237,6 +252,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
Assume.assumeTrue(cryptoTestData.firstSession.cryptoService().supportsShareKeysOnInvite())
// Alice
val aliceSession = cryptoTestData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
@ -258,11 +275,17 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
// Bob should be able to decrypt the message
var firstAliceMessageMegolmSessionId: String? = null
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
testHelper.retryPeriodically {
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)!!
testHelper.retryWithBackoff(
onFail = {
fail("Bob should be able to decrypt $aliceMessageId")
}
) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
.timelineService()
.getTimelineEvent(aliceMessageId!!)?.also {
Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}")
}
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
@ -279,11 +302,17 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
var secondAliceMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
testHelper.retryPeriodically {
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)!!.let { secondMessage ->
testHelper.retryWithBackoff(
onFail = {
fail("Bob should be able to decrypt the second message $secondMessage")
}
) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(secondMessage)
.timelineService()
.getTimelineEvent(secondMessage)?.also {
Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}")
}
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
@ -309,29 +338,44 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
).toContent()
)
Log.v("#E2E TEST ROTATION", "State update sent")
// ensure that the state did synced down
testHelper.retryPeriodically {
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
testHelper.retryWithBackoff(
onFail = {
fail("Alice state should be updated to ${nextRoomHistoryVisibility.historyVisibilityStr}")
}
) {
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
?.content
?.also {
Log.v("#E2E TEST ROTATION", "Alice sees state as $it")
}
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
testHelper.retryPeriodically {
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
.stateService()
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
?.content
?.toModel<RoomHistoryVisibilityContent>()
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
// testHelper.retryPeriodically {
// val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
// .stateService()
// .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
// ?.content
// ?.toModel<RoomHistoryVisibilityContent>()
// Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
// roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
// }
var aliceThirdMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
testHelper.retryPeriodically {
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)!!.let { thirdMessage ->
testHelper.retryWithBackoff(
onFail = {
fail("Bob should be able to decrypt $thirdMessage")
}
) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(thirdMessage)
.timelineService()
.getTimelineEvent(thirdMessage)?.also {
Log.v("#E2E TEST ROTATION", "Bob sees ${it.root.getClearType()}")
}
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
@ -341,7 +385,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
Log.v("#E2E TEST ROTATION", "second session id $secondAliceMessageSessionId")
Log.v("#E2E TEST ROTATION", "third session id $aliceThirdMessageSessionId")
when {
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
@ -352,8 +397,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Log.v("#E2E TEST ROTATION", "Rotation is needed!")
}
}
cryptoTestData.cleanUp(testHelper)
}
private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
@ -364,7 +407,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
otherAccounts.map {
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
@ -374,7 +417,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
@ -383,17 +426,15 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
wrapWithTimeout(60_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2023 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.filters.LargeTest
import junit.framework.TestCase.fail
import kotlinx.coroutines.delay
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
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
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2eeTestVerificationTestDirty : InstrumentedTest {
@Test
fun testVerificationStateRefreshedAfterKeyDownload() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val e2eRoomID = cryptoTestData.roomId
// We are going to setup a second session for bob that will send a message while alice session
// has stopped syncing.
aliceSession.syncService().stopSync()
aliceSession.syncService().stopAnyBackgroundSync()
// wait a bit for session to be really closed
delay(1_000)
Log.v("#E2E TEST", "Create a new session for Bob")
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
Log.v("#E2E TEST", "New bob session will send a message")
val eventId = testHelper.sendMessageInRoom(newBobSession.getRoom(e2eRoomID)!!, "I am unknown")
aliceSession.syncService().startSync(true)
// Check without starting a timeline so that it doesn't update itself
testHelper.retryWithBackoff(
onFail = {
fail("${aliceSession.myUserId.take(10)} should not have downloaded the device at time of decryption")
}) {
val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also {
Log.v("#E2E TEST", "Verification state is ${it?.root?.mxDecryptionResult?.verificationState}")
}
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
timeLineEvent.root.mxDecryptionResult?.verificationState == MessageVerificationState.UNKNOWN_DEVICE
}
// After key download it should be dirty (that will happen after sync completed)
testHelper.retryWithBackoff(
onFail = {
fail("${aliceSession.myUserId.take(10)} should be dirty")
}) {
val timeLineEvent = aliceSession.getRoom(e2eRoomID)?.getTimelineEvent(eventId).also {
Log.v("#E2E TEST", "Is verification state dirty ${it?.root?.verificationStateIsDirty}")
}
timeLineEvent?.root?.verificationStateIsDirty.orFalse()
}
Log.v("#E2E TEST", "Start timeline and check that verification state is updated")
// eventually should be marked as dirty then have correct state when a timeline is started
testHelper.ensureMessage(aliceSession.getRoom(e2eRoomID)!!, eventId) {
it.isEncrypted() &&
it.root.getClearType() == EventType.MESSAGE &&
it.root.mxDecryptionResult?.verificationState == MessageVerificationState.UN_SIGNED_DEVICE
}
}
}

View File

@ -20,6 +20,7 @@ import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.Test
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.api.util.fromBase64Safe
@Suppress("SpellCheckingInspection")
class ExtensionsKtTest {

View File

@ -24,6 +24,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@ -54,7 +53,6 @@ class XSigningTest : InstrumentedTest {
fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
@ -66,10 +64,10 @@ class XSigningTest : InstrumentedTest {
)
)
}
}, it)
}
})
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
val masterPubKey = myCrossSigningKeys?.masterKey()
assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey)
val selfSigningKey = myCrossSigningKeys?.selfSigningKey()
@ -79,13 +77,14 @@ class XSigningTest : InstrumentedTest {
assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true)
assertTrue("Signing Keys should be trusted", aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId).isVerified())
val userTrustResult = aliceSession.cryptoService().crossSigningService().checkUserTrust(aliceSession.myUserId)
assertTrue("Signing Keys should be trusted", userTrustResult.isVerified())
testHelper.signOutAndClose(aliceSession)
}
@Test
fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, _ ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -100,39 +99,30 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it)
}
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
})
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
})
// Check that alice can see bob keys
testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobSession.myUserId), true)
val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId)
assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey())
assertNull("Alice should not see bob User key", bobKeysFromAlicePOV.userKey())
assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV.selfSigningKey())
assertEquals(
"Bob keys from alice pov should match",
bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey,
bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey
)
assertEquals(
"Bob keys from alice pov should match",
bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey,
bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey
)
val myKeys = bobSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.masterKey()?.unpaddedBase64PublicKey, myKeys?.masterKey()?.unpaddedBase64PublicKey)
assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV.selfSigningKey()?.unpaddedBase64PublicKey, myKeys?.selfSigningKey()?.unpaddedBase64PublicKey)
assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted())
}
@ -153,40 +143,34 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it)
}
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
})
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
})
// Check that alice can see bob keys
val bobUserId = bobSession.myUserId
testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) }
aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true)
val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId)
assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false)
testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) }
aliceSession.cryptoService().crossSigningService().trustUser(bobUserId)
// Now bobs logs in on a new device and verifies it
// We will want to test that in alice POV, this new device would be trusted by cross signing
val bobSession2 = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
val bobSecondDeviceId = bobSession2.sessionParams.deviceId
// Check that bob first session sees the new login
val data = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
}
val data = bobSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true)
if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) {
fail("Bob should see the new device")
@ -196,14 +180,10 @@ class XSigningTest : InstrumentedTest {
assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
// Manually mark it as trusted from first session
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it)
}
bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId)
// Now alice should cross trust bob's second device
val data2 = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
}
val data2 = aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), true)
// check that the device is seen
if (data2.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) {
@ -216,11 +196,15 @@ class XSigningTest : InstrumentedTest {
@Test
fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
// Remove when https://github.com/matrix-org/matrix-rust-sdk/issues/1129
Assume.assumeTrue("Not yet supported by rust", aliceSession.cryptoService().name() != "rust-sdk")
val aliceAuthParams = UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
@ -230,20 +214,16 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it)
}
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
})
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
})
cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId)
@ -267,13 +247,11 @@ class XSigningTest : InstrumentedTest {
.getUserCrossSigningKeys(bobSession.myUserId)!!
.masterKey()!!.unpaddedBase64PublicKey!!
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it)
}
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
})
testHelper.retryPeriodically {
val newBobMsk = aliceSession.cryptoService().crossSigningService()

View File

@ -19,12 +19,13 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Assert
import org.junit.Assert.assertNull
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -59,6 +60,8 @@ class KeyShareTests : InstrumentedTest {
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message
@ -70,8 +73,9 @@ class KeyShareTests : InstrumentedTest {
)
val room = aliceSession.getRoom(roomId)
assertNotNull(room)
Thread.sleep(4_000)
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
commonTestHelper.retryWithBackoff {
room?.roomCryptoService()?.isEncrypted() == true
}
val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first()
val sentEventId = sentEvent.eventId
@ -100,7 +104,7 @@ class KeyShareTests : InstrumentedTest {
// Try to request
aliceSession2.cryptoService().enableKeyGossiping(true)
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
@ -163,30 +167,34 @@ class KeyShareTests : InstrumentedTest {
// Mark the device as trusted
Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}")
val aliceSecondSession = aliceSession2.cryptoService().getMyDevice()
Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyCryptoDevice().identityKey()}")
val aliceSecondSession = aliceSession2.cryptoService().getMyCryptoDevice()
Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}")
aliceSession.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession2.sessionParams.deviceId ?: ""
aliceSession2.sessionParams.deviceId
)
// We only accept forwards from trusted session, so we need to trust on other side to
aliceSession2.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession.sessionParams.deviceId ?: ""
aliceSession.sessionParams.deviceId
)
aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true
aliceSession.cryptoService().deviceWithIdentityKey(
aliceSecondSession.userId,
aliceSecondSession.identityKey()!!,
MXCRYPTO_ALGORITHM_OLM
)!!.isVerified shouldBeEqualTo true
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(aliceSession2)
// commonTestHelper.signOutAndClose(aliceSession)
// commonTestHelper.signOutAndClose(aliceSession2)
}
// See E2ESanityTest for a test regarding secret sharing
@ -203,6 +211,9 @@ class KeyShareTests : InstrumentedTest {
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
val bobSession = testData.secondSession!!
@ -235,6 +246,9 @@ class KeyShareTests : InstrumentedTest {
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
@ -257,11 +271,11 @@ class KeyShareTests : InstrumentedTest {
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
}
// commonTestHelper.signOutAndClose(aliceSession)
// commonTestHelper.signOutAndClose(aliceNewSession)
}
/**
* Tests that keys reshared with own verified session are done from the earliest known index
*/
@Test
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(
context(),
@ -270,6 +284,9 @@ class KeyShareTests : InstrumentedTest {
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
val bobSession = testData.secondSession!!
val roomFromBob = bobSession.getRoom(testData.roomId)!!
@ -331,10 +348,10 @@ class KeyShareTests : InstrumentedTest {
// Mark the new session as verified
aliceSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId)
aliceNewSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
// Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
@ -370,14 +387,11 @@ class KeyShareTests : InstrumentedTest {
result != null && result is RequestResult.Success && result.chainIndex == 3
}
commonTestHelper.signOutAndClose(aliceNewSession)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
// commonTestHelper.signOutAndClose(aliceNewSession)
// commonTestHelper.signOutAndClose(aliceSession)
// commonTestHelper.signOutAndClose(bobSession)
}
/**
* Tests that we don't cancel a request to early on first forward if the index is not good enough
*/
@Test
fun test_dontCancelToEarly() = runCryptoTest(
context(),
@ -385,6 +399,9 @@ class KeyShareTests : InstrumentedTest {
) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
val bobSession = testData.secondSession!!
val roomFromBob = bobSession.getRoom(testData.roomId)!!
@ -419,10 +436,10 @@ class KeyShareTests : InstrumentedTest {
// Mark the new session as verified
aliceSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId)
aliceNewSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
// /!\ Stop initial alice session syncing so that it can't reply
aliceSession.cryptoService().enableKeyGossiping(false)
@ -462,8 +479,8 @@ class KeyShareTests : InstrumentedTest {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state)
commonTestHelper.signOutAndClose(aliceNewSession)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
// commonTestHelper.signOutAndClose(aliceNewSession)
// commonTestHelper.signOutAndClose(aliceSession)
// commonTestHelper.signOutAndClose(bobSession)
}
}

View File

@ -20,13 +20,14 @@ import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Assert
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
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.NoOpMatrixCallback
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
@ -71,6 +72,7 @@ class WithHeldTests : InstrumentedTest {
val roomAlicePOV = aliceSession.getRoom(roomId)!!
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// =============================
// ACT
// =============================
@ -81,7 +83,7 @@ class WithHeldTests : InstrumentedTest {
val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
// await for bob unverified session to get the message
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
}
@ -106,24 +108,26 @@ class WithHeldTests : InstrumentedTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
// Let's see if the reply we got from bob first session is unverified
testHelper.retryPeriodically {
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
.firstOrNull { it.sessionId == megolmSessionId }
?.results
?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
?.result
?.let {
it as? RequestResult.Failure
}
?.code == WithHeldCode.UNVERIFIED
if (bobUnverifiedSession.cryptoService().supportsForwardedKeyWiththeld()) {
// Let's see if the reply we got from bob first session is unverified
testHelper.retryWithBackoff {
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
.firstOrNull { it.sessionId == megolmSessionId }
?.results
?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
?.result
?.let {
it as? RequestResult.Failure
}
?.code == WithHeldCode.UNVERIFIED
}
}
// enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
// wait until it's decrypted
ev?.root?.getClearType() == EventType.MESSAGE
@ -144,6 +148,7 @@ class WithHeldTests : InstrumentedTest {
}
@Test
@Ignore("ignore NoOlm for now, implementation not correct")
fun test_WithHeldNoOlm() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
@ -151,27 +156,26 @@ class WithHeldTests : InstrumentedTest {
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported", aliceSession.cryptoService().supportKeyRequestInspection())
val bobSession = testData.secondSession!!
val aliceInterceptor = testHelper.getTestInterceptor(aliceSession)
// Simulate no OTK
aliceInterceptor!!.addRule(
MockOkHttpInterceptor.SimpleRule(
"/keys/claim",
200,
"""
aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule(
"/keys/claim",
200,
"""
{ "one_time_keys" : {} }
"""
)
)
))
Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}")
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
val eventId = testHelper.sendMessageInRoom(roomAlicePov, "first message")
// await for bob session to get the message
testHelper.retryPeriodically {
testHelper.retryWithBackoff {
bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
}
@ -191,10 +195,7 @@ class WithHeldTests : InstrumentedTest {
// Ensure that alice has marked the session to be shared with bob
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(
bobSession.myUserId,
bobSession.sessionParams.credentials.deviceId
)
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId)
Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex)
// Add a new device for bob
@ -210,10 +211,7 @@ class WithHeldTests : InstrumentedTest {
bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
}
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(
bobSecondSession.myUserId,
bobSecondSession.sessionParams.credentials.deviceId
)
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId)
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
@ -221,6 +219,7 @@ class WithHeldTests : InstrumentedTest {
}
@Test
@Ignore("Outdated test, we don't request to others")
fun test_WithHeldKeyRequest() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
@ -228,6 +227,7 @@ class WithHeldTests : InstrumentedTest {
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
Assume.assumeTrue("Not supported by rust sdk", aliceSession.cryptoService().supportsForwardedKeyWiththeld())
val bobSession = testData.secondSession!!
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
@ -243,8 +243,8 @@ class WithHeldTests : InstrumentedTest {
cryptoTestHelper.initializeCrossSigning(bobSecondSession)
// Trust bob second device from Alice POV
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback())
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback())
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId)
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId)
var sessionId: String? = null
// Check that the
@ -265,5 +265,10 @@ class WithHeldTests : InstrumentedTest {
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
wc?.code == WithHeldCode.UNAUTHORISED
}
// // Check that bob second session requested the key
// testHelper.retryPeriodically {
// val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
// wc?.code == WithHeldCode.UNAUTHORISED
// }
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 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.keysbackup
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
internal class BackupStateHelper(
private val keysBackup: KeysBackupService) : KeysBackupStateListener {
init {
keysBackup.addListener(this)
}
val hasBackedUpOnce = CompletableDeferred<Unit>()
var backingUpOnce = false
override fun onStateChange(newState: KeysBackupState) {
Log.d("#E2E", "Keybackup onStateChange $newState")
if (newState == KeysBackupState.BackingUp) {
backingUpOnce = true
}
if (newState == KeysBackupState.ReadyToBackUp || newState == KeysBackupState.WillBackUp) {
if (backingUpOnce) {
hasBackedUpOnce.complete(Unit)
}
}
}
}

View File

@ -19,14 +19,13 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestData
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
/**
* Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
*/
internal data class KeysBackupScenarioData(
val cryptoTestData: CryptoTestData,
val aliceKeys: List<MXInboundMegolmSessionWrapper>,
val aliceKeysCount: Int,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session
) {

View File

@ -16,42 +16,35 @@
package org.matrix.android.sdk.internal.crypto.keysbackup
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.internal.assertFails
import org.amshove.kluent.internal.assertFailsWith
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.listeners.StepProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.waitFor
import java.security.InvalidParameterException
import java.util.Collections
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@ -59,7 +52,7 @@ import kotlin.coroutines.resume
@LargeTest
class KeysBackupTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
// @get:Rule val rule = RetryTestRule(3)
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
@ -67,39 +60,40 @@ class KeysBackupTest : InstrumentedTest {
* - Reset keys backup markers
*/
@Test
fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
@Ignore("Uses internal APIs")
fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { _, _ ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
//
// // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
// val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
// val sessions = cryptoStore.inboundGroupSessionsToBackup(100)
// val sessionsCount = sessions.size
//
// assertFalse(sessions.isEmpty())
// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
// assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
//
// // - Check backup keys after having marked one as backed up
// val session = sessions[0]
//
// cryptoStore.markBackupDoneForInboundGroupSessions(listOf(session))
//
// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
// assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
//
// val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100)
// assertEquals(sessionsCount - 1, sessions2.size)
//
// // - Reset keys backup markers
// cryptoStore.resetBackupMarkers()
//
// val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100)
// assertEquals(sessionsCount, sessions3.size)
// assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
// assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
// From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
val sessions = cryptoStore.inboundGroupSessionsToBackup(100)
val sessionsCount = sessions.size
assertFalse(sessions.isEmpty())
assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
// - Check backup keys after having marked one as backed up
val session = sessions[0]
cryptoStore.markBackupDoneForInboundGroupSessions(Collections.singletonList(session))
assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
assertEquals(1, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
val sessions2 = cryptoStore.inboundGroupSessionsToBackup(100)
assertEquals(sessionsCount - 1, sessions2.size)
// - Reset keys backup markers
cryptoStore.resetBackupMarkers()
val sessions3 = cryptoStore.inboundGroupSessionsToBackup(100)
assertEquals(sessionsCount, sessions3.size)
assertEquals(sessionsCount, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
cryptoTestData.cleanUp(testHelper)
// cryptoTestData.cleanUp(testHelper)
}
/**
@ -118,9 +112,7 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(null, null, it)
}
val megolmBackupCreationInfo = keysBackup.prepareKeysBackupVersion(null, null)
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm)
assertNotNull(megolmBackupCreationInfo.authData.publicKey)
@ -144,27 +136,20 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(null, null, it)
}
val megolmBackupCreationInfo =
keysBackup.prepareKeysBackupVersion(null, null)
assertFalse(keysBackup.isEnabled())
// Create the version
val version = testHelper.waitForCallback<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
val version = keysBackup.createKeysBackupVersion(megolmBackupCreationInfo)
// Backup must be enable now
assertTrue(keysBackup.isEnabled())
// Check that it's signed with MSK
val versionResult = testHelper.waitForCallback<KeysVersionResult?> {
keysBackup.getVersion(version.version, it)
}
val trust = testHelper.waitForCallback<KeysBackupVersionTrust> {
keysBackup.getKeysBackupTrust(versionResult!!, it)
}
val versionResult = keysBackup.getVersion(version.version)
val trust = keysBackup.getKeysBackupTrust(versionResult!!)
assertEquals("Should have 2 signatures", 2, trust.signatures.size)
@ -204,19 +189,17 @@ class KeysBackupTest : InstrumentedTest {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
keysBackupTestHelper.waitForKeybackUpBatching()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val latch = CountDownLatch(1)
assertEquals(2, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false))
assertEquals(0, cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true))
val stateObserver = StateObserver(keysBackup, latch, 5)
val stateObserver = BackupStateHelper(keysBackup).hasBackedUpOnce
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
testHelper.await(latch)
Log.d("#E2E", "Wait for a backup cycle")
stateObserver.await()
Log.d("#E2E", ".. Ok")
val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)
val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)
@ -225,15 +208,15 @@ class KeysBackupTest : InstrumentedTest {
assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
// Check the several backup state changes
stateObserver.stopAndCheckStates(
listOf(
KeysBackupState.Enabling,
KeysBackupState.ReadyToBackUp,
KeysBackupState.WillBackUp,
KeysBackupState.BackingUp,
KeysBackupState.ReadyToBackUp
)
)
// stateObserver.stopAndCheckStates(
// listOf(
// KeysBackupState.Enabling,
// KeysBackupState.ReadyToBackUp,
// KeysBackupState.WillBackUp,
// KeysBackupState.BackingUp,
// KeysBackupState.ReadyToBackUp
// )
// )
}
/**
@ -242,33 +225,27 @@ class KeysBackupTest : InstrumentedTest {
@Test
fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
Log.d("#E2E", "Setting up Alice Bob with messages")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
Log.d("#E2E", "Creating key backup...")
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
Log.d("#E2E", "... created")
// Check that backupAllGroupSessions returns valid data
val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)
assertEquals(2, nbOfKeys)
var lastBackedUpKeysProgress = 0
testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
assertEquals(nbOfKeys, total)
lastBackedUpKeysProgress = progress
}
}, it)
testHelper.retryWithBackoff {
Log.d("#E2E", "Backup ${keysBackup.getTotalNumbersOfBackedUpKeys()}/${keysBackup.getTotalNumbersOfBackedUpKeys()}")
keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys()
}
assertEquals(nbOfKeys, lastBackedUpKeysProgress)
val backedUpKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(true)
assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
@ -285,41 +262,42 @@ class KeysBackupTest : InstrumentedTest {
* - Compare the decrypted megolm key with the original one
*/
@Test
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
val stateObserver = StateObserver(keysBackup)
// - Pick a megolm key
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session)
assertNotNull(keyBackupData)
assertNotNull(keyBackupData!!.sessionData)
// - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
assertNotNull(decryption)
// - Check decryptKeyBackupData() returns stg
val sessionData = keysBackup
.decryptKeyBackupData(
keyBackupData,
session.safeSessionId!!,
cryptoTestData.roomId,
decryption!!
)
assertNotNull(sessionData)
// - Compare the decrypted megolm key with the original one
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
stateObserver.stopAndCheckStates(null)
@Ignore("Uses internal API")
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { _, _ ->
// val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
//
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
//
// val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
//
// val stateObserver = StateObserver(keysBackup)
//
// // - Pick a megolm key
// val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
//
// val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
//
// // - Check encryptGroupSession() returns stg
// val keyBackupData = keysBackup.encryptGroupSession(session)
// assertNotNull(keyBackupData)
// assertNotNull(keyBackupData!!.sessionData)
//
// // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
// val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey.toBase58())
// assertNotNull(decryption)
// // - Check decryptKeyBackupData() returns stg
// val sessionData = keysBackup
// .decryptKeyBackupData(
// keyBackupData,
// session.safeSessionId!!,
// cryptoTestData.roomId,
// keyBackupCreationInfo.recoveryKey
// )
// assertNotNull(sessionData)
// // - Compare the decrypted megolm key with the original one
// keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
//
// stateObserver.stopAndCheckStates(null)
}
/**
@ -335,16 +313,15 @@ class KeysBackupTest : InstrumentedTest {
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver
val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
null,
null,
null,
it
)
}
val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
null,
null,
null
)
Log.d("#E2E", "importRoomKeysResult is $importRoomKeysResult")
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
@ -401,7 +378,7 @@ class KeysBackupTest : InstrumentedTest {
// // Request is either sent or unsent
// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
//
// testData.cleanUp(mTestHelper)
// testData.cleanUp(testHelper)
// }
/**
@ -430,13 +407,10 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device
testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
true,
it
)
}
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
true
)
// Wait for backup state to be ReadyToBackUp
keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
@ -446,16 +420,17 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult()
val keysVersionResult = testData.aliceSession2.cryptoService()
.keysBackupService()
.getCurrentVersion()!!
.toKeysVersionResult()
// - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
}
val keysBackupVersionTrust = testData.aliceSession2.cryptoService()
.keysBackupService()
.getKeysBackupTrust(keysVersionResult)
// - It must be trusted and must have 2 signatures now
assertTrue(keysBackupVersionTrust.usable)
@ -490,32 +465,32 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the recovery key
testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
it
)
}
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey
)
// Wait for backup state to be ReadyToBackUp
keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
assertEquals(
testData.prepareKeysBackupDataResult.version,
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version
)
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult()
val keysVersionResult = testData.aliceSession2.cryptoService().keysBackupService()
.getCurrentVersion()!!
.toKeysVersionResult()
// - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
}
val keysBackupVersionTrust = testData.aliceSession2.cryptoService()
.keysBackupService()
.getKeysBackupTrust(keysVersionResult)
// - It must be trusted and must have 2 signatures now
assertTrue(keysBackupVersionTrust.usable)
@ -548,11 +523,10 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong recovery key
testHelper.waitForCallbackError<Unit> {
assertFails {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
"Bad recovery key",
it
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key")!!,
)
}
@ -592,13 +566,10 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the password
testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password,
it
)
}
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password
)
// Wait for backup state to be ReadyToBackUp
keysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
@ -608,16 +579,16 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server
val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult()
val keysVersionResult = testData.aliceSession2.cryptoService().keysBackupService()
.getCurrentVersion()!!
.toKeysVersionResult()
// - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
}
val keysBackupVersionTrust = testData.aliceSession2.cryptoService()
.keysBackupService()
.getKeysBackupTrust(keysVersionResult)
// - It must be trusted and must have 2 signatures now
assertTrue(keysBackupVersionTrust.usable)
@ -653,11 +624,10 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong password
testHelper.waitForCallbackError<Unit> {
assertFails {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
badPassword,
it
)
}
@ -683,18 +653,15 @@ class KeysBackupTest : InstrumentedTest {
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a wrong recovery key
val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
assertFailsWith<InvalidParameterException> {
keysBackupService.restoreKeysWithRecoveryKey(
keysBackupService.keysBackupVersion!!,
"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,
it
)
}
assertTrue(importRoomKeysResult is InvalidParameterException)
}
/**
@ -714,20 +681,17 @@ class KeysBackupTest : InstrumentedTest {
// - Restore the e2e backup with the password
val steps = ArrayList<StepProgressListener.Step>()
val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password,
null,
null,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
steps.add(step)
}
},
it
)
}
val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password,
null,
null,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
steps.add(step)
}
}
)
// Check steps
assertEquals(105, steps.size)
@ -770,18 +734,15 @@ class KeysBackupTest : InstrumentedTest {
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a wrong password
val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
assertFailsWith<InvalidParameterException> {
keysBackupService.restoreKeyBackupWithPassword(
keysBackupService.keysBackupVersion!!,
wrongPassword,
null,
null,
null,
it
)
}
assertTrue(importRoomKeysResult is InvalidParameterException)
}
/**
@ -799,16 +760,13 @@ class KeysBackupTest : InstrumentedTest {
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the recovery key.
val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
null,
null,
null,
it
)
}
val importRoomKeysResult = testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
null,
null,
null
)
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
}
@ -823,22 +781,19 @@ class KeysBackupTest : InstrumentedTest {
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword("password")
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a password
val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
keysBackupService.restoreKeyBackupWithPassword(
keysBackupService.keysBackupVersion!!,
"password",
null,
null,
null,
it
)
}
val importRoomKeysResult = keysBackupService.restoreKeyBackupWithPassword(
keysBackupService.keysBackupVersion!!,
"password",
null,
null,
null,
)
assertTrue(importRoomKeysResult is IllegalStateException)
assertTrue(importRoomKeysResult.importedSessionInfo.isNotEmpty())
}
/**
@ -860,14 +815,10 @@ class KeysBackupTest : InstrumentedTest {
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Get key backup version from the homeserver
val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
keysBackup.getCurrentVersion(it)
}.toKeysVersionResult()
val keysVersionResult = keysBackup.getCurrentVersion()!!.toKeysVersionResult()
// - Check the returned KeyBackupVersion is trusted
val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
keysBackup.getKeysBackupTrust(keysVersionResult!!, it)
}
val keysBackupVersionTrust = keysBackup.getKeysBackupTrust(keysVersionResult!!)
assertNotNull(keysBackupVersionTrust)
assertTrue(keysBackupVersionTrust.usable)
@ -876,7 +827,7 @@ class KeysBackupTest : InstrumentedTest {
val signature = keysBackupVersionTrust.signatures[0] as KeysBackupVersionTrustSignature.DeviceSignature
assertTrue(signature.valid)
assertNotNull(signature.device)
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
assertEquals(cryptoTestData.firstSession.cryptoService().getMyCryptoDevice().deviceId, signature.deviceId)
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
stateObserver.stopAndCheckStates(null)
@ -888,7 +839,7 @@ class KeysBackupTest : InstrumentedTest {
* - Make alice back up her keys to her homeserver
* - Create a new backup with fake data on the homeserver
* - Make alice back up all her keys again
* -> That must fail and her backup state must be WrongBackUpVersion
* -> That must fail and her backup state must be WrongBackUpVersion or Not trusted?
*/
@Test
fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
@ -899,58 +850,28 @@ class KeysBackupTest : InstrumentedTest {
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
assertFalse(keysBackup.isEnabled())
// Wait for keys backup to be finished
var count = 0
waitFor(
continueWhen = {
suspendCancellableCoroutine<Unit> { continuation ->
val listener = object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
// Check the backup completes
if (newState == KeysBackupState.ReadyToBackUp) {
count++
if (count == 2) {
// Remove itself from the list of listeners
keysBackup.removeListener(this)
continuation.resume(Unit)
}
}
}
}
keysBackup.addListener(listener)
continuation.invokeOnCancellation { keysBackup.removeListener(listener) }
}
},
action = {
// - Make alice back up her keys to her homeserver
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
},
)
val backupWaitHelper = BackupStateHelper(keysBackup)
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled())
// - Create a new backup with fake data on the homeserver, directly using the rest client
val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo()
testHelper.waitForCallback<KeysVersion> {
(keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it)
backupWaitHelper.hasBackedUpOnce.await()
val newSession = testHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, SessionTestParams(true))
keysBackupTestHelper.prepareAndCreateKeysBackupData(newSession.cryptoService().keysBackupService())
// Make a new key for alice to backup
cryptoTestData.firstSession.cryptoService().discardOutboundSession(cryptoTestData.roomId)
testHelper.sendMessageInRoom(cryptoTestData.firstSession.getRoom(cryptoTestData.roomId)!!, "new")
// - Alice first session should not be able to backup
testHelper.retryPeriodically {
Log.d("#E2E", "backup state is ${keysBackup.getState()}")
KeysBackupState.NotTrusted == keysBackup.getState()
}
// Reset the store backup status for keys
(cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers()
// - Make alice back up all her keys again
testHelper.waitForCallbackError<Unit> { keysBackup.backupAllGroupSessions(null, it) }
// -> That must fail and her backup state must be WrongBackUpVersion
assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState())
assertFalse(keysBackup.isEnabled())
stateObserver.stopAndCheckStates(null)
}
/**
@ -971,57 +892,52 @@ class KeysBackupTest : InstrumentedTest {
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession)
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
// - Make alice back up her keys to her homeserver
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Wait for keys backup to finish by asking again to backup keys.
testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(null, it)
testHelper.retryWithBackoff {
keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys()
}
testHelper.retryWithBackoff {
keysBackup.getState() == KeysBackupState.ReadyToBackUp
}
val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!!
val oldKeyBackupVersion = keysBackup.currentBackupVersion
val aliceUserId = cryptoTestData.firstSession.myUserId
// - Log Alice on a new device
Log.d("#E2E", "Log Alice on a new device")
val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
// - Post a message to have a new megolm session
Log.d("#E2E", "Post a message to have a new megolm session")
aliceSession2.cryptoService().setWarnOnUnknownDevices(false)
val room2 = aliceSession2.getRoom(cryptoTestData.roomId)!!
testHelper.sendTextMessage(room2, "New key", 1)
testHelper.sendMessageInRoom(room2, "New key")
// - Try to backup all in aliceSession2, it must fail
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
assertFalse("Backup should not be enabled", keysBackup2.isEnabled())
val stateObserver2 = StateObserver(keysBackup2)
testHelper.waitForCallbackError<Unit> { keysBackup2.backupAllGroupSessions(null, it) }
// Backup state must be NotTrusted
assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState())
assertFalse("Backup should not be enabled", keysBackup2.isEnabled())
// - Validate the old device from the new one
aliceSession2.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
aliceSession2.myUserId,
oldDeviceId
)
cryptoTestHelper.verifyNewSession(cryptoTestData.firstSession, aliceSession2)
// -> Backup should automatically enable on the new device
suspendCancellableCoroutine<Unit> { continuation ->
val listener = object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
Log.d("#E2E", "keysBackup2 onStateChange: $newState")
// Check the backup completes
if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) {
// Remove itself from the list of listeners
@ -1037,15 +953,17 @@ class KeysBackupTest : InstrumentedTest {
// -> It must use the same backup version
assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion)
testHelper.waitForCallback<Unit> {
aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it)
// aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it)
testHelper.retryPeriodically {
keysBackup2.getTotalNumbersOfKeys() == keysBackup2.getTotalNumbersOfBackedUpKeys()
}
testHelper.retryPeriodically {
aliceSession2.cryptoService().keysBackupService().getState() == KeysBackupState.ReadyToBackUp
}
// -> It must success
assertTrue(aliceSession2.cryptoService().keysBackupService().isEnabled())
stateObserver.stopAndCheckStates(null)
stateObserver2.stopAndCheckStates(null)
}
/**
@ -1070,7 +988,7 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(keysBackup.isEnabled())
// Delete the backup
testHelper.waitForCallback<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) }
keysBackup.deleteBackup(keyBackupCreationInfo.version)
// Backup is now disabled
assertFalse(keysBackup.isEnabled())

View File

@ -18,13 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.Assert
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.assertDictEquals
@ -53,29 +50,22 @@ internal class KeysBackupTestHelper(
waitForKeybackUpBatching()
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
// val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
// val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
// - Do an e2e backup to the homeserver
val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
var lastProgress = 0
var lastTotal = 0
testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
lastProgress = progress
lastTotal = total
}
}, it)
testHelper.retryPeriodically {
keysBackup.getTotalNumbersOfKeys() == keysBackup.getTotalNumbersOfBackedUpKeys()
}
val totalNumbersOfBackedUpKeys = cryptoTestData.firstSession.cryptoService().keysBackupService().getTotalNumbersOfBackedUpKeys()
Assert.assertEquals(2, lastProgress)
Assert.assertEquals(2, lastTotal)
Assert.assertEquals(2, totalNumbersOfBackedUpKeys)
val aliceUserId = cryptoTestData.firstSession.myUserId
@ -83,19 +73,18 @@ internal class KeysBackupTestHelper(
val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
// Test check: aliceSession2 has no keys at login
Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
val inboundGroupSessionCount = aliceSession2.cryptoService().inboundGroupSessionsCount(false)
Assert.assertEquals(0, inboundGroupSessionCount)
// Wait for backup state to be NotTrusted
waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
stateObserver.stopAndCheckStates(null)
return KeysBackupScenarioData(
cryptoTestData,
aliceKeys,
return KeysBackupScenarioData(cryptoTestData,
totalNumbersOfBackedUpKeys,
prepareKeysBackupDataResult,
aliceSession2
)
aliceSession2)
}
suspend fun prepareAndCreateKeysBackupData(
@ -104,18 +93,15 @@ internal class KeysBackupTestHelper(
): PrepareKeysBackupDataResult {
val stateObserver = StateObserver(keysBackup)
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(password, null, it)
}
val megolmBackupCreationInfo = keysBackup.prepareKeysBackupVersion(password, null)
Assert.assertNotNull(megolmBackupCreationInfo)
Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled())
// Create the version
val keysVersion = testHelper.waitForCallback<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
val keysVersion =
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo)
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
@ -152,7 +138,7 @@ internal class KeysBackupTestHelper(
}
}
fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
internal fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
Assert.assertNotNull(keys1)
Assert.assertNotNull(keys2)
@ -174,24 +160,27 @@ internal class KeysBackupTestHelper(
* - The new device must have the same count of megolm keys
* - Alice must have the same keys on both devices
*/
fun checkRestoreSuccess(
suspend fun checkRestoreSuccess(
testData: KeysBackupScenarioData,
total: Int,
imported: Int
) {
// - Imported keys number must be correct
Assert.assertEquals(testData.aliceKeys.size, total)
Assert.assertEquals(testData.aliceKeysCount, total)
Assert.assertEquals(total, imported)
// - The new device must have the same count of megolm keys
Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false))
val inboundGroupSessionCount = testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false)
Assert.assertEquals(testData.aliceKeysCount, inboundGroupSessionCount)
// - Alice must have the same keys on both devices
for (aliceKey1 in testData.aliceKeys) {
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
.getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
Assert.assertNotNull(aliceKey2)
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
}
// TODO can't access internals as we can switch from rust/kotlin
// for (aliceKey1 in testData.aliceKeys) {
// val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
// .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
// Assert.assertNotNull(aliceKey2)
// assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
// }
}
}

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.keysbackup
import android.util.Log
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
@ -51,10 +52,13 @@ internal class StateObserver(
KeysBackupState.NotTrusted to KeysBackupState.CheckingBackUpOnHomeserver,
// This transition happens when we trust the device
KeysBackupState.NotTrusted to KeysBackupState.ReadyToBackUp,
// This transition happens when we create a new backup from an untrusted one
KeysBackupState.NotTrusted to KeysBackupState.Enabling,
KeysBackupState.ReadyToBackUp to KeysBackupState.WillBackUp,
KeysBackupState.Unknown to KeysBackupState.CheckingBackUpOnHomeserver,
KeysBackupState.Unknown to KeysBackupState.Enabling,
KeysBackupState.WillBackUp to KeysBackupState.BackingUp,
@ -90,6 +94,7 @@ internal class StateObserver(
}
override fun onStateChange(newState: KeysBackupState) {
Log.d("#E2E", "Keybackup onStateChange $newState")
stateList.add(newState)
// Check that state transition is valid

View File

@ -21,6 +21,7 @@ import org.amshove.kluent.internal.assertFailsWith
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@ -43,6 +44,9 @@ class ReplayAttackTest : InstrumentedTest {
// Alice
val aliceSession = cryptoTestData.firstSession
// Until https://github.com/matrix-org/matrix-rust-sdk/issues/397
Assume.assumeTrue("Not yet supported by rust", cryptoTestData.firstSession.cryptoService().name() != "rust-sdk")
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob

View File

@ -88,7 +88,7 @@ class QuadSTests : InstrumentedTest {
assertNotNull(defaultKeyAccountData?.content)
assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key"))
testHelper.signOutAndClose(aliceSession)
// testHelper.signOutAndClose(aliceSession)
}
@Test

View File

@ -1,611 +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 org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
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.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
import org.matrix.android.sdk.internal.crypto.model.rest.toValue
import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Ignore
class SASTest : InstrumentedTest {
@Test
fun test_aliceStartThenAliceCancel() = 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 bobTxCreatedLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
bobTxCreatedLatch.countDown()
}
}
bobVerificationService.addListener(bobListener)
val txID = aliceVerificationService.beginKeyVerification(
VerificationMethod.SAS,
bobSession.myUserId,
bobSession.cryptoService().getMyDevice().deviceId,
null
)
assertNotNull("Alice should have a started transaction", txID)
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!)
assertNotNull("Alice should have a started transaction", aliceKeyTx)
testHelper.await(bobTxCreatedLatch)
bobVerificationService.removeListener(bobListener)
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)
assertNotNull("Bob should have started verif transaction", bobKeyTx)
assertTrue(bobKeyTx is SASDefaultVerificationTransaction)
assertNotNull("Bob should have starting a SAS transaction", bobKeyTx)
assertTrue(aliceKeyTx is SASDefaultVerificationTransaction)
assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction?
val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction?
assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state)
assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state)
// Let's cancel from alice side
val cancelLatch = CountDownLatch(1)
val bobListener2 = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.transactionId == txID) {
val immutableState = (tx as SASDefaultVerificationTransaction).state
if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) {
cancelLatch.countDown()
}
}
}
}
bobVerificationService.addListener(bobListener2)
aliceSasTx.cancel(CancelCode.User)
testHelper.await(cancelLatch)
assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled)
assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled)
val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled
val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled
assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe)
assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe)
assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode)
assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.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) {
if (tx.transactionId == tid && tx.state is VerificationTxState.Cancelled) {
cancelReason = (tx.state as VerificationTxState.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().getMyDevice().deviceId
val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
(tx as IncomingSasVerificationTransaction).performAccept()
}
}
}
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
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().getMyDevice().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().getMyDevice().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 fun fakeBobStart(
bobSession: Session,
aliceUserID: String?,
aliceDevice: String?,
tid: String,
protocols: List<String> = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
hashes: List<String> = SASDefaultVerificationTransaction.KNOWN_HASHES,
mac: List<String> = SASDefaultVerificationTransaction.KNOWN_MACS,
codes: List<String> = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES
) {
val startMessage = KeyVerificationStart(
fromDevice = bobSession.cryptoService().getMyDevice().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(2)
val createdTx = mutableListOf<SASDefaultVerificationTransaction>()
val aliceListener = object : VerificationService.Listener {
override fun transactionCreated(tx: VerificationTransaction) {
createdTx.add(tx as SASDefaultVerificationTransaction)
aliceCreatedLatch.countDown()
}
override fun transactionUpdated(tx: VerificationTransaction) {
if ((tx as SASDefaultVerificationTransaction).state is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) {
aliceCancelledLatch.countDown()
}
}
}
aliceVerificationService.addListener(aliceListener)
val bobUserId = bobSession!!.myUserId
val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
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()
var accepted: ValidVerificationInfoAccept? = null
var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null
val aliceAcceptedLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}")
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
val at = tx as SASDefaultVerificationTransaction
accepted = at.accepted
startReq = at.startReq
aliceAcceptedLatch.countDown()
}
}
}
aliceVerificationService.addListener(aliceListener)
val bobListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}")
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
bobVerificationService.removeListener(this)
val at = tx as IncomingSasVerificationTransaction
at.performAccept()
}
}
}
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
testHelper.await(aliceAcceptedLatch)
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()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService()
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
when (uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
aliceSASLatch.countDown()
}
else -> Unit
}
}
}
aliceVerificationService.addListener(aliceListener)
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as IncomingSasVerificationTransaction).uxState
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
tx.performAccept()
}
else -> Unit
}
if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) {
bobSASLatch.countDown()
}
}
}
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId
val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
testHelper.await(aliceSASLatch)
testHelper.await(bobSASLatch)
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()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService()
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
Log.v("TEST", "== aliceState ${uxState.name}")
when (uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
if (matchOnce) {
matchOnce = false
aliceSASLatch.countDown()
}
}
else -> Unit
}
}
}
aliceVerificationService.addListener(aliceListener)
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
var acceptOnce = true
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as IncomingSasVerificationTransaction).uxState
Log.v("TEST", "== bobState ${uxState.name}")
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
if (acceptOnce) {
acceptOnce = false
tx.performAccept()
}
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
matchOnce = false
tx.userHasVerifiedShortCode()
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
bobSASLatch.countDown()
}
else -> Unit
}
}
}
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.cryptoService().getMyDevice().deviceId
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
testHelper.await(aliceSASLatch)
testHelper.await(bobSASLatch)
// Assert that devices are verified
val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().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()
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
)
var requestID: String? = null
testHelper.retryPeriodically {
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
requestID = prAlicePOV?.transactionId
Log.v("TEST", "== alicePOV is $prAlicePOV")
prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
}
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?.isReady != null
}
// Start concurrent!
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID!!,
cryptoTestData.roomId,
bobSession.myUserId,
bobSession.sessionParams.deviceId!!
)
bobVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID!!,
cryptoTestData.roomId,
aliceSession.myUserId,
aliceSession.sessionParams.deviceId!!
)
// 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 == VerificationTxState.ShortCodeReady
}
// 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 == VerificationTxState.ShortCodeReady
}
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
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.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestData
class SasVerificationTestHelper(private val testHelper: CommonTestHelper) {
suspend fun requestVerificationAndWaitForReadyState(
scope: CoroutineScope,
cryptoTestData: CryptoTestData, supportedMethods: List<VerificationMethod>
): String {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession.cryptoService().verificationService()
val bobSeesVerification = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request != null) {
bobSeesVerification.complete(request)
return@collect cancel()
}
}
}
val bobUserId = bobSession.myUserId
// Step 1: Alice starts a verification request
val transactionId = aliceVerificationService.requestKeyVerificationInDMs(
supportedMethods, bobUserId, cryptoTestData.roomId
).transactionId
val aliceReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
aliceReady.complete(request)
return@collect cancel()
}
}
}
bobSeesVerification.await()
bobVerificationService.readyPendingVerification(
supportedMethods,
aliceSession.myUserId,
transactionId
)
aliceReady.await()
return transactionId
}
suspend fun requestSelfKeyAndWaitForReadyState(session1: Session, session2: Session, supportedMethods: List<VerificationMethod>): String {
val session1VerificationService = session1.cryptoService().verificationService()
val session2VerificationService = session2.cryptoService().verificationService()
val requestID = session1VerificationService.requestSelfKeyVerification(supportedMethods).transactionId
val myUserId = session1.myUserId
testHelper.retryWithBackoff {
val incomingRequest = session2VerificationService.getExistingVerificationRequest(myUserId, requestID)
if (incomingRequest != null) {
session2VerificationService.readyPendingVerification(
supportedMethods,
myUserId,
incomingRequest.transactionId
)
true
} else {
false
}
}
// wait for alice to see the ready
testHelper.retryPeriodically {
val pendingRequest = session1VerificationService.getExistingVerificationRequest(myUserId, requestID)
pendingRequest?.state == EVerificationState.Ready
}
return requestID
}
}

View File

@ -0,0 +1,235 @@
/*
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.launch
import org.amshove.kluent.shouldBe
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.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class VerificationTest : InstrumentedTest {
data class ExpectedResult(
val sasIsSupported: Boolean = false,
val otherCanScanQrCode: Boolean = false,
val otherCanShowQrCode: Boolean = false
)
private val sas = listOf(
VerificationMethod.SAS
)
private val sasShow = listOf(
VerificationMethod.SAS,
VerificationMethod.QR_CODE_SHOW
)
private val sasScan = listOf(
VerificationMethod.SAS,
VerificationMethod.QR_CODE_SCAN
)
private val sasShowScan = listOf(
VerificationMethod.SAS,
VerificationMethod.QR_CODE_SHOW,
VerificationMethod.QR_CODE_SCAN
)
@Test
fun test_aliceAndBob_sas_sas() = doTest(
sas,
sas,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_sas_show() = doTest(
sas,
sasShow,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_show_sas() = doTest(
sasShow,
sas,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_sas_scan() = doTest(
sas,
sasScan,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_scan_sas() = doTest(
sasScan,
sas,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_scan_scan() = doTest(
sasScan,
sasScan,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_show_show() = doTest(
sasShow,
sasShow,
ExpectedResult(sasIsSupported = true),
ExpectedResult(sasIsSupported = true)
)
@Test
fun test_aliceAndBob_show_scan() = doTest(
sasShow,
sasScan,
ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true),
ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true)
)
@Test
fun test_aliceAndBob_scan_show() = doTest(
sasScan,
sasShow,
ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true),
ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true)
)
@Test
fun test_aliceAndBob_all_all() = doTest(
sasShowScan,
sasShowScan,
ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true),
ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true)
)
private fun doTest(
aliceSupportedMethods: List<VerificationMethod>,
bobSupportedMethods: List<VerificationMethod>,
expectedResultForAlice: ExpectedResult,
expectedResultForBob: ExpectedResult
) = runCryptoTest(context()) { cryptoTestHelper, _ ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
cryptoTestHelper.initializeCrossSigning(aliceSession)
cryptoTestHelper.initializeCrossSigning(bobSession)
val scope = CoroutineScope(SupervisorJob())
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession.cryptoService().verificationService()
val bobSeesVerification = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request != null) {
bobSeesVerification.complete(request)
return@collect cancel()
}
}
}
val aliceReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
aliceVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
aliceReady.complete(request)
return@collect cancel()
}
}
}
val bobReady = CompletableDeferred<PendingVerificationRequest>()
scope.launch(Dispatchers.IO) {
bobVerificationService.requestEventFlow()
.cancellable()
.collect {
val request = it.getRequest()
if (request?.state == EVerificationState.Ready) {
bobReady.complete(request)
return@collect cancel()
}
}
}
val requestID = aliceVerificationService.requestKeyVerificationInDMs(
methods = aliceSupportedMethods,
otherUserId = bobSession.myUserId,
roomId = cryptoTestData.roomId
).transactionId
bobSeesVerification.await()
bobVerificationService.readyPendingVerification(
bobSupportedMethods,
aliceSession.myUserId,
requestID
)
val aliceRequest = aliceReady.await()
val bobRequest = bobReady.await()
aliceRequest.let { pr ->
pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported
pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode
pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode
}
bobRequest.let { pr ->
pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported
pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode
pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode
}
scope.cancel()
}
}

View File

@ -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.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldNotBeEqualTo
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 SharedSecretTest : InstrumentedTest {
@Test
fun testSharedSecretLengthCase() {
repeat(100) {
generateSharedSecretV2().length shouldBe 11
}
}
@Test
fun testSharedDiffCase() {
val sharedSecret1 = generateSharedSecretV2()
val sharedSecret2 = generateSharedSecretV2()
sharedSecret1 shouldNotBeEqualTo sharedSecret2
}
}

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
import org.junit.Ignore
@ -29,14 +31,13 @@ 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.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.VerificationEvent
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@ -164,7 +165,6 @@ class VerificationTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
testHelper.waitForCallback<Unit> { callback ->
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
@ -177,11 +177,9 @@ class VerificationTest : InstrumentedTest {
)
)
}
}, callback
}
)
}
testHelper.waitForCallback<Unit> { callback ->
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
@ -194,64 +192,50 @@ class VerificationTest : InstrumentedTest {
)
)
}
}, callback
}
)
}
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession.cryptoService().verificationService()
var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null
var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null
val transactionId = aliceVerificationService.requestKeyVerificationInDMs(
aliceSupportedMethods, bobSession.myUserId, cryptoTestData.roomId
)
.transactionId
val latch = CountDownLatch(2)
val aliceListener = object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
// Step 4: Alice receive the ready request
if (pr.isReady) {
aliceReadyPendingVerificationRequest = pr
latch.countDown()
}
}
}
aliceVerificationService.addListener(aliceListener)
val bobListener = object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// Step 2: Bob accepts the verification request
bobVerificationService.readyPendingVerificationInDMs(
testHelper.retryPeriodically {
val incomingRequest = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, transactionId)
if (incomingRequest != null) {
bobVerificationService.readyPendingVerification(
bobSupportedMethods,
aliceSession.myUserId,
cryptoTestData.roomId,
pr.transactionId!!
incomingRequest.transactionId
)
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
// Step 3: Bob is ready
if (pr.isReady) {
bobReadyPendingVerificationRequest = pr
latch.countDown()
}
true
} else {
false
}
}
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
// Step 1: Alice starts a verification request
aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId)
testHelper.await(latch)
aliceReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported() shouldBe expectedResultForAlice.sasIsSupported
pr.otherCanShowQrCode() shouldBe expectedResultForAlice.otherCanShowQrCode
pr.otherCanScanQrCode() shouldBe expectedResultForAlice.otherCanScanQrCode
// wait for alice to see the ready
testHelper.retryPeriodically {
val pendingRequest = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId, transactionId)
pendingRequest?.state == EVerificationState.Ready
}
bobReadyPendingVerificationRequest!!.let { pr ->
pr.isSasSupported() shouldBe expectedResultForBob.sasIsSupported
pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode
pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode
val aliceReadyPendingVerificationRequest = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId, transactionId)!!
val bobReadyPendingVerificationRequest = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, transactionId)!!
aliceReadyPendingVerificationRequest.let { pr ->
pr.isSasSupported shouldBe expectedResultForAlice.sasIsSupported
pr.weShouldShowScanOption shouldBe expectedResultForAlice.otherCanShowQrCode
pr.weShouldDisplayQRCode shouldBe expectedResultForAlice.otherCanScanQrCode
}
bobReadyPendingVerificationRequest.let { pr ->
pr.isSasSupported shouldBe expectedResultForBob.sasIsSupported
pr.weShouldShowScanOption shouldBe expectedResultForBob.otherCanShowQrCode
pr.weShouldDisplayQRCode shouldBe expectedResultForBob.otherCanScanQrCode
}
}
@ -273,21 +257,42 @@ class VerificationTest : InstrumentedTest {
val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService()
val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService()
serviceOfVerifier.addListener(object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// Accept verification request
serviceOfVerifier.readyPendingVerification(
verificationMethods,
pr.otherUserId,
pr.transactionId!!,
)
var job: Job? = null
job = async {
serviceOfVerifier.requestEventFlow().collect {
when (it) {
is VerificationEvent.RequestAdded -> {
val pr = it.request
serviceOfVerifier.readyPendingVerification(
verificationMethods,
pr.otherUserId,
pr.transactionId,
)
job?.cancel()
}
is VerificationEvent.RequestUpdated,
is VerificationEvent.TransactionAdded,
is VerificationEvent.TransactionUpdated -> {
}
}
}
})
}
job.await()
// serviceOfVerifier.addListener(object : VerificationService.Listener {
// override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// // Accept verification request
// runBlocking {
// serviceOfVerifier.readyPendingVerification(
// verificationMethods,
// pr.otherUserId,
// pr.transactionId!!,
// )
// }
// }
// })
serviceOfVerified.requestKeyVerification(
serviceOfVerified.requestSelfKeyVerification(
methods = verificationMethods,
otherUserId = aliceSessionToVerify.myUserId,
otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
)
testHelper.retryPeriodically {
@ -295,8 +300,8 @@ class VerificationTest : InstrumentedTest {
requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
}
testHelper.signOutAndClose(aliceSessionToVerify)
testHelper.signOutAndClose(aliceSessionThatVerifies)
testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent)
// testHelper.signOutAndClose(aliceSessionToVerify)
// testHelper.signOutAndClose(aliceSessionThatVerifies)
// testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent)
}
}

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.session.room.timeline
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import org.amshove.kluent.fail
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
@ -45,8 +47,9 @@ import java.util.concurrent.CountDownLatch
@FixMethodOrder(MethodSorters.JVM)
class PollAggregationTest : InstrumentedTest {
// This test needs to be refactored, I am not sure it's working properly
@Test
fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, _ ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -57,14 +60,14 @@ class PollAggregationTest : InstrumentedTest {
// Bob creates a poll
roomFromBobPOV.sendService().sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
aliceSession.syncService().startSync(true)
val aliceTimeline = roomFromAlicePOV.timelineService().createTimeline(null, TimelineSettings(30))
aliceTimeline.start()
val TOTAL_TEST_COUNT = 7
val lock = CountDownLatch(TOTAL_TEST_COUNT)
val deff = CompletableDeferred<Unit>()
val aliceEventsListener = object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent ->
val pollEventId = pollEvent.eventId
@ -123,21 +126,28 @@ class PollAggregationTest : InstrumentedTest {
fail("Lock count ${lock.count} didn't handled.")
}
}
if (lock.count.toInt() == 0) deff.complete(Unit)
}
}
}
aliceTimeline.start()
aliceTimeline.addListener(aliceEventsListener)
commonTestHelper.await(lock)
// QUICK FIX
// This was locking the thread thus blocking the timeline updates
// Changed to a suspendable but this test is not well constructed..
// commonTestHelper.await(lock)
deff.await()
aliceTimeline.removeAllListeners()
aliceSession.syncService().stopSync()
aliceTimeline.dispose()
}
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testInitialPollConditions")
// No votes yet, poll summary should be null
pollSummary shouldBe null
// Question should be the same as intended
@ -150,6 +160,7 @@ class PollAggregationTest : InstrumentedTest {
}
private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testBobVotesOption1")
if (pollSummary == null) {
fail("Poll summary shouldn't be null when someone votes")
return
@ -165,6 +176,7 @@ class PollAggregationTest : InstrumentedTest {
}
private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testBobChangesVoteToOption2")
if (pollSummary == null) {
fail("Poll summary shouldn't be null when someone votes")
return
@ -180,6 +192,7 @@ class PollAggregationTest : InstrumentedTest {
}
private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testAliceAndBobVoteToOption2")
if (pollSummary == null) {
fail("Poll summary shouldn't be null when someone votes")
return
@ -196,6 +209,7 @@ class PollAggregationTest : InstrumentedTest {
}
private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testAliceVotesOption1AndBobVotesOption2")
if (pollSummary == null) {
fail("Poll summary shouldn't be null when someone votes")
return
@ -215,10 +229,12 @@ class PollAggregationTest : InstrumentedTest {
}
private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
Log.v("#E2E TEST", "testEndedPoll")
pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
}
private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
Log.v("#E2E TEST", "assertTotalVotesCount")
aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
}

View File

@ -124,8 +124,8 @@ class SpaceCreationTest : InstrumentedTest {
assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name)
assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
// commonTestHelper.signOutAndClose(aliceSession)
// commonTestHelper.signOutAndClose(bobSession)
}
@Test

View File

@ -334,7 +334,7 @@ class SpaceHierarchyTest : InstrumentedTest {
}
)
commonTestHelper.signOutAndClose(session)
// commonTestHelper.signOutAndClose(session)
}
data class TestSpaceCreationResult(

View File

@ -19,15 +19,12 @@ 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.Assert.assertNotNull
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.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.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@ -38,7 +35,7 @@ class PreShareKeysTest : InstrumentedTest {
@Test
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
@ -49,42 +46,47 @@ class PreShareKeysTest : InstrumentedTest {
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
Log.d("#Test", "Room Key Received from alice $preShareCount")
Log.d("#E2E", "Room Key Received from alice $preShareCount")
// Force presharing of new outbound key
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it)
}
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID)
testHelper.retryPeriodically {
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
newKeysCount > preShareCount
}
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
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 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()
// val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
//
// assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
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)
// 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 sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
val sentEventId = testHelper.sendMessageInRoom(aliceSession.getRoom(e2eRoomID)!!, "Allo")
assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
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())
}
}

View File

@ -116,9 +116,9 @@ class UnwedgingTest : InstrumentedTest {
// - 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().getMyDevice().identityKey()!!)
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)
sessionIdsForBob!!.size shouldBeEqualTo 1
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)!!
val oldSession = serializeForRealm(olmSession.olmSession)
@ -142,7 +142,7 @@ class UnwedgingTest : InstrumentedTest {
aliceCryptoStore.storeSession(
OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!),
bobSession.cryptoService().getMyDevice().identityKey()!!
bobSession.cryptoService().getMyCryptoDevice().identityKey()!!
)
olmDevice.clearOlmSessionCache()
@ -170,7 +170,6 @@ class UnwedgingTest : InstrumentedTest {
Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// It's a trick to force key request on fail to decrypt
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
@ -183,9 +182,7 @@ class UnwedgingTest : InstrumentedTest {
)
)
}
}, it
)
}
})
// Wait until we received back the key
testHelper.retryPeriodically {

View File

@ -0,0 +1,609 @@
/*
* 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
}
}
*/
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.migration
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import io.realm.Realm
import io.realm.kotlin.where
import org.amshove.kluent.internal.assertEquals
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.InstrumentedTest
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.store.db.model.CryptoMetadataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import org.matrix.android.sdk.internal.database.TestRealmConfigurationFactory
import org.matrix.android.sdk.internal.session.MigrateEAtoEROperation
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmManager
import org.matrix.rustcomponents.sdk.crypto.OlmMachine
import java.io.File
@RunWith(AndroidJUnit4::class)
class ElementAndroidToElementRMigrationTest : InstrumentedTest {
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
lateinit var context: Context
var realm: Realm? = null
@Before
fun setUp() {
// Ensure Olm is initialized
OlmManager()
context = InstrumentationRegistry.getInstrumentation().context
}
@After
fun tearDown() {
realm?.close()
}
@Test
fun given_a_valid_crypto_store_realm_file_then_migration_should_be_successful() {
val realmName = "crypto_store_migration_16.realm"
val migration = RealmCryptoStoreMigration(object : Clock {
override fun epochMillis() = 0L
})
val realmConfiguration = configurationFactory.createConfiguration(
realmName,
null,
RealmCryptoStoreModule(),
migration.schemaVersion,
migration
)
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
realm = Realm.getInstance(realmConfiguration)
val metaData = realm!!.where<CryptoMetadataEntity>().findFirst()!!
val userId = metaData.userId!!
val deviceId = metaData.deviceId!!
val olmAccount = metaData.getOlmAccount()!!
val extractor = MigrateEAtoEROperation()
val targetFile = File(configurationFactory.root, "rust-sdk")
extractor.execute(realmConfiguration, targetFile, null)
val machine = OlmMachine(userId, deviceId, targetFile.path, null)
assertEquals(olmAccount.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY], machine.identityKeys()["ed25519"])
assertNotNull(machine.getBackupKeys())
val crossSigningStatus = machine.crossSigningStatus()
assertTrue(crossSigningStatus.hasMaster)
assertTrue(crossSigningStatus.hasSelfSigning)
assertTrue(crossSigningStatus.hasUserSigning)
val inboundGroupSessionEntities = realm!!.where<OlmInboundGroupSessionEntity>().findAll()
assertEquals(inboundGroupSessionEntities.size, machine.roomKeyCounts().total.toInt())
val backedUpInboundGroupSessionEntities = realm!!
.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true)
.findAll()
assertEquals(backedUpInboundGroupSessionEntities.size, machine.roomKeyCounts().backedUp.toInt())
}
// @Test
// fun given_an_empty_crypto_store_realm_file_then_migration_should_not_happen() {
// val realmConfiguration = realmConfigurationFactory.configurationForMigrationFrom15To16(populateCryptoStore = false)
// Realm.getInstance(realmConfiguration).use {
// assertTrue(it.isEmpty)
// }
// val machine = OlmMachine("@ganfra146:matrix.org", "UTDQCHKKNS", realmConfigurationFactory.root.path, null)
// assertNull(machine.getBackupKeys())
// val crossSigningStatus = machine.crossSigningStatus()
// assertFalse(crossSigningStatus.hasMaster)
// assertFalse(crossSigningStatus.hasSelfSigning)
// assertFalse(crossSigningStatus.hasUserSigning)
// }
}

View File

@ -0,0 +1,61 @@
/*
* 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
}
}

View File

@ -0,0 +1,32 @@
/*
* 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)
}
}

View File

@ -5,7 +5,7 @@
* 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
* 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,

View File

@ -5,7 +5,7 @@
* 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
* 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,

View File

@ -36,7 +36,7 @@ data class MessageVerificationRequestContent(
@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 {
) : MessageContent, VerificationInfoRequest {
override fun toEventContent() = toContent()
}

View File

@ -0,0 +1,262 @@
/*
* 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.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 bindSendEventTask(task: DefaultSendEventTask): SendEventTask
@Binds
abstract fun bindInitalizeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
}

View File

@ -0,0 +1,145 @@
/*
* 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
)
}
}
}

View File

@ -53,7 +53,6 @@ import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListen
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
@ -73,7 +72,10 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.api.session.sync.model.DeviceListResponse
import org.matrix.android.sdk.api.session.sync.model.DeviceOneTimeKeysCountSyncResponse
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
@ -86,6 +88,7 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningSe
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -104,9 +107,7 @@ import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.time.Clock
@ -182,18 +183,27 @@ internal class DefaultCryptoService @Inject constructor(
private val loadRoomMembersTask: LoadRoomMembersTask,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor,
private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy<StreamEventsManager>,
private val unrequestedForwardManager: UnRequestedForwardManager,
) : CryptoService {
private val cryptoSyncHandler: CryptoSyncHandler,
) : CryptoService, DeviceListManager.UserDevicesUpdateListener {
private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
override fun name() = "kotlin-sdk"
override fun supportsKeyWithheld() = true
override fun supportKeyRequestInspection() = true
override fun supportsDisablingKeyGossiping() = true
override fun supportsForwardedKeyWiththeld() = true
override suspend fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
@ -201,7 +211,7 @@ internal class DefaultCryptoService @Inject constructor(
}
}
fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) {
override suspend fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) {
// handle state events
if (event.isStateEvent()) {
when (event.type) {
@ -214,8 +224,8 @@ internal class DefaultCryptoService @Inject constructor(
// handle verification
if (!isInitialSync) {
if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) {
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
verificationMessageProcessor.process(event)
withContext(coroutineDispatchers.dmVerif) {
verificationMessageProcessor.process(roomId, event)
}
}
}
@ -223,69 +233,48 @@ internal class DefaultCryptoService @Inject constructor(
// val gossipingBuffer = mutableListOf<Event>()
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
override suspend fun setDeviceName(deviceId: String, deviceName: String) {
setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// bg refresh of crypto device
downloadKeys(listOf(userId), true, NoOpMatrixCallback())
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
.execute(SetDeviceNameTask.Params(deviceId, deviceName))
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
downloadKeys(listOf(userId), true)
}
}
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback)
override suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor)
}
override fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
withContext(coroutineDispatchers.crypto) {
deleteDeviceTask
.execute(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null))
}
}
override fun getCryptoVersion(context: Context, longFormat: Boolean): String {
return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
}
override fun getMyDevice(): CryptoDeviceInfo {
override fun getMyCryptoDevice(): CryptoDeviceInfo {
return myDeviceInfoHolder.get().myDevice
}
override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask
.configureWith {
// this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<DevicesListResponse> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices.orEmpty())
callback.onSuccess(data)
}
}
}
.executeBy(taskExecutor)
override suspend fun fetchDevicesList(): List<DeviceInfo> {
val data = getDevicesTask
.execute(Unit)
cryptoStore.saveMyDevicesInfo(data.devices.orEmpty())
return data.devices.orEmpty()
}
override fun getMyDevicesInfoLive(): LiveData<List<DeviceInfo>> {
return cryptoStore.getLiveMyDevicesInfo()
}
override suspend fun fetchDeviceInfo(deviceId: String): DeviceInfo {
return getDeviceInfoTask.execute(GetDeviceInfoTask.Params(deviceId))
}
override fun getMyDevicesInfoLive(deviceId: String): LiveData<Optional<DeviceInfo>> {
return cryptoStore.getLiveMyDevicesInfo(deviceId)
}
@ -294,18 +283,10 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getMyDevicesInfo()
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}
/**
* Provides the tracking status.
*
* @param userId the user id
* @return the tracking status
*/
override fun getDeviceTrackingStatus(userId: String): Int {
return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED)
override suspend fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return withContext(coroutineDispatchers.io) {
cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}
}
/**
@ -313,7 +294,7 @@ internal class DefaultCryptoService @Inject constructor(
*
* @return true if the crypto is started
*/
fun isStarted(): Boolean {
override fun isStarted(): Boolean {
return isStarted.get()
}
@ -333,15 +314,14 @@ internal class DefaultCryptoService @Inject constructor(
* devices.
*
*/
fun start() {
override fun start() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart()
}
// Just update
fetchDevicesList(NoOpMatrixCallback())
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
tryOrNull("Failed to update device list on start") {
fetchDevicesList()
}
cryptoStore.tidyUpDataBase()
deviceListManager.addListener(this@DefaultCryptoService)
}
}
@ -360,6 +340,10 @@ internal class DefaultCryptoService @Inject constructor(
uploadDeviceKeys()
}
tryOrNull {
deviceListManager.recover()
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
// this can throw if no backup
tryOrNull {
@ -368,8 +352,8 @@ internal class DefaultCryptoService @Inject constructor(
}
}
fun onSyncWillProcess(isInitialSync: Boolean) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
override suspend fun onSyncWillProcess(isInitialSync: Boolean) {
withContext(coroutineDispatchers.crypto) {
if (isInitialSync) {
try {
// On initial sync, we start all our tracking from
@ -392,6 +376,7 @@ internal class DefaultCryptoService @Inject constructor(
return
}
isStarting.set(true)
ensureDevice()
// Open the store
cryptoStore.open()
@ -403,7 +388,8 @@ internal class DefaultCryptoService @Inject constructor(
/**
* Close the crypto.
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
override fun close() = runBlocking(coroutineDispatchers.crypto) {
deviceListManager.removeListener(this@DefaultCryptoService)
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingKeyRequestManager.close()
outgoingKeyRequestManager.close()
@ -433,81 +419,80 @@ internal class DefaultCryptoService @Inject constructor(
* @param syncResponse the syncResponse
* @param cryptoStoreAggregator data aggregated during the sync response treatment to store
*/
fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
override suspend fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) {
// if (syncResponse.deviceLists != null) {
// deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
// }
// if (syncResponse.deviceOneTimeKeysCount != null) {
// val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
// oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
// }
cryptoStore.storeData(cryptoStoreAggregator)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
}
if (syncResponse.deviceOneTimeKeysCount != null) {
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
// unwedge if needed
try {
eventDecryptor.unwedgeDevicesIfNeeded()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
}
// unwedge if needed
try {
eventDecryptor.unwedgeDevicesIfNeeded()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
}
// There is a limit of to_device events returned per sync.
// If we are in a case of such limited to_device sync we can't try to generate/upload
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
// the old otk too early. In this case we want to wait for the pending to_device before doing anything
// As per spec:
// If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response.
// 100 messages is recommended as a reasonable limit.
// The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure
// that there are no pending to_device
val toDevices = syncResponse.toDevice?.events.orEmpty()
if (isStarted() && toDevices.isEmpty()) {
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
// If there's no unused signed_curve25519 fallback key we need a new one.
if (syncResponse.deviceUnusedFallbackKeyTypes != null &&
// Generate a fallback key only if the server does not already have an unused fallback key.
!syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE)) {
oneTimeKeysUploader.needsNewFallback()
}
// There is a limit of to_device events returned per sync.
// If we are in a case of such limited to_device sync we can't try to generate/upload
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
// the old otk too early. In this case we want to wait for the pending to_device before doing anything
// As per spec:
// If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response.
// 100 messages is recommended as a reasonable limit.
// The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure
// that there are no pending to_device
val toDevices = syncResponse.toDevice?.events.orEmpty()
if (isStarted() && toDevices.isEmpty()) {
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
// If there's no unused signed_curve25519 fallback key we need a new one.
if (syncResponse.deviceUnusedFallbackKeyTypes != null &&
// Generate a fallback key only if the server does not already have an unused fallback key.
!syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE)) {
oneTimeKeysUploader.needsNewFallback()
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
}
// Process pending key requests
try {
if (toDevices.isEmpty()) {
// this is not blocking
outgoingKeyRequestManager.requireProcessAllPendingKeyRequests()
} else {
Timber.tag(loggerTag.value)
.w("Don't process key requests yet as there might be more to_device to catchup")
}
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process pending request")
}
// Process pending key requests
try {
if (toDevices.isEmpty()) {
// this is not blocking
outgoingKeyRequestManager.requireProcessAllPendingKeyRequests()
} else {
Timber.tag(loggerTag.value)
.w("Don't process key requests yet as there might be more to_device to catchup")
}
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process pending request")
}
try {
incomingKeyRequestManager.processIncomingRequests()
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
}
try {
incomingKeyRequestManager.processIncomingRequests()
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
}
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events ->
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
events.forEach {
onRoomKeyEvent(it, true)
}
}
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events ->
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
events.forEach {
onRoomKeyEvent(it, true)
}
}
}
}
override fun logDbUsageInfo() {
//
}
/**
* Find a device by curve25519 identity key.
*
@ -515,11 +500,18 @@ internal class DefaultCryptoService @Inject constructor(
* @param algorithm the encryption algorithm.
* @return the device info, or null if not found / unsupported algorithm / crypto released
*/
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? {
override suspend fun deviceWithIdentityKey(userId: String, senderKey: String, algorithm: String): CryptoDeviceInfo? {
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
// We only deal in olm keys
null
} else cryptoStore.deviceWithIdentityKey(senderKey)
} else {
withContext(coroutineDispatchers.io) {
cryptoStore.deviceWithIdentityKey(senderKey).takeIf {
// check that the claimed user id matches
it?.userId == userId
}
}
}
}
/**
@ -528,26 +520,32 @@ internal class DefaultCryptoService @Inject constructor(
* @param userId the user id
* @param deviceId the device id
*/
override fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(userId, deviceId)
withContext(coroutineDispatchers.io) {
cryptoStore.getUserDevice(userId, deviceId)
}
} else {
null
}
}
override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}
.executeBy(taskExecutor)
}
// override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
// getDeviceInfoTask
// .configureWith(GetDeviceInfoTask.Params(deviceId)) {
// this.executionThread = TaskThread.CRYPTO
// this.callback = callback
// }
// .executeBy(taskExecutor)
// }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
//
// override fun getCryptoDeviceInfoFlow(userId: String): Flow<List<CryptoDeviceInfo>> {
// return cryptoStore.getUserDeviceListFlow(userId)
// }
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
return cryptoStore.getLiveDeviceList()
@ -571,7 +569,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method.
* @param callback the asynchronous callback
*/
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
// build a devices map
val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId })
@ -609,7 +607,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param userId the owner of the device
* @param deviceId the unique identifier for the device.
*/
override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
override suspend fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
setDeviceVerificationAction.handle(trustLevel, userId, deviceId)
}
@ -691,8 +689,10 @@ internal class DefaultCryptoService @Inject constructor(
/**
* @return the stored device keys for a user.
*/
override fun getUserDevices(userId: String): MutableList<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList()
override suspend fun getUserDevices(userId: String): List<CryptoDeviceInfo> {
return withContext(coroutineDispatchers.io) {
cryptoStore.getUserDevices(userId)?.values?.toList().orEmpty()
}
}
private fun isEncryptionEnabledForInvitedUser(): Boolean {
@ -723,14 +723,13 @@ internal class DefaultCryptoService @Inject constructor(
* @param roomId the room identifier the event will be sent.
* @param callback the asynchronous callback
*/
override fun encryptEventContent(
override suspend fun encryptEventContent(
eventContent: Content,
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>
) {
): MXEncryptEventContentResult {
// moved to crypto scope to have uptodate values
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
return withContext(coroutineDispatchers.crypto) {
val userIds = getRoomUserIds(roomId)
var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
@ -745,11 +744,9 @@ internal class DefaultCryptoService @Inject constructor(
if (safeAlgorithm != null) {
val t0 = clock.epochMillis()
Timber.tag(loggerTag.value).v("encryptEventContent() starts")
runCatching {
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}.foldToCallback(callback)
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
return@withContext MXEncryptEventContentResult(content, EventType.ENCRYPTED)
} else {
val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(
@ -757,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor(
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON
)
Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))
}
}
}
@ -785,17 +782,6 @@ internal class DefaultCryptoService @Inject constructor(
return internalDecryptEvent(event, timeline)
}
/**
* 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
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
eventDecryptor.decryptEventAsync(event, timeline, callback)
}
/**
* Decrypt an event.
*
@ -805,7 +791,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
@Throws(MXCryptoError::class)
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return eventDecryptor.decryptEvent(event, timeline)
return withContext(coroutineDispatchers.crypto) { eventDecryptor.decryptEvent(event, timeline) }
}
/**
@ -865,7 +851,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the key event.
* @param acceptUnrequested, if true it will force to accept unrequested keys.
*/
private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
private suspend fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return
Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>")
@ -921,19 +907,27 @@ internal class DefaultCryptoService @Inject constructor(
): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
crossSigningService.onSecretMSKGossip(secretValue)
}
true
}
SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretValue)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
crossSigningService.onSecretSSKGossip(secretValue)
}
true
}
USER_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretUSKGossip(secretValue)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
crossSigningService.onSecretUSKGossip(secretValue)
}
true
}
KEYBACKUP_SECRET_SSSS_NAME -> {
keysBackupService.onSecretKeyGossip(secretValue)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
keysBackupService.onSecretKeyGossip(secretValue)
}
true
}
else -> false
@ -946,13 +940,13 @@ internal class DefaultCryptoService @Inject constructor(
* @param roomId the room Id
* @param event the encryption event.
*/
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
private suspend fun onRoomEncryptionEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) {
// Ignore
Timber.tag(loggerTag.value).w("Invalid encryption event")
return
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
withContext(coroutineDispatchers.io) {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
}
@ -970,7 +964,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param roomId the room Id
* @param event the membership event causing the change
*/
private fun onRoomMembershipEvent(roomId: String, event: Event) {
private suspend fun onRoomMembershipEvent(roomId: String, event: Event) {
// because the encryption event can be after the join/invite in the same batch
event.stateKey?.let { _ ->
val roomMember: RoomMemberContent? = event.content.toModel()
@ -979,47 +973,47 @@ internal class DefaultCryptoService @Inject constructor(
unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis())
}
}
roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
event.stateKey?.let { userId ->
val roomMember: RoomMemberContent? = event.content.toModel()
val membership = roomMember?.membership
if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE &&
shouldEncryptForInvitedMembers(roomId) &&
isEncryptionEnabledForInvitedUser()) {
// track the deviceList for this invited user.
// Caution: there's a big edge case here in that federated servers do not
// know what other servers are in the room at the time they've been invited.
// They therefore will not send device updates if a user logs in whilst
// their state is invite.
deviceListManager.startTrackingDeviceList(listOf(userId))
withContext(coroutineDispatchers.io) {
event.stateKey?.let { userId ->
val roomMember: RoomMemberContent? = event.content.toModel()
val membership = roomMember?.membership
if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE &&
shouldEncryptForInvitedMembers(roomId) &&
isEncryptionEnabledForInvitedUser()) {
// track the deviceList for this invited user.
// Caution: there's a big edge case here in that federated servers do not
// know what other servers are in the room at the time they've been invited.
// They therefore will not send device updates if a user logs in whilst
// their state is invite.
deviceListManager.startTrackingDeviceList(listOf(userId))
}
}
}
}
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
private suspend fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
val historyVisibility = eventContent?.historyVisibility
if (historyVisibility == null) {
if (cryptoStoreAggregator != null) {
cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false
withContext(coroutineDispatchers.io) {
if (historyVisibility == null) {
if (cryptoStoreAggregator != null) {
cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false
} else {
cryptoStore.setShouldShareHistory(roomId, false)
}
} else {
// Store immediately
cryptoStore.setShouldShareHistory(roomId, false)
}
} else {
if (cryptoStoreAggregator != null) {
cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED
cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory()
} else {
// Store immediately
cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
if (cryptoStoreAggregator != null) {
cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED
cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory()
} else {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
}
}
}
}
@ -1034,19 +1028,40 @@ internal class DefaultCryptoService @Inject constructor(
}
// Prepare the device keys data to send
// Sign it
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
var rest = getMyDevice().toRest()
val myCryptoDevice = getMyCryptoDevice()
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myCryptoDevice.signalableJSONDictionary())
var rest = myCryptoDevice.toRest()
rest = rest.copy(
signatures = objectSigner.signObject(canonicalJson)
)
val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null, null)
val keyUploadBody = KeysUploadBody(
deviceKeys = rest,
)
val uploadDeviceKeysParams = UploadKeysTask.Params(keyUploadBody)
uploadKeysTask.execute(uploadDeviceKeysParams)
cryptoStore.setDeviceKeysUploaded(true)
}
override suspend fun receiveSyncChanges(
toDevice: ToDeviceSyncResponse?,
deviceChanges: DeviceListResponse?,
keyCounts: DeviceOneTimeKeysCountSyncResponse?,
deviceUnusedFallbackKeyTypes: List<String>?
) {
withContext(coroutineDispatchers.crypto) {
deviceListManager.handleDeviceListsChanges(deviceChanges?.changed.orEmpty(), deviceChanges?.left.orEmpty())
if (keyCounts != null) {
val currentCount = keyCounts.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
cryptoSyncHandler.handleToDevice(toDevice?.events.orEmpty())
}
}
/**
* Export the crypto keys.
*
@ -1149,6 +1164,22 @@ internal class DefaultCryptoService @Inject constructor(
}
}
override suspend fun downloadKeysIfNeeded(userIds: List<String>, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
return deviceListManager.downloadKeys(userIds, forceDownload)
}
override suspend fun getCryptoDeviceInfoList(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
//
// fun getLiveCryptoDeviceInfoList(userId: String): Flow<List<CryptoDeviceInfo>> {
// cryptoStore.getLiveDeviceList(userId).asFlow()
// }
//
// fun getLiveCryptoDeviceInfoList(userIds: List<String>): Flow<List<CryptoDeviceInfo>> {
//
// }
/**
* Set the global override for whether the client should ever send encrypted
* messages to unverified devices.
@ -1169,6 +1200,8 @@ internal class DefaultCryptoService @Inject constructor(
override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled()
override fun supportsShareKeysOnInvite() = true
override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable)
/**
@ -1232,11 +1265,11 @@ internal class DefaultCryptoService @Inject constructor(
*
* @param event the event to decrypt again.
*/
override fun reRequestRoomKeyForEvent(event: Event) {
override suspend fun reRequestRoomKeyForEvent(event: Event) {
outgoingKeyRequestManager.requestKeyForEvent(event, true)
}
override fun requestRoomKeyForEvent(event: Event) {
suspend fun requestRoomKeyForEvent(event: Event) {
outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
@ -1282,12 +1315,8 @@ internal class DefaultCryptoService @Inject constructor(
return unknownDevices
}
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
deviceListManager.downloadKeys(userIds, forceDownload)
}.foldToCallback(callback)
}
suspend fun downloadKeys(userIds: List<String>, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
return deviceListManager.downloadKeys(userIds, forceDownload)
}
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
@ -1297,6 +1326,10 @@ internal class DefaultCryptoService @Inject constructor(
override fun removeSessionListener(listener: NewSessionListener) {
roomDecryptorProvider.removeSessionListener(listener)
}
override fun onUsersDeviceUpdate(userIds: List<String>) {
cryptoSessionInfoProvider.markMessageVerificationStateAsDirty(userIds)
}
/* ==========================================================================================
* DEBUG INFO
* ========================================================================================== */
@ -1351,8 +1384,8 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
}
override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
override suspend fun prepareToEncrypt(roomId: String) {
withContext(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
// Ensure to load all room members
try {
@ -1372,19 +1405,10 @@ internal class DefaultCryptoService @Inject constructor(
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason")
callback.onFailure(IllegalArgumentException("Missing algorithm"))
return@launch
throw IllegalArgumentException("Missing algorithm")
}
runCatching {
(alg as? IMXGroupEncryption)?.preshareKey(userIds)
}.fold(
{ callback.onSuccess(Unit) },
{
Timber.tag(loggerTag.value).e(it, "prepareToEncrypt() failed.")
callback.onFailure(it)
}
)
(alg as? IMXGroupEncryption)?.preshareKey(userIds)
}
}
@ -1412,6 +1436,14 @@ internal class DefaultCryptoService @Inject constructor(
}
}
override fun onE2ERoomMemberLoadedFromServer(roomId: String) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val userIds = getRoomUserIds(roomId)
// Because of LL we might want to update tracked users
deviceListManager.startTrackingDeviceList(userIds)
}
}
/* ==========================================================================================
* For test only
* ========================================================================================== */

View File

@ -17,7 +17,9 @@
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
@ -35,7 +37,6 @@ 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.task.TaskExecutor
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
@ -51,7 +52,7 @@ internal class DeviceListManager @Inject constructor(
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock,
matrixConfiguration: MatrixConfiguration
) {
@ -93,8 +94,9 @@ internal class DeviceListManager @Inject constructor(
private val cryptoCoroutineContext = coroutineDispatchers.crypto
init {
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
// 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) {
@ -142,7 +144,7 @@ internal class DeviceListManager @Inject constructor(
}
fun onRoomMembersLoadedFor(roomId: String) {
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
cryptoCoroutineScope.launch(cryptoCoroutineContext) {
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
// It's OK to track also device for invited users
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)

View File

@ -106,7 +106,7 @@ internal class EventDecryptor @Inject constructor(
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
verificationState = result.messageVerificationState
)
}
}

View File

@ -23,11 +23,15 @@ import org.matrix.android.sdk.api.extensions.orFalse
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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MessageVerificationState
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.algorithms.megolm.MXOutboundSessionInfo
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
@ -59,6 +63,7 @@ internal class MXOlmDevice @Inject constructor(
private val store: IMXCryptoStore,
private val olmSessionStore: OlmSessionStore,
private val inboundGroupSessionStore: InboundGroupSessionStore,
private val crossSigningOlm: CrossSigningOlm,
private val clock: Clock,
) {
@ -851,15 +856,61 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
val verificationState = if (sessionHolder.wrapper.sessionData.trusted.orFalse()) {
// let's get info on the device
val sendingDevice = store.deviceWithIdentityKey(senderKey)
if (sendingDevice == null) {
MessageVerificationState.UNKNOWN_DEVICE
} else {
val isDeviceOwnerOfSession = sessionHolder.wrapper.sessionData.keysClaimed?.get("ed25519") == sendingDevice.fingerprint()
if (!isDeviceOwnerOfSession) {
// should it fail to decrypt here?
MessageVerificationState.UNSAFE_SOURCE
} else if (sendingDevice.isVerified) {
MessageVerificationState.VERIFIED
} else {
val isDeviceOwnerVerified = store.getCrossSigningInfo(sendingDevice.userId)?.isTrusted() ?: false
val isDeviceSignedByItsOwner = isDeviceSignByItsOwner(sendingDevice)
if (isDeviceSignedByItsOwner) {
if (isDeviceOwnerVerified) MessageVerificationState.VERIFIED
else MessageVerificationState.SIGNED_DEVICE_OF_UNVERIFIED_USER
} else {
if (isDeviceOwnerVerified) MessageVerificationState.UN_SIGNED_DEVICE_OF_VERIFIED_USER
else MessageVerificationState.UN_SIGNED_DEVICE
}
}
}
} else {
MessageVerificationState.UNSAFE_SOURCE
}
return OlmDecryptionResult(
payload,
wrapper.sessionData.keysClaimed,
senderKey,
wrapper.sessionData.forwardingCurve25519KeyChain,
isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse()
isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse(),
verificationState = verificationState,
)
}
private fun isDeviceSignByItsOwner(device: CryptoDeviceInfo): Boolean {
val otherKeys = store.getCrossSigningInfo(device.userId) ?: return false
val otherSSKSignature = device.signatures?.get(device.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return false
// Check bob's device is signed by bob's SSK
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
device.canonicalSignable()
)
return true
} catch (e: Throwable) {
return false
}
}
/**
* Reset replay attack data for the given timeline.
*

View File

@ -61,7 +61,7 @@ internal class MyDeviceInfoHolder @Inject constructor(
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
myDevice = CryptoDeviceInfo(
credentials.deviceId!!,
credentials.deviceId,
credentials.userId,
keys = keys,
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),

View File

@ -19,6 +19,7 @@ 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
@ -138,7 +139,7 @@ internal class OneTimeKeysUploader @Inject constructor(
private suspend fun fetchOtkCount(): Int? {
return tryOrNull("Unable to get OTK count") {
val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null, null))
val result = uploadKeysTask.execute(UploadKeysTask.Params(KeysUploadBody()))
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
}
}
@ -227,9 +228,11 @@ internal class OneTimeKeysUploader @Inject constructor(
// 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(
deviceKeys = null,
oneTimeKeys = oneTimeJson,
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
KeysUploadBody(
deviceKeys = null,
oneTimeKeys = oneTimeJson,
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
)
)
return uploadKeysTask.executeRetry(uploadParams, 3)
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -74,11 +74,11 @@ internal class RoomDecryptorProvider @Inject constructor(
val alg = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply {
this.newSessionListener = object : NewSessionListener {
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
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, senderKey, sessionId)
it.onNewSession(roomId, sessionId)
} catch (ignore: Throwable) {
}
}

View File

@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
@ -36,7 +35,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -153,10 +151,7 @@ internal class SecretShareManager @Inject constructor(
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64()
else -> null
}
if (secretValue == null) {
@ -248,7 +243,7 @@ internal class SecretShareManager @Inject constructor(
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
sendToDeviceTask.execute(params)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -91,10 +91,21 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
}
// Let's now claim one time keys
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = withContext(coroutineDispatchers.io) {
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 ->

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -57,6 +57,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
progressListener: ProgressListener?
): ImportRoomKeysResult {
val t0 = clock.epochMillis()
val importedSession = mutableMapOf<String, MutableMap<String, MutableList<String>>>()
val totalNumbersOfKeys = megolmSessionsData.size
var lastProgress = 0
@ -70,18 +71,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
if (null != decrypting) {
try {
val sessionId = megolmSessionData.sessionId
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(
megolmSessionData.sessionId ?: "",
megolmSessionData.roomId ?: "",
megolmSessionData.senderKey ?: "",
sessionId,
roomId,
senderKey,
tryOrNull {
olmInboundGroupSessionWrappers
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
@ -93,7 +99,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
// Have another go at decrypting events sent with this session
when (decrypting) {
is MXMegolmDecryption -> {
decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!)
decrypting.onNewSession(megolmSessionData.roomId, senderKey, sessionId)
}
}
} catch (e: Exception) {
@ -121,6 +127,6 @@ internal class MegolmSessionDataImporter @Inject constructor(
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys, importedSession)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -29,7 +29,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
private val defaultKeysBackupService: DefaultKeysBackupService
) {
fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
suspend fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
val device = cryptoStore.getUserDevice(userId, deviceId)
// Sanity check

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -43,5 +43,5 @@ internal interface IMXDecrypting {
* @param defaultKeysBackupService the keys backup service
* @param forceAccept the keys backup service
*/
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.
@ -18,7 +18,6 @@ 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.extensions.orFalse
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
@ -100,7 +99,7 @@ internal class MXMegolmDecryption(
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty(),
isSafe = olmDecryptionResult.isSafe.orFalse()
messageVerificationState = olmDecryptionResult.verificationState,
).also {
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
}
@ -189,7 +188,7 @@ internal class MXMegolmDecryption(
* @param defaultKeysBackupService the keys backup service
* @param forceAccept if true will force to accept the forwarded key
*/
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
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
@ -360,6 +359,6 @@ internal class MXMegolmDecryption(
*/
fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
newSessionListener?.onNewSession(roomId, sessionId)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.

View File

@ -184,7 +184,9 @@ internal class MXMegolmEncryption(
trusted = true
)
defaultKeysBackupService.maybeBackupKeys()
cryptoCoroutineScope.launch {
defaultKeysBackupService.maybeBackupKeys()
}
return MXOutboundSessionInfo(
sessionId = sessionId,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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.

View File

@ -21,7 +21,8 @@ import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
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
@ -35,6 +36,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif
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
@ -48,8 +50,6 @@ 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.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
@ -127,7 +127,9 @@ internal class DefaultCrossSigningService @Inject constructor(
}
// Recover local trust in case private key are there?
setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
}
}
} catch (e: Throwable) {
// Mmm this kind of a big issue
@ -152,40 +154,30 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures.
*/
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) {
Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params(
interactiveAuthInterceptor = uiaInterceptor
)
initializeCrossSigningTask.configureWith(params) {
this.callbackThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "Error in initializeCrossSigning()")
callback.onFailure(failure)
}
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
val crossSigningInfo = MXCrossSigningInfo(
myUserId,
listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo),
true
)
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()) }
callback.onSuccess(Unit)
}
}
}.executeBy(taskExecutor)
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 fun onSecretMSKGossip(mskPrivateKey: String) {
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")
@ -212,7 +204,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun onSecretSSKGossip(sskPrivateKey: String) {
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")
@ -239,7 +231,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun onSecretUSKGossip(uskPrivateKey: String) {
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 ")
@ -265,7 +257,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun checkTrustFromPrivateKeys(
override suspend fun checkTrustFromPrivateKeys(
masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?
@ -328,7 +320,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
return UserTrustResult.Failure("Keys not trusted $mxCrossSigningInfo") // UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
} else {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
val checkSelfTrust = checkSelfTrust()
@ -354,18 +346,22 @@ internal class DefaultCrossSigningService @Inject constructor(
* USK
* .
*/
override fun isUserTrusted(otherUserId: String): Boolean {
return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
override suspend fun isUserTrusted(otherUserId: String): Boolean {
return withContext(coroutineDispatchers.io) {
cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
}
}
override fun isCrossSigningVerified(): Boolean {
return checkSelfTrust().isVerified()
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 fun checkUserTrust(otherUserId: String): UserTrustResult {
override suspend fun checkUserTrust(otherUserId: String): UserTrustResult {
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
if (otherUserId == myUserId) {
return checkSelfTrust()
@ -380,17 +376,17 @@ internal class DefaultCrossSigningService @Inject constructor(
return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
}
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
override fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
if (!myCrossSigningInfo.isTrusted()) {
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
// Let's get the other user master key
val otherMasterKey = otherInfo?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
?: return UserTrustResult.Failure("Unknown MSK for ${otherInfo?.userId}") // UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(myUserId) // Signatures made by me
@ -398,7 +394,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
return UserTrustResult.KeyNotSigned(otherMasterKey)
return UserTrustResult.Failure("MSK not signed by my USK $otherMasterKey") // UserTrustResult.KeyNotSigned(otherMasterKey)
}
// Check that Alice USK signature of Bob MSK is valid
@ -409,7 +405,7 @@ internal class DefaultCrossSigningService @Inject constructor(
otherMasterKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
return UserTrustResult.Failure("Invalid signature $masterKeySignaturesMadeByMyUserKey") // UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
}
return UserTrustResult.Success
@ -424,7 +420,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId))
}
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
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)
@ -473,7 +469,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
if (!isMaterKeyTrusted) {
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
val myUserKey = myCrossSigningInfo.userKey()
@ -485,7 +481,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK")
return UserTrustResult.KeyNotSigned(myUserKey)
return UserTrustResult.Failure("USK not signed by MSK") // UserTrustResult.KeyNotSigned(myUserKey)
}
// Check that Alice USK signature of Alice MSK is valid
@ -496,7 +492,7 @@ internal class DefaultCrossSigningService @Inject constructor(
myUserKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
return UserTrustResult.Failure("Invalid MSK signature of USK") // UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
}
val mySSKey = myCrossSigningInfo.selfSigningKey()
@ -508,7 +504,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK")
return UserTrustResult.KeyNotSigned(mySSKey)
return UserTrustResult.Failure("SSK not signed by MSK") // UserTrustResult.KeyNotSigned(mySSKey)
}
// Check that Alice USK signature of Alice MSK is valid
@ -519,26 +515,32 @@ internal class DefaultCrossSigningService @Inject constructor(
mySSKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
return UserTrustResult.Failure("Invalid signature $ssKeySignaturesMadeByMyMasterKey") // UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
}
return UserTrustResult.Success
}
override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(otherUserId)
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 fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return withContext(coroutineDispatchers.io) {
cryptoStore.getMyCrossSigningInfo()
}
}
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
return cryptoStore.getCrossSigningPrivateKeys()
override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
return withContext(coroutineDispatchers.io) {
cryptoStore.getCrossSigningPrivateKeys()
}
}
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
@ -555,24 +557,20 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
}
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
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) {
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
return@launch
throw Throwable("## CrossSigning - Other master signing key is not known")
}
val myKeys = getUserCrossSigningKeys(myUserId)
if (myKeys == null) {
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
return@launch
}
?: throw Throwable("## CrossSigning - CrossSigning is not setup for this account")
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || crossSigningOlm.userPkSigning == null) {
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
return@launch
throw Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")
}
// Sign the other MasterKey with our UserSigning key
@ -580,12 +578,8 @@ internal class DefaultCrossSigningService @Inject constructor(
Map::class.java,
otherMasterKeys.signalableJSONDictionary()
).let { crossSigningOlm.userPkSigning?.sign(it) }
if (newSignature == null) {
// race??
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
return@launch
}
?: // race??
throw Throwable("## CrossSigning - Failed to sign")
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
@ -593,10 +587,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val uploadQuery = UploadSignatureQueryBuilder()
.withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature))
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
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 ->
@ -607,8 +599,8 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun markMyMasterKeyAsTrusted() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
override suspend fun markMyMasterKeyAsTrusted() {
withContext(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust()
// re-verify all trusts
@ -616,35 +608,26 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
override suspend fun trustDevice(deviceId: String) {
withContext(coroutineDispatchers.crypto) {
// This device should be yours
val device = cryptoStore.getUserDevice(myUserId, deviceId)
if (device == null) {
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
return@launch
throw IllegalArgumentException("This device [$deviceId] is not known, or not yours")
}
val myKeys = getUserCrossSigningKeys(myUserId)
if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
return@launch
}
?: throw Throwable("CrossSigning is not setup for this account")
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
return@launch
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")
if (newSignature == null) {
// race??
callback.onFailure(Throwable("Failed to sign"))
return@launch
}
val toUpload = device.copy(
signatures = mapOf(
myUserId
@ -658,14 +641,16 @@ internal class DefaultCrossSigningService @Inject constructor(
val uploadQuery = UploadSignatureQueryBuilder()
.withDeviceInfo(toUpload)
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery))
}
}
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
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)
@ -787,10 +772,12 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
checkTrustAndAffectedRoomShields(userIds)
runBlocking {
checkTrustAndAffectedRoomShields(userIds)
}
}
fun checkTrustAndAffectedRoomShields(userIds: List<String>) {
override suspend fun checkTrustAndAffectedRoomShields(userIds: List<String>) {
Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}")
val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
@ -808,7 +795,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.enqueue()
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
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?
@ -818,7 +805,10 @@ internal class DefaultCrossSigningService @Inject constructor(
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
// called within a real transaction, has to block
runBlocking {
checkUserTrust(it).isVerified()
}
}
users.forEach {

View File

@ -15,11 +15,9 @@
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import android.util.Base64
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
import timber.log.Timber
internal fun CryptoDeviceInfo.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
@ -28,15 +26,3 @@ internal fun CryptoDeviceInfo.canonicalSignable(): String {
internal fun CryptoCrossSigningKey.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
}
/**
* Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source
*/
internal fun String.fromBase64Safe(): ByteArray? {
return try {
Base64.decode(this, Base64.DEFAULT)
} catch (throwable: Throwable) {
Timber.e(throwable, "Unable to decode base64 string")
null
}
}

Some files were not shown because too many files have changed in this diff Show More