Merge branch 'release/1.6.8' into main

This commit is contained in:
Benoit Marty 2023-11-28 17:18:51 +01:00
commit 552b143f8c
234 changed files with 905 additions and 24003 deletions

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 }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES
run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES
- name: Upload ${{ matrix.target }} debug APKs
uses: actions/upload-artifact@v3
with:
@ -57,7 +57,7 @@ jobs:
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble GPlay unsigned apk
run: ./gradlew clean assembleGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload Gplay unsigned APKs
uses: actions/upload-artifact@v3
with:
@ -79,7 +79,7 @@ jobs:
- name: Execute exodus-standalone
uses: docker://exodusprivacy/exodus-standalone:latest
with:
args: /github/workspace/gplayRustCrypto/release/vector-gplay-rustCrypto-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
args: /github/workspace/gplay/release/vector-gplay-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
- name: Upload exodus json report
uses: actions/upload-artifact@v3
with:

View file

@ -1,37 +0,0 @@
name: ER APK Build
on:
pull_request: { }
push:
branches: [ develop ]
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
jobs:
debug:
name: Build debug APKs ER
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/main'
strategy:
fail-fast: false
matrix:
target: [ Gplay, Fdroid ]
# Allow all jobs on develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/develop' && format('elementr-{0}-{1}', matrix.target, github.sha) || format('build-er-debug-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Assemble ${{ matrix.target }} debug apk
run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES

View file

@ -35,7 +35,7 @@ jobs:
yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly APK
run: |
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
env:
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}

View file

@ -1,46 +0,0 @@
name: Build and release Element R nightly APK
on:
schedule:
# Every nights at 4
- cron: "0 4 * * *"
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
jobs:
nightly:
name: Build and publish ER nightly Gplay APK to Firebase
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.8
uses: actions/setup-python@v4
with:
python-version: 3.8
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Install towncrier
run: |
python3 -m pip install towncrier
- name: Prepare changelog file
run: |
mv towncrier.toml towncrier.toml.bak
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak
yes n | towncrier build --version nightly
- name: Build and upload Gplay Nightly ER APK
run: |
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
env:
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
FIREBASE_TOKEN: ${{ secrets.ELEMENT_R_NIGHTLY_FIREBASE_TOKEN }}

View file

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

View file

@ -1,102 +0,0 @@
name: Test
on:
pull_request: { }
push:
branches: [ main, develop ]
paths-ignore:
- '.github/**'
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx5g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx3g" -Dkotlin.incremental=false
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon
jobs:
tests:
name: Runs all tests with rust crypto
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 90 # We might need to increase it if the time for tests grows
strategy:
matrix:
api-level: [28]
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-rust-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-rust-{0}', github.sha) || format('unit-tests-rust-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
lfs: true
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'
- uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/develop' }}
# - name: Run screenshot tests
# run: ./gradlew verifyScreenshots $CI_GRADLE_ARG_PROPERTIES
# - name: Archive Screenshot Results on Error
# if: failure()
# uses: actions/upload-artifact@v3
# with:
# name: screenshot-results
# path: |
# **/out/failures/
# **/build/reports/tests/*UnitTest/
- uses: actions/setup-python@v4
with:
python-version: 3.8
- uses: michaelkaye/setup-matrix-synapse@v1.0.4
with:
uploadLogs: true
httpPort: 8080
disableRateLimiting: true
public_baseurl: "http://10.0.2.2:8080/"
- name: Run all the codecoverage tests at once
uses: reactivecircus/android-emulator-runner@v2
# continue-on-error: true
with:
api-level: ${{ matrix.api-level }}
arch: x86
profile: Nexus 5X
target: playstore
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
# emulator-build: 7425822
script: |
./gradlew gatherGplayRustCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
./gradlew instrumentationTestsRustWithCoverage $CI_GRADLE_ARG_PROPERTIES
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
- name: Upload Rust Integration Test Report Log
uses: actions/upload-artifact@v3
if: always()
with:
name: integration-test-rust-error-results
path: |
*/build/outputs/androidTest-results/connected/
*/build/reports/androidTests/connected/
# For now ignore sonar
# - name: Publish results to Sonar
# env:
# GITHUB_TOKEN: ${{ secrets.SONARQUBE_GITHUB_API_TOKEN }} # Needed to get PR information, if any
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
# if: ${{ always() && env.GITHUB_TOKEN != '' && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
# run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
- name: Format unit test results
if: always()
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml

View file

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

View file

@ -1,3 +1,21 @@
Changes in Element v1.6.8 (2023-11-28)
======================================
Bugfixes 🐛
----------
- Stop incoming call ringing if the call is cancelled or answered on another session. ([#4066](https://github.com/vector-im/element-android/issues/4066))
- Ensure the incoming call will not ring forever, in case the call is not ended by another way. ([#8178](https://github.com/vector-im/element-android/issues/8178))
- Unified Push: Ignore the potential SSL error when the custom gateway is testing locally ([#8683](https://github.com/vector-im/element-android/issues/8683))
- Fix issue with timeline message view reuse while rich text editor is enabled ([#8688](https://github.com/vector-im/element-android/issues/8688))
Other changes
-------------
- Remove unused WebRTC dependency ([#8658](https://github.com/vector-im/element-android/issues/8658))
- Take into account boolean "io.element.disable_network_constraint" from the .well-known file. ([#8662](https://github.com/vector-im/element-android/issues/8662))
- Update regex for email address to be aligned on RFC 5322 ([#8671](https://github.com/vector-im/element-android/issues/8671))
- Bump crypto sdk bindings to v0.3.16 ([#8679](https://github.com/vector-im/element-android/issues/8679))
Changes in Element v1.6.6 (2023-10-05)
======================================

View file

@ -312,7 +312,7 @@ tasks.register("recordScreenshots", GradleBuild) {
tasks.register("verifyScreenshots", GradleBuild) {
startParameter.projectProperties.screenshot = ""
tasks = [':vector:verifyPaparazziRustCryptoDebug']
tasks = [':vector:verifyPaparazziDebug']
}
ext.initScreenshotTests = { project ->

View file

@ -87,11 +87,5 @@ task unitTestsWithCoverage(type: GradleBuild) {
task instrumentationTestsWithCoverage(type: GradleBuild) {
startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
}
task instrumentationTestsRustWithCoverage(type: GradleBuild) {
startParameter.projectProperties.coverage = "true"
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
tasks = [':vector-app:connectedGplayRustCryptoDebugAndroidTest', ':vector:connectedRustCryptoDebugAndroidTest', 'matrix-sdk-android:connectedRustCryptoDebugAndroidTest']
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
}

View file

@ -20,7 +20,7 @@ def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
def flipper = "0.190.0"
def epoxy = "5.0.0"
def mavericks = "3.0.2"
def mavericks = "3.0.7"
def glide = "4.15.1"
def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
@ -101,7 +101,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:2.2.2"
'wysiwyg' : "io.element.android:wysiwyg:2.14.1"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",

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 assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
```
Then you can reset the change on the codebase.

View file

@ -0,0 +1,2 @@
Main changes in this version: Bugfixes.
Full changelog: https://github.com/vector-im/element-android/releases

View file

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

View file

@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret
# Customise the Lint version to use a more recent version than the one bundled with AGP
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
android.experimental.lint.version=8.0.0-alpha10
android.experimental.lint.version=8.3.0-alpha12

View file

@ -3,7 +3,6 @@ plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
apply from: '../flavor.gradle'
android {
namespace "org.matrix.android.sdk.flow"

View file

@ -41,7 +41,6 @@ dokkaHtml {
}
}
}
apply from: '../flavor.gradle'
android {
namespace "org.matrix.android.sdk"
@ -63,7 +62,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.6.6\""
buildConfigField "String", "SDK_VERSION", "\"1.6.8\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -158,7 +157,7 @@ dependencies {
// implementation libs.androidx.appCompat
implementation libs.androidx.core
rustCryptoImplementation libs.androidx.lifecycleLivedata
implementation libs.androidx.lifecycleLivedata
// Lifecycle
implementation libs.androidx.lifecycleCommon
@ -216,8 +215,8 @@ dependencies {
implementation libs.google.phonenumber
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.15")
// rustCryptoApi project(":library:rustCrypto")
implementation("org.matrix.rustcomponents:crypto-android:0.3.16")
// api project(":library:rustCrypto")
testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281

View file

@ -261,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
authData = createFakeMegolmBackupAuthData(),
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")!!
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")
)
}

View file

@ -28,14 +28,12 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
@ -197,7 +195,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
@Test
fun testNeedsRotationFromSharedToWorldReadable() {
Assume.assumeTrue("Test is flacky on legacy crypto", BuildConfig.FLAVOR == "rustCrypto")
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
}

View file

@ -542,7 +542,7 @@ class KeysBackupTest : InstrumentedTest {
assertFails {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key")!!,
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key"),
)
}
@ -680,7 +680,7 @@ class KeysBackupTest : InstrumentedTest {
assertFailsWith<InvalidParameterException> {
keysBackupService.restoreKeysWithRecoveryKey(
keysBackupService.keysBackupVersion!!,
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")!!,
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"),
null,
null,
null,

View file

@ -1,44 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import io.realm.RealmConfiguration
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.time.DefaultClock
import kotlin.random.Random
internal class CryptoStoreHelper {
fun createStore(): IMXCryptoStore {
return RealmCryptoStore(
realmConfiguration = RealmConfiguration.Builder()
.name("test.realm")
.modules(RealmCryptoStoreModule())
.build(),
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
userId = "userId_" + Random.nextInt(),
deviceId = "deviceId_sample",
clock = DefaultClock(),
myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
)
}
}

View file

@ -1,142 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.realm.Realm
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.util.time.DefaultClock
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmManager
import org.matrix.olm.OlmSession
private const val DUMMY_DEVICE_KEY = "DeviceKey"
@RunWith(AndroidJUnit4::class)
@Ignore
class CryptoStoreTest : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
private val cryptoStoreHelper = CryptoStoreHelper()
private val clock = DefaultClock()
@Before
fun setup() {
Realm.init(context())
}
// @Test
// fun test_metadata_realm_ok() {
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
//
// assertFalse(cryptoStore.hasData())
//
// cryptoStore.open()
//
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
//
// assertTrue(cryptoStore.hasData())
//
// // Cleanup
// cryptoStore.close()
// cryptoStore.deleteStore()
// }
@Test
fun test_lastSessionUsed() {
// Ensure Olm is initialized
OlmManager()
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
val olmAccount1 = OlmAccount().apply {
generateOneTimeKeys(1)
}
val olmSession1 = OlmSession().apply {
initOutboundSession(
olmAccount1,
olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
)
}
val sessionId1 = olmSession1.sessionIdentifier()
val olmSessionWrapper1 = OlmSessionWrapper(olmSession1)
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
val olmAccount2 = OlmAccount().apply {
generateOneTimeKeys(1)
}
val olmSession2 = OlmSession().apply {
initOutboundSession(
olmAccount2,
olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
)
}
val sessionId2 = olmSession2.sessionIdentifier()
val olmSessionWrapper2 = OlmSessionWrapper(olmSession2)
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
// Ensure sessionIds are distinct
assertNotEquals(sessionId1, sessionId2)
// Note: we cannot be sure what will be the result of getLastUsedSessionId() here
olmSessionWrapper2.onMessageReceived(clock.epochMillis())
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
// sessionId2 is returned now
assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
Thread.sleep(2)
olmSessionWrapper1.onMessageReceived(clock.epochMillis())
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
// sessionId1 is returned now
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
// Cleanup
olmSession1.releaseSession()
olmSession2.releaseSession()
olmAccount1.releaseAccount()
olmAccount2.releaseAccount()
}
}

View file

@ -1,92 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class PreShareKeysTest : InstrumentedTest {
@Test
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
// clear any outbound session
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
Log.d("#E2E", "Room Key Received from alice $preShareCount")
// Force presharing of new outbound key
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID)
testHelper.retryPeriodically {
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
newKeysCount > preShareCount
}
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
// val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
//
// val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
// val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId)!!
// val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
// assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
// assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
// val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
//
// assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
// val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
// .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
//
// assertEquals("The session received by bob should match what alice sent", 0, sharedIndex)
// Just send a real message as test
val sentEventId = testHelper.sendMessageInRoom(aliceSession.getRoom(e2eRoomID)!!, "Allo")
val sentEvent = aliceSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!
// assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
testHelper.retryPeriodically {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
}
// check that no additional key was shared
assertEquals(newKeysCount, bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys())
}
}

View file

@ -1,225 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
import org.matrix.olm.OlmSession
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
/**
* Ref:
* - https://github.com/matrix-org/matrix-doc/pull/1719
* - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages
* - https://github.com/matrix-org/matrix-js-sdk/pull/780
* - https://github.com/matrix-org/matrix-ios-sdk/pull/778
* - https://github.com/matrix-org/matrix-ios-sdk/pull/784
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class UnwedgingTest : InstrumentedTest {
private lateinit var messagesReceivedByBob: List<TimelineEvent>
@Before
fun init() {
messagesReceivedByBob = emptyList()
}
/**
* - Alice & Bob in a e2e room
* - Alice sends a 1st message with a 1st megolm session
* - Store the olm session between A&B devices
* - Alice sends a 2nd message with a 2nd megolm session
* - Simulate Alice using a backup of her OS and make her crypto state like after the first message
* - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
*
* What Bob must see:
* -> No issue with the 2 first messages
* -> The third event must fail to decrypt at first because Bob the olm session is wedged
* -> This is automatically fixed after SDKs restarted the olm session
*/
@Test
fun testUnwedging() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val bobSession = cryptoTestData.secondSession!!
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20))
bobTimeline.start()
messagesReceivedByBob = emptyList()
// - Alice sends a 1st message with a 1st megolm session
roomFromAlicePOV.sendService().sendTextMessage("First message")
// Wait for the message to be received by Bob
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1)
messagesReceivedByBob.size shouldBeEqualTo 1
val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// - Store the olm session between A&B devices
// Let us pickle our session with bob here so we can later unpickle it
// and wedge our session.
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)
sessionIdsForBob!!.size shouldBeEqualTo 1
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)!!
val oldSession = serializeForRealm(olmSession.olmSession)
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
messagesReceivedByBob = emptyList()
Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
// - Alice sends a 2nd message with a 2nd megolm session
roomFromAlicePOV.sendService().sendTextMessage("Second message")
// Wait for the message to be received by Bob
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2)
messagesReceivedByBob.size shouldBeEqualTo 2
// Session should have changed
val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Assert.assertNotEquals(firstMessageSession, secondMessageSession)
// Let us wedge the session now. Set crypto state like after the first message
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
aliceCryptoStore.storeSession(
OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!),
bobSession.cryptoService().getMyCryptoDevice().identityKey()!!
)
olmDevice.clearOlmSessionCache()
// Force new session, and key share
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
// - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
roomFromAlicePOV.sendService().sendTextMessage("Third message")
// Bob should not be able to decrypt, because the session key could not be sent
// Wait for the message to be received by Bob
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3)
messagesReceivedByBob.size shouldBeEqualTo 3
val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
Assert.assertNotEquals(secondMessageSession, thirdMessageSession)
Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType())
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
// Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// It's a trick to force key request on fail to decrypt
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
})
// Wait until we received back the key
testHelper.retryPeriodically {
// we should get back the key and be able to decrypt
val result = tryOrNull {
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
}
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
}
bobTimeline.dispose()
}
}
private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> {
return suspendCancellableCoroutine { continuation ->
val listener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED }
if (messagesReceived.size == expectedCount) {
removeListener(this)
continuation.resume(messagesReceived)
}
}
}
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
}
}

View file

@ -1,609 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class SASTest : InstrumentedTest {
val scope = CoroutineScope(SupervisorJob())
@Test
fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
Log.d("#E2E", "verification: doE2ETestWithAliceAndBobInARoom")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
Log.d("#E2E", "verification: initializeCrossSigning")
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService()
Log.d("#E2E", "verification: requestVerificationAndWaitForReadyState")
val txId = SasVerificationTestHelper(testHelper)
.requestVerificationAndWaitForReadyState(scope, cryptoTestData, listOf(VerificationMethod.SAS))
Log.d("#E2E", "verification: startKeyVerification")
aliceVerificationService.startKeyVerification(
VerificationMethod.SAS,
bobSession.myUserId,
txId
)
Log.d("#E2E", "verification: ensure bob has received start")
testHelper.retryWithBackoff {
Log.d("#E2E", "verification: ${bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state}")
bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state == EVerificationState.Started
}
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId)
assertNotNull("Bob should have started verif transaction", bobKeyTx)
assertTrue(bobKeyTx is SasVerificationTransaction)
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId)
assertTrue(aliceKeyTx is SasVerificationTransaction)
assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
val aliceCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
aliceVerificationService.requestEventFlow().onEach {
Log.d("#E2E", "alice flow event $it | ${it.getTransaction()?.dbgState()}")
val tx = it.getTransaction()
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
if (tx.state() is SasTransactionState.Cancelled) {
aliceCancelled.complete(tx.state() as SasTransactionState.Cancelled)
}
}
}.launchIn(scope)
val bobCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
bobVerificationService.requestEventFlow().onEach {
Log.d("#E2E", "bob flow event $it | ${it.getTransaction()?.dbgState()}")
val tx = it.getTransaction()
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
if (tx.state() is SasTransactionState.Cancelled) {
bobCancelled.complete(tx.state() as SasTransactionState.Cancelled)
}
}
}.launchIn(scope)
aliceVerificationService.cancelVerificationRequest(bobSession.myUserId, txId)
val cancelledAlice = aliceCancelled.await()
val cancelledBob = bobCancelled.await()
assertEquals("Should be User cancelled on alice side", CancelCode.User, cancelledAlice.cancelCode)
assertEquals("Should be User cancelled on bob side", CancelCode.User, cancelledBob.cancelCode)
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId))
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId))
}
/*
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val bobSession = cryptoTestData.secondSession!!
val protocols = listOf("meh_dont_know")
val tid = "00000000"
// Bob should receive a cancel
var cancelReason: CancelCode? = null
val cancelLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
tx as SasVerificationTransaction
if (tx.transactionId == tid && tx.state() is SasTransactionState.Cancelled) {
cancelReason = (tx.state() as SasTransactionState.Cancelled).cancelCode
cancelLatch.countDown()
}
}
}
// bobSession.cryptoService().verificationService().addListener(bobListener)
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
// TODO override fun onToDeviceEvent(event: Event?) {
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
// TODO canceledToDeviceEvent = event
// TODO cancelLatch.countDown()
// TODO }
// TODO }
// TODO }
// TODO })
val aliceSession = cryptoTestData.firstSession
val aliceUserID = aliceSession.myUserId
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
tx as SasVerificationTransaction
if (tx.state() is SasTransactionState.SasStarted) {
runBlocking {
tx.acceptVerification()
}
}
}
}
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
testHelper.await(cancelLatch)
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val bobSession = cryptoTestData.secondSession!!
val mac = listOf("shaBit")
val tid = "00000000"
// Bob should receive a cancel
val canceledToDeviceEvent: Event? = null
val cancelLatch = CountDownLatch(1)
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
// TODO override fun onToDeviceEvent(event: Event?) {
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
// TODO canceledToDeviceEvent = event
// TODO cancelLatch.countDown()
// TODO }
// TODO }
// TODO }
// TODO })
val aliceSession = cryptoTestData.firstSession
val aliceUserID = aliceSession.myUserId
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac)
testHelper.await(cancelLatch)
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val bobSession = cryptoTestData.secondSession!!
val codes = listOf("bin", "foo", "bar")
val tid = "00000000"
// Bob should receive a cancel
var canceledToDeviceEvent: Event? = null
val cancelLatch = CountDownLatch(1)
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
// TODO override fun onToDeviceEvent(event: Event?) {
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
// TODO canceledToDeviceEvent = event
// TODO cancelLatch.countDown()
// TODO }
// TODO }
// TODO }
// TODO })
val aliceSession = cryptoTestData.firstSession
val aliceUserID = aliceSession.myUserId
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes)
testHelper.await(cancelLatch)
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
}
private suspend fun fakeBobStart(
bobSession: Session,
aliceUserID: String?,
aliceDevice: String?,
tid: String,
protocols: List<String> = SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
hashes: List<String> = SasVerificationTransaction.KNOWN_HASHES,
mac: List<String> = SasVerificationTransaction.KNOWN_MACS,
codes: List<String> = SasVerificationTransaction.KNOWN_SHORT_CODES
) {
val startMessage = KeyVerificationStart(
fromDevice = bobSession.cryptoService().getMyCryptoDevice().deviceId,
method = VerificationMethod.SAS.toValue(),
transactionId = tid,
keyAgreementProtocols = protocols,
hashes = hashes,
messageAuthenticationCodes = mac,
shortAuthenticationStrings = codes
)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
// TODO val sendLatch = CountDownLatch(1)
// TODO bobSession.cryptoRestClient.sendToDevice(
// TODO EventType.KEY_VERIFICATION_START,
// TODO contentMap,
// TODO tid,
// TODO TestMatrixCallback<Void>(sendLatch)
// TODO )
}
// any two devices may only have at most one key verification in flight at a time.
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
@Test
fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val aliceCreatedLatch = CountDownLatch(2)
val aliceCancelledLatch = CountDownLatch(1)
val createdTx = mutableListOf<VerificationTransaction>()
val aliceListener = object : VerificationService.Listener {
override fun transactionCreated(tx: VerificationTransaction) {
createdTx.add(tx)
aliceCreatedLatch.countDown()
}
override fun transactionUpdated(tx: VerificationTransaction) {
tx as SasVerificationTransaction
if (tx.state() is SasTransactionState.Cancelled && !(tx.state() as SasTransactionState.Cancelled).byMe) {
aliceCancelledLatch.countDown()
}
}
}
// aliceVerificationService.addListener(aliceListener)
val bobUserId = bobSession!!.myUserId
val bobDeviceId = bobSession.cryptoService().getMyCryptoDevice().deviceId
// TODO
// aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), forceDownload = true)
// aliceVerificationService.beginKeyVerification(listOf(VerificationMethod.SAS), bobUserId, bobDeviceId)
// aliceVerificationService.beginKeyVerification(bobUserId, bobDeviceId)
// testHelper.await(aliceCreatedLatch)
// testHelper.await(aliceCancelledLatch)
cryptoTestData.cleanUp(testHelper)
}
/**
* Test that when alice starts a 'correct' request, bob agrees.
*/
// @Test
// @Ignore("This test will be ignored until it is fixed")
// fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
//
// val aliceSession = cryptoTestData.firstSession
// val bobSession = cryptoTestData.secondSession
//
// val aliceVerificationService = aliceSession.cryptoService().verificationService()
// val bobVerificationService = bobSession!!.cryptoService().verificationService()
//
// val aliceAcceptedLatch = CountDownLatch(1)
// val aliceListener = object : VerificationService.Listener {
// override fun transactionUpdated(tx: VerificationTransaction) {
// if (tx.state() is VerificationTxState.OnAccepted) {
// aliceAcceptedLatch.countDown()
// }
// }
// }
// aliceVerificationService.addListener(aliceListener)
//
// val bobListener = object : VerificationService.Listener {
// override fun transactionUpdated(tx: VerificationTransaction) {
// if (tx.state() is VerificationTxState.OnStarted && tx is SasVerificationTransaction) {
// bobVerificationService.removeListener(this)
// runBlocking {
// tx.acceptVerification()
// }
// }
// }
// }
// bobVerificationService.addListener(bobListener)
//
// val bobUserId = bobSession.myUserId
// val bobDeviceId = runBlocking {
// bobSession.cryptoService().getMyCryptoDevice().deviceId
// }
//
// aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
// testHelper.await(aliceAcceptedLatch)
//
// aliceVerificationService.getExistingTransaction(bobUserId, )
//
// assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
//
// // check that agreement is valid
// assertTrue("Agreed Protocol should be Valid", accepted != null)
// assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol))
// assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash))
// assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode))
//
// accepted!!.shortAuthenticationStrings.forEach {
// assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
// }
// }
// @Test
// fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
// cryptoTestData.initializeCrossSigning(cryptoTestHelper)
// val sasTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
// val aliceSession = cryptoTestData.firstSession
// val bobSession = cryptoTestData.secondSession!!
// val transactionId = sasTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, supportedMethods)
//
// val latch = CountDownLatch(2)
// val aliceListener = object : VerificationService.Listener {
// override fun transactionUpdated(tx: VerificationTransaction) {
// Timber.v("Alice transactionUpdated: ${tx.state()}")
// latch.countDown()
// }
// }
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
// val bobListener = object : VerificationService.Listener {
// override fun transactionUpdated(tx: VerificationTransaction) {
// Timber.v("Bob transactionUpdated: ${tx.state()}")
// latch.countDown()
// }
// }
// bobSession.cryptoService().verificationService().addListener(bobListener)
// aliceSession.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, transactionId)
//
// testHelper.await(latch)
// val aliceTx =
// aliceSession.cryptoService().verificationService().getExistingTransaction(bobSession.myUserId, transactionId) as SasVerificationTransaction
// val bobTx = bobSession.cryptoService().verificationService().getExistingTransaction(aliceSession.myUserId, transactionId) as SasVerificationTransaction
//
// assertEquals("Should have same SAS", aliceTx.getDecimalCodeRepresentation(), bobTx.getDecimalCodeRepresentation())
//
// val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
// val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
//
// assertEquals(
// "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
// bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
// )
// }
@Test
fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
val sasVerificationTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
val transactionId = sasVerificationTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, listOf(VerificationMethod.SAS))
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService()
val verifiedLatch = CountDownLatch(2)
val aliceListener = object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
Timber.v("RequestUpdated pr=$pr")
}
var matched = false
var verified = false
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx !is SasVerificationTransaction) return
Timber.v("Alice transactionUpdated: ${tx.state()} on thread:${Thread.currentThread()}")
when (tx.state()) {
SasTransactionState.SasShortCodeReady -> {
if (!matched) {
matched = true
runBlocking {
delay(500)
tx.userHasVerifiedShortCode()
}
}
}
is SasTransactionState.Done -> {
if (!verified) {
verified = true
verifiedLatch.countDown()
}
}
else -> Unit
}
}
}
// aliceVerificationService.addListener(aliceListener)
val bobListener = object : VerificationService.Listener {
var accepted = false
var matched = false
var verified = false
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
Timber.v("RequestUpdated: pr=$pr")
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx !is SasVerificationTransaction) return
Timber.v("Bob transactionUpdated: ${tx.state()} on thread: ${Thread.currentThread()}")
when (tx.state()) {
// VerificationTxState.SasStarted -> {
// if (!accepted) {
// accepted = true
// runBlocking {
// tx.acceptVerification()
// }
// }
// }
SasTransactionState.SasShortCodeReady -> {
if (!matched) {
matched = true
runBlocking {
delay(500)
tx.userHasVerifiedShortCode()
}
}
}
is SasTransactionState.Done -> {
if (!verified) {
verified = true
verifiedLatch.countDown()
}
}
else -> Unit
}
}
}
// bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = runBlocking {
bobSession.cryptoService().getMyCryptoDevice().deviceId
}
aliceVerificationService.startKeyVerification(VerificationMethod.SAS, bobUserId, transactionId)
Timber.v("Await after beginKey ${Thread.currentThread()}")
testHelper.await(verifiedLatch)
// Assert that devices are verified
val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyCryptoDevice().deviceId)
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
}
@Test
fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession.cryptoService().verificationService()
val req = aliceVerificationService.requestKeyVerificationInDMs(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
bobSession.myUserId,
cryptoTestData.roomId
)
val requestID = req.transactionId
Log.v("TEST", "== requestID is $requestID")
testHelper.retryPeriodically {
val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
Log.v("TEST", "== prBobPOV is $prBobPOV")
prBobPOV?.transactionId == requestID
}
bobVerificationService.readyPendingVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceSession.myUserId,
requestID
)
// wait for alice to get the ready
testHelper.retryPeriodically {
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
Log.v("TEST", "== prAlicePOV is $prAlicePOV")
prAlicePOV?.transactionId == requestID && prAlicePOV.state == EVerificationState.Ready
}
// Start concurrent!
aliceVerificationService.startKeyVerification(
method = VerificationMethod.SAS,
otherUserId = bobSession.myUserId,
requestId = requestID,
)
bobVerificationService.startKeyVerification(
method = VerificationMethod.SAS,
otherUserId = aliceSession.myUserId,
requestId = requestID,
)
// we should reach SHOW SAS on both
var alicePovTx: SasVerificationTransaction?
var bobPovTx: SasVerificationTransaction?
testHelper.retryPeriodically {
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID) as? SasVerificationTransaction
Log.v("TEST", "== alicePovTx is $alicePovTx")
alicePovTx?.state() == SasTransactionState.SasShortCodeReady
}
// wait for alice to get the ready
testHelper.retryPeriodically {
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID) as? SasVerificationTransaction
Log.v("TEST", "== bobPovTx is $bobPovTx")
bobPovTx?.state() == SasTransactionState.SasShortCodeReady
}
}
*/
}

View file

@ -1,251 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldNotBeNull
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class QrCodeTest : InstrumentedTest {
private val qrCode1 = QrCodeData.VerifyingAnotherUser(
transactionId = "MaTransaction",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value1 =
"MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted(
transactionId = "MaTransaction",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value2 =
"MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted(
transactionId = "MaTransaction",
deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
sharedSecret = "MTIzNDU2Nzg"
)
private val value3 =
"MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008B\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678"
private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1)
private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5")
private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55")
@Test
fun testEncoding1() {
qrCode1.toEncodedString() shouldBeEqualTo value1
}
@Test
fun testEncoding2() {
qrCode2.toEncodedString() shouldBeEqualTo value2
}
@Test
fun testEncoding3() {
qrCode3.toEncodedString() shouldBeEqualTo value3
}
@Test
fun testSymmetry1() {
qrCode1.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode1
}
@Test
fun testSymmetry2() {
qrCode2.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode2
}
@Test
fun testSymmetry3() {
qrCode3.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode3
}
@Test
fun testCase1() {
val url = qrCode1.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldBeEqualTo 0
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testCase2() {
val url = qrCode2.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldBeEqualTo 1
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testCase3() {
val url = qrCode3.toEncodedString()
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
checkHeader(byteArray)
// Mode
byteArray[7] shouldBeEqualTo 2
checkSizeAndTransaction(byteArray)
compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray)
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray)
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
}
@Test
fun testLongTransactionId() {
// Size on two bytes (2_000 = 0x07D0)
val longTransactionId = "PatternId_".repeat(200)
val qrCode = qrCode1.copy(transactionId = longTransactionId)
val result = qrCode.toEncodedString()
val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId")
result shouldBeEqualTo expected
// Reverse operation
expected.toQrCodeData() shouldBeEqualTo qrCode
}
@Test
fun testAnyTransactionId() {
for (qty in 0 until 0x1FFF step 200) {
val longTransactionId = "a".repeat(qty)
val qrCode = qrCode1.copy(transactionId = longTransactionId)
// Symmetric operation
qrCode.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode
}
}
// Error cases
@Test
fun testErrorHeader() {
value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull()
value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull()
value1.replace("MATRIX", "").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorVersion() {
value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull()
value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorSecretTooShort() {
value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull()
}
@Test
fun testErrorNoTransactionNoKeyNoSecret() {
// But keep transaction length
"MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull()
}
@Test
fun testErrorNoKeyNoSecret() {
"MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull()
}
@Test
fun testErrorTransactionLengthTooShort() {
// In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch
value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull()
}
@Test
fun testErrorTransactionLengthTooBig() {
value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull()
}
private fun compareArray(actual: ByteArray, expected: ByteArray) {
actual.size shouldBeEqualTo expected.size
for (i in actual.indices) {
actual[i] shouldBeEqualTo expected[i]
}
}
private fun checkHeader(byteArray: ByteArray) {
// MATRIX
byteArray[0] shouldBeEqualTo 'M'.code.toByte()
byteArray[1] shouldBeEqualTo 'A'.code.toByte()
byteArray[2] shouldBeEqualTo 'T'.code.toByte()
byteArray[3] shouldBeEqualTo 'R'.code.toByte()
byteArray[4] shouldBeEqualTo 'I'.code.toByte()
byteArray[5] shouldBeEqualTo 'X'.code.toByte()
// Version
byteArray[6] shouldBeEqualTo 2
}
private fun checkSizeAndTransaction(byteArray: ByteArray) {
// Size
byteArray[8] shouldBeEqualTo 0
byteArray[9] shouldBeEqualTo 13
// Transaction
byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldBeEqualTo "MaTransaction"
}
}

View file

@ -1,69 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import io.realm.Realm
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
import org.matrix.android.sdk.internal.util.time.Clock
class CryptoSanityMigrationTest {
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
lateinit var context: Context
var realm: Realm? = null
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().context
}
@After
fun tearDown() {
realm?.close()
}
@Test
fun cryptoDatabaseShouldMigrateGracefully() {
val realmName = "crypto_store_20.realm"
val migration = RealmCryptoStoreMigration(
object : Clock {
override fun epochMillis(): Long {
return 0L
}
}
)
val realmConfiguration = configurationFactory.createConfiguration(
realmName,
"7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca",
RealmCryptoStoreModule(),
migration.schemaVersion,
migration
)
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
realm = Realm.getInstance(realmConfiguration)
}
}

View file

@ -1,61 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
import org.matrix.olm.OlmPkMessage
class BackupRecoveryKey(private val key: ByteArray) : IBackupRecoveryKey {
override fun equals(other: Any?): Boolean {
if (other !is BackupRecoveryKey) return false
return this.toBase58() == other.toBase58()
}
override fun hashCode(): Int {
return key.contentHashCode()
}
override fun toBase58() = computeRecoveryKey(key)
override fun toBase64() = key.toBase64NoPadding()
override fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String): String = withOlmDecryption {
it.setPrivateKey(key)
it.decrypt(OlmPkMessage().apply {
this.mEphemeralKey = ephemeralKey
this.mCipherText = ciphertext
this.mMac = mac
})
}
override fun megolmV1PublicKey() = v1pk
private val v1pk = object : IMegolmV1PublicKey {
override val publicKey: String
get() = withOlmDecryption {
it.setPrivateKey(key)
}
override val privateKeySalt: String?
get() = null // not use in kotlin sdk
override val privateKeyIterations: Int?
get() = null // not use in kotlin sdk
override val backupAlgorithm: String
get() = "" // not use in kotlin sdk
}
}

View file

@ -1,32 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
object BackupUtils {
fun recoveryKeyFromBase58(base58: String): IBackupRecoveryKey? {
return extractCurveKeyFromRecoveryKey(base58)?.let {
BackupRecoveryKey(it)
}
}
fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? {
return BackupRecoveryKey(generatePrivateKeyWithPassword(passphrase, null).privateKey)
}
}

View file

@ -1,64 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
@JsonClass(generateAdapter = true)
internal data class MessageVerificationAcceptContent(
@Json(name = "hash") override val hash: String?,
@Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?,
@Json(name = "message_authentication_code") override val messageAuthenticationCode: String?,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
@Json(name = "commitment") override var commitment: String? = null
) : VerificationInfoAccept {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
companion object : VerificationInfoAcceptFactory {
override fun create(
tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>
): VerificationInfoAccept {
return MessageVerificationAcceptContent(
hash,
keyAgreementProtocol,
messageAuthenticationCode,
shortAuthenticationStrings,
RelationDefaultContent(
RelationType.REFERENCE,
tid
),
commitment
)
}
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
@JsonClass(generateAdapter = true)
data class MessageVerificationCancelContent(
@Json(name = "code") override val code: String? = null,
@Json(name = "reason") override val reason: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoCancel {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
companion object {
fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent {
return MessageVerificationCancelContent(
reason.value,
reason.humanReadable,
RelationDefaultContent(
RelationType.REFERENCE,
transactionId
)
)
}
}
}

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.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo
@JsonClass(generateAdapter = true)
internal data class MessageVerificationDoneContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfo<ValidVerificationDone> {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent(): Content? = toContent()
override fun asValidObject(): ValidVerificationDone? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationDone(
validTransactionId
)
}
}
internal data class ValidVerificationDone(
val transactionId: String
)

View file

@ -1,52 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
@JsonClass(generateAdapter = true)
internal data class MessageVerificationKeyContent(
/**
* The devices ephemeral public key, as an unpadded base64 string.
*/
@Json(name = "key") override val key: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoKey {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
companion object : VerificationInfoKeyFactory {
override fun create(tid: String, pubKey: String): VerificationInfoKey {
return MessageVerificationKeyContent(
pubKey,
RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
@JsonClass(generateAdapter = true)
internal data class MessageVerificationMacContent(
@Json(name = "mac") override val mac: Map<String, String>? = null,
@Json(name = "keys") override val keys: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoMac {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
companion object : VerificationInfoMacFactory {
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
return MessageVerificationMacContent(
mac,
keys,
RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
@JsonClass(generateAdapter = true)
internal data class MessageVerificationReadyContent(
@Json(name = "from_device") override val fromDevice: String? = null,
@Json(name = "methods") override val methods: List<String>? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoReady {
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
companion object : MessageVerificationReadyFactory {
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
return MessageVerificationReadyContent(
fromDevice = fromDevice,
methods = methods,
relatesTo = RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

View file

@ -1,42 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
@JsonClass(generateAdapter = true)
data class MessageVerificationRequestContent(
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
@Json(name = "body") override val body: String,
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>,
@Json(name = "to") val toUserId: String,
@Json(name = "timestamp") override val timestamp: Long?,
@Json(name = "format") val format: String? = null,
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
// Not parsed, but set after, using the eventId
override val transactionId: String? = null
) : MessageContent, VerificationInfoRequest {
override fun toEventContent() = toContent()
}

View file

@ -1,45 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@JsonClass(generateAdapter = true)
internal data class MessageVerificationStartContent(
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "hashes") override val hashes: List<String>?,
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>?,
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
@Json(name = "method") override val method: String?,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
@Json(name = "secret") override val sharedSecret: String?
) : VerificationInfoStart {
override fun toCanonicalJson(): String {
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
}
override val transactionId: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent()
}

View file

@ -1,266 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import dagger.Binds
import dagger.Module
import dagger.Provides
import io.realm.RealmConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers
import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.database.RealmKeysUtils
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.di.UserMd5
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.cache.ClearCacheTask
import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask
import retrofit2.Retrofit
import java.io.File
@Module
internal abstract class CryptoModule {
@Module
companion object {
internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
@JvmStatic
@Provides
@CryptoDatabase
@SessionScope
fun providesRealmConfiguration(
@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils,
realmCryptoStoreMigration: RealmCryptoStoreMigration
): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
.allowWritesOnUiThread(true)
.schemaVersion(realmCryptoStoreMigration.schemaVersion)
.migration(realmCryptoStoreMigration)
.build()
}
@JvmStatic
@Provides
@SessionScope
fun providesCryptoCoroutineScope(coroutineDispatchers: MatrixCoroutineDispatchers): CoroutineScope {
return CoroutineScope(SupervisorJob() + coroutineDispatchers.crypto)
}
@JvmStatic
@Provides
@CryptoDatabase
fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask {
return RealmClearCacheTask(realmConfiguration)
}
@JvmStatic
@Provides
@SessionScope
fun providesCryptoAPI(retrofit: Retrofit): CryptoApi {
return retrofit.create(CryptoApi::class.java)
}
@JvmStatic
@Provides
@SessionScope
fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi {
return retrofit.create(RoomKeysApi::class.java)
}
}
@Binds
abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService
@Binds
abstract fun bindKeysBackupService(service: DefaultKeysBackupService): KeysBackupService
@Binds
abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask
@Binds
abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds
abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask
@Binds
abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask
@Binds
abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask
@Binds
abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask
@Binds
abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
@Binds
abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask
@Binds
abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask
@Binds
abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask
@Binds
abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
@Binds
abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
@Binds
abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask
@Binds
abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask
@Binds
abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask
@Binds
abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
@Binds
abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask
@Binds
abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask
@Binds
abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
@Binds
abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask
@Binds
abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask
@Binds
abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask
@Binds
abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask
@Binds
abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask
@Binds
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask
@Binds
abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService
@Binds
abstract fun bindVerificationService(service: DefaultVerificationService): VerificationService
@Binds
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
@Binds
abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore
@Binds
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
@Binds
abstract fun bindInitalizeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
}

View file

@ -1,145 +0,0 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import javax.inject.Inject
internal class DecryptRoomEventUseCase @Inject constructor(
private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
) {
suspend operator fun invoke(event: Event, requestKeysOnFail: Boolean = true): MXEventDecryptionResult {
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
if (encryptedEventContent.senderKey.isNullOrBlank() ||
encryptedEventContent.sessionId.isNullOrBlank() ||
encryptedEventContent.ciphertext.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
try {
val olmDecryptionResult = olmDevice.decryptGroupMessage(
encryptedEventContent.ciphertext,
event.roomId,
"",
eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)
if (olmDecryptionResult.payload != null) {
return MXEventDecryptionResult(
clearEvent = olmDecryptionResult.payload,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty(),
messageVerificationState = olmDecryptionResult.verificationState
)
} else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
} catch (throwable: Throwable) {
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// So we know that session, but it's ratcheted and we can't decrypt at that index
// Check if partially withheld
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX",
null
)
}
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.OLM,
reason,
detailedReason
)
}
if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Check if it was withheld by sender to enrich error code
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
}
throw throwable
}
}
private fun requestKeysForEvent(event: Event) {
outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
suspend fun decryptAndSaveResult(event: Event) {
tryOrNull(message = "Unable to decrypt the event") {
invoke(event)
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
verificationState = result.messageVerificationState
)
}
}
}

View file

@ -1,603 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.extensions.measureMetric
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
// Legacy name: MXDeviceList
@SessionScope
internal class DeviceListManager @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice,
private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock,
matrixConfiguration: MatrixConfiguration
) {
private val metricPlugins = matrixConfiguration.metricPlugins
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(userIds: List<String>)
}
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
fun addListener(listener: UserDevicesUpdateListener) {
synchronized(deviceChangeListeners) {
deviceChangeListeners.add(listener)
}
}
fun removeListener(listener: UserDevicesUpdateListener) {
synchronized(deviceChangeListeners) {
deviceChangeListeners.remove(listener)
}
}
private fun dispatchDeviceChange(users: List<String>) {
synchronized(deviceChangeListeners) {
deviceChangeListeners.forEach {
try {
it.onUsersDeviceUpdate(users)
} catch (failure: Throwable) {
Timber.e(failure, "Failed to dispatch device change")
}
}
}
}
// HS not ready for retry
private val notReadyToRetryHS = mutableSetOf<String>()
private val cryptoCoroutineContext = coroutineDispatchers.crypto
// Reset in progress status in case of restart
suspend fun recover() {
withContext(cryptoCoroutineContext) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
}
/**
* Tells if the key downloads should be tried.
*
* @param userId the userId
* @return true if the keys download can be retrieved
*/
private fun canRetryKeysDownload(userId: String): Boolean {
var res = false
if (':' in userId) {
try {
synchronized(notReadyToRetryHS) {
res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
}
} catch (e: Exception) {
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
}
}
return res
}
/**
* Clear the unavailable server lists.
*/
private fun clearUnavailableServersList() {
synchronized(notReadyToRetryHS) {
notReadyToRetryHS.clear()
}
}
fun onRoomMembersLoadedFor(roomId: String) {
cryptoCoroutineScope.launch(cryptoCoroutineContext) {
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
// It's OK to track also device for invited users
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
startTrackingDeviceList(userIds)
refreshOutdatedDeviceLists()
}
}
}
/**
* Mark the cached device list for the given user outdated
* flag the given user for device-list tracking, if they are not already.
*
* @param userIds the user ids list
*/
fun startTrackingDeviceList(userIds: List<String>) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
/**
* Update the devices list statuses.
*
* @param changed the user ids list which have new devices
* @param left the user ids list which left a room
*/
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
if (changed.isNotEmpty() || left.isNotEmpty()) {
clearUnavailableServersList()
}
for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
/**
* This will flag each user whose devices we are tracking as in need of an update.
*/
fun invalidateAllDeviceLists() {
handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
}
/**
* The keys download failed.
*
* @param userIds the user ids list
*/
private fun onKeysDownloadFailed(userIds: List<String>) {
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
/**
* The keys download succeeded.
*
* @param userIds the userIds list
* @param failures the failure map.
*/
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> {
if (failures != null) {
for ((k, value) in failures) {
val statusCode = when (val status = value["status"]) {
is Double -> status.toInt()
is Int -> status.toInt()
else -> 0
}
if (statusCode == 503) {
synchronized(notReadyToRetryHS) {
notReadyToRetryHS.add(k)
}
}
}
}
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>()
for (userId in userIds) {
val devices = cryptoStore.getUserDevices(userId)
if (null == devices) {
if (canRetryKeysDownload(userId)) {
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
Timber.e("failed to retry the devices of $userId : retry later")
} else {
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER
Timber.e("failed to retry the devices of $userId : the HS is not available")
}
}
} else {
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE
Timber.v("Device list for $userId now up to date")
}
// And the response result
usersDevicesInfoMap.setObjects(userId, devices)
}
}
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
dispatchDeviceChange(userIds)
return usersDevicesInfoMap
}
/**
* Download the device keys for a list of users and stores the keys in the MXStore.
* It must be called in getEncryptingThreadHandler() thread.
*
* @param userIds The users to fetch.
* @param forceDownload Always download the keys even if cached.
*/
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
// Map from userId -> deviceId -> MXDeviceInfo
val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
// List of user ids we need to download keys for
val downloadUsers = ArrayList<String>()
if (null != userIds) {
if (forceDownload) {
downloadUsers.addAll(userIds)
} else {
for (userId in userIds) {
val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED)
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
// not yet retrieved
if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) {
downloadUsers.add(userId)
} else {
val devices = cryptoStore.getUserDevices(userId)
// should always be true
if (devices != null) {
stored.setObjects(userId, devices)
} else {
downloadUsers.add(userId)
}
}
}
}
}
return if (downloadUsers.isEmpty()) {
Timber.v("## CRYPTO | downloadKeys() : no new user device")
stored
} else {
Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = clock.epochMillis()
try {
val result = doKeyDownloadForUsers(downloadUsers)
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
result.also {
it.addEntriesFromMap(stored)
}
} catch (failure: Throwable) {
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
if (forceDownload) {
throw failure
} else {
stored
}
}
}
}
/**
* Download the devices keys for a set of users.
*
* @param downloadUsers the user ids list
*/
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
// get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) {
// trigger nothing
return MXUsersDevicesMap()
}
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
val response: KeysQueryResponse
relevantPlugins.measureMetric {
response = try {
downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
if (throwable is CancellationException) {
// the crypto module is getting closed, so we cannot access the DB anymore
Timber.w("The crypto module is closed, ignoring this error")
} else {
onKeysDownloadFailed(filteredUsers)
}
throw throwable
}
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
}
val userDataToStore = UserDataToStore()
for (userId in filteredUsers) {
// al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap()
for ((deviceId, deviceInfo) in models) {
// Get the potential previously store device keys for this device
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
// in some race conditions (like unit tests)
// the self device must be seen as verified
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
}
// Validate received keys
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
// New device keys are not valid. Do not store them
workingCopy.remove(deviceId)
if (null != previouslyStoredDeviceKeys) {
// But keep old validated ones if any
workingCopy[deviceId] = previouslyStoredDeviceKeys
}
} else if (null != previouslyStoredDeviceKeys) {
// The verified status is not sync'ed with hs.
// This is a client side information, valid only for this client.
// So, transfer its previous value
workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
}
}
// Update the store
// Note that devices which aren't in the response will be removed from the stores
userDataToStore.userDevices[userId] = workingCopy
}
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
}
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
}
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
}
userDataToStore.userIdentities[userId] = UserIdentity(
masterKey = masterKey,
selfSigningKey = selfSigningKey,
userSigningKey = userSigningKey
)
}
cryptoStore.storeData(userDataToStore)
// Update devices trust for these users
// dispatchDeviceChange(downloadUsers)
return onKeysDownloadSucceed(filteredUsers, response.failures)
}
/**
* Validate device keys.
* This method must called on getEncryptingThreadHandler() thread.
*
* @param deviceKeys the device keys to validate.
* @param userId the id of the user of the device.
* @param deviceId the id of the device.
* @param previouslyStoredDeviceKeys the device keys we received before for this device
* @return true if succeeds
*/
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
if (null == deviceKeys) {
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
return false
}
if (null == deviceKeys.keys) {
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
return false
}
if (null == deviceKeys.signatures) {
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
return false
}
// Check that the user_id and device_id in the received deviceKeys are correct
if (deviceKeys.userId != userId) {
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
return false
}
if (deviceKeys.deviceId != deviceId) {
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
return false
}
val signKeyId = "ed25519:" + deviceKeys.deviceId
val signKey = deviceKeys.keys[signKeyId]
if (null == signKey) {
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
return false
}
val signatureMap = deviceKeys.signatures[userId]
if (null == signatureMap) {
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
return false
}
val signature = signatureMap[signKeyId]
if (null == signature) {
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
return false
}
var isVerified = false
var errorMessage: String? = null
try {
olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature)
isVerified = true
} catch (e: Exception) {
errorMessage = e.message
}
if (!isVerified) {
Timber.e(
"## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" +
deviceKeys.deviceId + " with error " + errorMessage
)
return false
}
if (null != previouslyStoredDeviceKeys) {
if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
// This should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
Timber.e(
"## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" +
deviceKeys.deviceId + " has changed : " +
previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey
)
Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
return false
}
}
return true
}
/**
* Start device queries for any users who sent us an m.new_device recently
* This method must be called on getEncryptingThreadHandler() thread.
*/
suspend fun refreshOutdatedDeviceLists() {
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
}
if (users.isEmpty()) {
return
}
// update the statuses
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
runCatching {
doKeyDownloadForUsers(users)
}.fold(
{
Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
},
{
Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
}
)
}
companion object {
/**
* State transition diagram for DeviceList.deviceTrackingStatus.
* <pre>
*
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
*
* </pre>
*/
const val TRACKING_STATUS_NOT_TRACKED = -1
const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
const val TRACKING_STATUS_UP_TO_DATE = 3
const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
}
}

View file

@ -1,283 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope
internal class EventDecryptor @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val messageEncrypter: MessageEncrypter,
private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
) {
/**
* Rate limit unwedge attempt, should we persist that?
*/
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
data class WedgedDeviceInfo(
val userId: String,
val senderKey: String?
)
private val wedgedMutex = Mutex()
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
/**
* Decrypt an event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or throw in case of error
*/
@Throws(MXCryptoError::class)
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
}
/**
* Decrypt an event and save the result in the given event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
*/
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
// event is not encrypted or already decrypted
if (event.getClearType() != EventType.ENCRYPTED) return
tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") {
decryptEvent(event, timeline)
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
verificationState = result.messageVerificationState
)
}
}
/**
* Decrypt an event asynchronously.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param callback the callback to return data or null
*/
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
// is it needed to do that on the crypto scope??
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
internalDecryptEvent(event, timeline)
}.foldToCallback(callback)
}
}
/**
* Decrypt an event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error
*/
@Throws(MXCryptoError::class)
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content
if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else if (event.isRedacted()) {
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
return MXEventDecryptionResult(
clearEvent = mapOf(
"room_id" to event.roomId.orEmpty(),
"type" to EventType.MESSAGE,
"content" to emptyMap<String, Any>(),
"unsigned" to event.unsignedData.toContent()
)
)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else {
try {
return alg.decryptEvent(event, timeline)
} catch (mxCryptoError: MXCryptoError) {
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base &&
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device
val olmContent = event.content.toModel<OlmEventContent>()
if (event.senderId != null && olmContent?.senderKey != null) {
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
} else {
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
}
}
}
throw mxCryptoError
}
}
}
}
private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
wedgedMutex.withLock {
val info = WedgedDeviceInfo(senderId, senderKey)
if (!wedgedDevices.contains(info)) {
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
wedgedDevices.add(info)
}
}
}
// coroutineDispatchers.crypto scope
suspend fun unwedgeDevicesIfNeeded() {
// handle wedged devices
// Some olm decryption have failed and some device are wedged
// we should force start a new session for those
Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
// get the one that should be retried according to rate limit
val now = clock.epochMillis()
val toUnwedge = wedgedMutex.withLock {
wedgedDevices.filter {
val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
return@filter false
}
// let's already mark that we tried now
lastNewSessionForcedDates[it] = now
true
}
}
if (toUnwedge.isEmpty()) {
Timber.tag(loggerTag.value).v("Nothing to unwedge")
return
}
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
toUnwedge
.chunked(100) // safer to chunk if we ever have lots of wedged devices
.forEach { wedgedList ->
val groupedByUserId = wedgedList.groupBy { it.userId }
// lets download keys if needed
withContext(coroutineDispatchers.io) {
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
}
// find the matching devices
groupedByUserId
.map { groupedByUser ->
val userId = groupedByUser.key
val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
knownDevices.firstOrNull { it.identityKey() == senderKey }
}
}
.toMap()
.let { deviceList ->
try {
// force creating new outbound session and mark them as most recent to
// be used for next encryption (dummy)
val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
// Now send a dummy message on that session so the other side knows about it.
val payloadJson = mapOf(
"type" to EventType.DUMMY
)
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sessionToUse.map.values
.flatMap { it.values }
.map { it.deviceInfo }
.forEach { deviceInfo ->
Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
}
// now let's send that
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
}
deviceList.values.flatten().forEach { deviceInfo ->
wedgedMutex.withLock {
wedgedDevices.removeAll {
it.senderKey == deviceInfo.identityKey() &&
it.userId == deviceInfo.userId
}
}
}
} catch (failure: Throwable) {
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
}
}
}
}
}
}

View file

@ -1,126 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.util.LruCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import javax.inject.Inject
internal data class InboundGroupSessionHolder(
val wrapper: MXInboundMegolmSessionWrapper,
val mutex: Mutex = Mutex()
)
private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
/**
* Allows to cache and batch store operations on inbound group session store.
* Because it is used in the decrypt flow, that can be called quite rapidly
*/
internal class InboundGroupSessionStore @Inject constructor(
private val store: IMXCryptoStore,
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) {
private data class CacheKey(
val sessionId: String,
val senderKey: String
)
private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
if (oldValue != null) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
// store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
oldValue.wrapper.session.releaseSession()
}
}
}
}
@Synchronized
fun clear() {
sessionCache.evictAll()
}
@Synchronized
fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
val known = sessionCache[CacheKey(sessionId, senderKey)]
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}")
return known
?: store.getInboundGroupSession(sessionId, senderKey)?.also {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
}?.let {
InboundGroupSessionHolder(it)
}
}
@Synchronized
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
store.removeInboundGroupSession(sessionId, senderKey)
sessionCache.remove(CacheKey(sessionId, senderKey))
// release removed session
old.wrapper.session.releaseSession()
internalStoreGroupSession(new, sessionId, senderKey)
}
@Synchronized
fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
store.storeInboundGroupSessions(
listOf(
old.wrapper.copy(
sessionData = old.wrapper.sessionData.copy(trusted = true)
)
)
)
// will release it :/
sessionCache.remove(CacheKey(sessionId, senderKey))
}
@Synchronized
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
internalStoreGroupSession(holder, sessionId, senderKey)
}
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert
// If it's already known, no need to update cache it's already there
sessionCache.put(CacheKey(sessionId, senderKey), holder)
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
store.storeInboundGroupSessions(listOf(holder.wrapper))
}
}
}

View file

@ -1,465 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.system.measureTimeMillis
private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO)
@SessionScope
internal class IncomingKeyRequestManager @Inject constructor(
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val olmDevice: MXOlmDevice,
private val cryptoConfig: MXCryptoConfig,
private val messageEncrypter: MessageEncrypter,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sendToDeviceTask: SendToDeviceTask,
private val clock: Clock,
) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
val sequencer = SemaphoreCoroutineSequencer()
private val incomingRequestBuffer = mutableListOf<ValidMegolmRequestBody>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
enum class MegolmRequestAction {
Request, Cancel
}
data class ValidMegolmRequestBody(
val requestId: String,
val requestingUserId: String,
val requestingDeviceId: String,
val roomId: String,
val senderKey: String,
val sessionId: String,
val action: MegolmRequestAction
) {
fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId"
}
private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? {
val deviceId = requestingDeviceId ?: return null
val body = body ?: return null
val roomId = body.roomId ?: return null
val sessionId = body.sessionId ?: return null
val senderKey = body.senderKey ?: return null
val requestId = this.requestId ?: return null
if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
val action = when (this.action) {
"request" -> MegolmRequestAction.Request
"request_cancellation" -> MegolmRequestAction.Cancel
else -> null
} ?: return null
return ValidMegolmRequestBody(
requestId = requestId,
requestingUserId = senderId,
requestingDeviceId = deviceId,
roomId = roomId,
senderKey = senderKey,
sessionId = sessionId,
action = action
)
}
fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) {
if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value)
.i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}")
return
}
outgoingRequestScope.launch {
// It is important to handle requests in order
sequencer.post {
val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also {
Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}")
}
// is there already one like that?
val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest }
if (existing == null) {
when (validMegolmRequest.action) {
MegolmRequestAction.Request -> {
// just add to the buffer
incomingRequestBuffer.add(validMegolmRequest)
}
MegolmRequestAction.Cancel -> {
// ignore, we can't cancel as it's not known (probably already processed)
// still notify app layer if it was passed up previously
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
listenersCopy.onEach {
tryOrNull {
withContext(coroutineDispatchers.main) {
it.onRequestCancelled(iReq)
}
}
}
}
}
}
}
} else {
when (validMegolmRequest.action) {
MegolmRequestAction.Request -> {
// it's already in buffer, nop keep existing
}
MegolmRequestAction.Cancel -> {
// discard the request in buffer
incomingRequestBuffer.remove(existing)
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
listenersCopy.onEach {
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
withContext(coroutineDispatchers.main) {
tryOrNull { it.onRequestCancelled(iReq) }
}
}
}
}
}
}
}
}
}
}
fun processIncomingRequests() {
outgoingRequestScope.launch {
sequencer.post {
measureTimeMillis {
Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process")
incomingRequestBuffer.forEach {
// should not happen, we only store requests
if (it.action != MegolmRequestAction.Request) return@forEach
try {
handleIncomingRequest(it)
} catch (failure: Throwable) {
// ignore and continue, should not happen
Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it")
}
}
incomingRequestBuffer.clear()
}.let { duration ->
Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms")
}
}
}
}
private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) {
// We don't want to download keys, if we don't know the device yet we won't share any how?
val requestingDevice =
cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId)
?: return Unit.also {
Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}")
}
cryptoStore.saveIncomingKeyRequestAuditTrail(
request.requestId,
request.roomId,
request.sessionId,
request.senderKey,
MXCRYPTO_ALGORITHM_MEGOLM,
request.requestingUserId,
request.requestingDeviceId
)
val roomAlgorithm = // withContext(coroutineDispatchers.crypto) {
cryptoStore.getRoomAlgorithm(request.roomId)
// }
if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
// strange we received a request for a room that is not encrypted
// maybe a broken state?
Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}")
return
}
// Is it for one of our sessions?
if (request.requestingUserId == credentials.userId) {
Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}")
if (request.requestingDeviceId == credentials.deviceId) {
// ignore it's a remote echo
return
}
// If it's verified we share from the early index we know
// if not we check if it was originaly shared or not
if (requestingDevice.isVerified) {
// we share from the earliest known chain index
shareMegolmKey(request, requestingDevice, null)
} else {
shareIfItWasPreviouslyShared(request, requestingDevice)
}
} else {
if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}")
return
}
Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}")
if (requestingDevice.isBlocked) {
// it's blocked, so send a withheld code
sendWithheldForRequest(request, WithHeldCode.BLACKLISTED)
} else {
shareIfItWasPreviouslyShared(request, requestingDevice)
}
}
}
private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) {
// we don't reshare unless it was previously shared with
val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) {
cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice)
}
if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) {
// we share from the index it was previously shared with
shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong())
} else {
val isOwnDevice = requestingDevice.userId == credentials.userId
sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED)
// if it's our device we could delegate to the app layer to decide
if (isOwnDevice) {
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
val iReq = IncomingRoomKeyRequest(
userId = requestingDevice.userId,
deviceId = requestingDevice.deviceId,
requestId = request.requestId,
requestBody = RoomKeyRequestBody(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
senderKey = request.senderKey,
sessionId = request.sessionId,
roomId = request.roomId
),
localCreationTimestamp = clock.epochMillis()
)
listenersCopy.onEach {
withContext(coroutineDispatchers.main) {
tryOrNull { it.onRoomKeyRequest(iReq) }
}
}
}
}
}
}
private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) {
Timber.tag(loggerTag.value)
.w("Send withheld $code for req: ${request.shortDbgString()}")
val withHeldContent = RoomKeyWithHeldContent(
roomId = request.roomId,
senderKey = request.senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sessionId = request.sessionId,
codeString = code.value,
fromDevice = credentials.deviceId
)
val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap<Any>().apply {
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
}
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(params)
Timber.tag(loggerTag.value)
.d("Send withheld $code req: ${request.shortDbgString()}")
}
cryptoStore.saveWithheldAuditTrail(
roomId = request.roomId,
sessionId = request.sessionId,
senderKey = request.senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
code = code,
userId = request.requestingUserId,
deviceId = request.requestingDeviceId
)
} catch (failure: Throwable) {
// Ignore it's not that important?
// do we want to fallback to a worker?
Timber.tag(loggerTag.value)
.w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}")
}
}
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
request.requestId ?: return
request.deviceId ?: return
request.userId ?: return
request.requestBody?.roomId ?: return
request.requestBody.senderKey ?: return
request.requestBody.sessionId ?: return
val validReq = ValidMegolmRequestBody(
requestId = request.requestId,
requestingDeviceId = request.deviceId,
requestingUserId = request.userId,
roomId = request.requestBody.roomId,
senderKey = request.requestBody.senderKey,
sessionId = request.requestBody.sessionId,
action = MegolmRequestAction.Request
)
val requestingDevice =
cryptoStore.getUserDevice(request.userId, request.deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}")
}
shareMegolmKey(validReq, requestingDevice, null)
}
private suspend fun shareMegolmKey(
validRequest: ValidMegolmRequestBody,
requestingDevice: CryptoDeviceInfo,
chainIndex: Long?
): Boolean {
Timber.tag(loggerTag.value)
.d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}")
val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to establish olm session")
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
return false
}
val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("reshareKey: no session with this device, probably because there were no one-time keys")
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
return false
}
val sessionHolder = try {
olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}")
// It's unavailable
sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE)
return false
}
val export = sessionHolder.mutex.withLock {
sessionHolder.wrapper.exportKeys(chainIndex)
} ?: return false.also {
Timber.tag(loggerTag.value)
.e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}")
}
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload)
Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
return try {
sendToDeviceTask.execute(sendToDeviceParams)
Timber.tag(loggerTag.value)
.i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
cryptoStore.saveForwardKeyAuditTrail(
validRequest.roomId,
validRequest.sessionId,
validRequest.senderKey,
MXCRYPTO_ALGORITHM_MEGOLM,
requestingDevice.userId,
requestingDevice.deviceId,
chainIndex
)
true
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
false
}
}
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
fun close() {
try {
outgoingRequestScope.cancel("User Terminate")
incomingRequestBuffer.clear()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
}
}
}

View file

@ -1,80 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class MyDeviceInfoHolder @Inject constructor(
// The credentials,
credentials: Credentials,
// the crypto store
cryptoStore: IMXCryptoStore,
// Olm device
olmDevice: MXOlmDevice
) {
// Our device keys
/**
* my device info.
*/
val myDevice: CryptoDeviceInfo
init {
val keys = HashMap<String, String>()
// TODO it's a bit strange, why not load from DB?
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
}
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
}
// myDevice.keys = keys
//
// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
// TODO hwo to really check cross signed status?
//
val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
myDevice = CryptoDeviceInfo(
credentials.deviceId,
credentials.userId,
keys = keys,
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
trustLevel = DeviceTrustLevel(crossSigned, true)
)
// Add our own deviceinfo to the store
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
myDevices[myDevice.deviceId] = myDevice
cryptoStore.storeUserDevices(credentials.userId, myDevices)
}
}

View file

@ -1,54 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.auth.data.Credentials
import javax.inject.Inject
internal class ObjectSigner @Inject constructor(
private val credentials: Credentials,
private val olmDevice: MXOlmDevice
) {
/**
* Sign Object.
*
* Example:
* <pre>
* {
* "[MY_USER_ID]": {
* "ed25519:[MY_DEVICE_ID]": "sign(str)"
* }
* }
* </pre>
*
* @param strToSign the String to sign and to include in the Map
* @return a Map (see example)
*/
fun signObject(strToSign: String): Map<String, Map<String, String>> {
val result = HashMap<String, Map<String, String>>()
val content = HashMap<String, String>()
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
?: "" // null reported by rageshake if happens during logout
result[credentials.userId] = content
return result
}
}

View file

@ -1,160 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.olm.OlmSession
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
/**
* Keep the used olm session in memory and load them from the data layer when needed.
* Access is synchronized for thread safety.
*/
internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
/**
* Map of device key to list of olm sessions (it is possible to have several active sessions with a device).
*/
private val olmSessions = HashMap<String, MutableList<OlmSessionWrapper>>()
/**
* Store a session between our own device and another device.
* This will be called after the session has been created but also every time it has been used
* in order to persist the correct state for next run
* @param olmSessionWrapper the end-to-end session.
* @param deviceKey the public key of the other device.
*/
@Synchronized
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
// This could be a newly created session or one that was just created
// Anyhow we should persist ratchet state for future app lifecycle
addNewSessionInCache(olmSessionWrapper, deviceKey)
store.storeSession(olmSessionWrapper, deviceKey)
}
/**
* Get all the Olm Sessions we are sharing with the given device.
*
* @param deviceKey the public key of the other device.
* @return A set of sessionId, or empty if device is not known
*/
@Synchronized
fun getDeviceSessionIds(deviceKey: String): List<String> {
// we need to get the persisted ids first
val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
.orEmpty()
.toMutableList()
// Do we have some in cache not yet persisted?
olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
if (!persistedKnownSessions.contains(cachedSessionId)) {
// as it's in cache put in on top
persistedKnownSessions.add(0, cachedSessionId)
}
}
}
return persistedKnownSessions
}
/**
* Retrieve an end-to-end session between our own device and another
* device.
*
* @param sessionId the session Id.
* @param deviceKey the public key of the other device.
* @return the session wrapper if found
*/
@Synchronized
fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
// get from cache or load and add to cache
return internalGetSession(sessionId, deviceKey)
}
/**
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist.
*
* @param deviceKey the public key of the other device.
* @return last used sessionId, or null if not found
*/
@Synchronized
fun getLastUsedSessionId(deviceKey: String): String? {
// We want to avoid to load in memory old session if possible
val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
// we should check if we have one in cache with a higher last message received?
olmSessions[deviceKey].orEmpty().forEach { inCache ->
if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
candidate = inCache
}
}
return candidate?.olmSession?.sessionIdentifier()
}
/**
* Release all sessions and clear cache.
*/
@Synchronized
fun clear() {
olmSessions.entries.onEach { entry ->
entry.value.onEach { it.olmSession.releaseSession() }
}
olmSessions.clear()
}
private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
return getSessionInCache(sessionId, deviceKey)
?: // deserialize from store
return store.getDeviceSession(sessionId, deviceKey)?.also {
addNewSessionInCache(it, deviceKey)
}
}
private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
return olmSessions[deviceKey]?.firstOrNull {
getSafeSessionIdentifier(it.olmSession) == sessionId
}
}
private fun getSafeSessionIdentifier(session: OlmSession): String? {
return try {
session.sessionIdentifier()
} catch (throwable: Throwable) {
Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
null
}
}
private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
it.add(session)
// remove and release if was there but with different instance
if (existing != null && existing.olmSession != session.olmSession) {
// mm not sure when this could happen
// anyhow we should remove and release the one known
it.remove(existing)
existing.olmSession.releaseSession()
}
}
}
}

View file

@ -1,250 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.content.Context
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.model.MXKey
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.olm.OlmAccount
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.floor
import kotlin.math.min
// The spec recommend a 5mn delay, but due to federation
// or server downtime we give it a bit more time (1 hour)
private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
@SessionScope
internal class OneTimeKeysUploader @Inject constructor(
private val olmDevice: MXOlmDevice,
private val objectSigner: ObjectSigner,
private val uploadKeysTask: UploadKeysTask,
private val clock: Clock,
context: Context
) {
// tell if there is a OTK check in progress
private var oneTimeKeyCheckInProgress = false
// last OTK check timestamp
private var lastOneTimeKeyCheck: Long = 0
private var oneTimeKeyCount: Int? = null
// Simple storage to remember when was uploaded the last fallback key
private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE)
/**
* Stores the current one_time_key count which will be handled later (in a call of
* _onSyncCompleted). The count is e.g. coming from a /sync response.
*
* @param currentCount the new count
*/
fun updateOneTimeKeyCount(currentCount: Int) {
oneTimeKeyCount = currentCount
}
fun needsNewFallback() {
if (olmDevice.generateFallbackKeyIfNeeded()) {
// As we generated a new one, it's already forgetting one
// so we can clear the last publish time
// (in case the network calls fails after to avoid calling forgetKey)
saveLastFallbackKeyPublishTime(0L)
}
}
/**
* Check if the OTK must be uploaded.
*/
suspend fun maybeUploadOneTimeKeys() {
if (oneTimeKeyCheckInProgress) {
Timber.v("maybeUploadOneTimeKeys: already in progress")
return
}
if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
// we've done a key upload recently.
Timber.v("maybeUploadOneTimeKeys: executed too recently")
return
}
oneTimeKeyCheckInProgress = true
val oneTimeKeyCountFromSync = oneTimeKeyCount
?: fetchOtkCount() // we don't have count from sync so get from server
?: return Unit.also {
oneTimeKeyCheckInProgress = false
Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server")
}
Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}")
lastOneTimeKeyCheck = clock.epochMillis()
// We then check how many keys we can store in the Account object.
val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys()
// Try to keep at most half that number on the server. This leaves the
// rest of the slots free to hold keys that have been claimed from the
// server but we haven't received a message for.
// If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
// To complicate things further then can be a delay between a device
// claiming a public one time key from the server and it sending us a
// message. We need to keep the corresponding private key locally until
// we receive the message.
// But that message might never arrive leaving us stuck with duff
// private keys clogging up our local storage.
// So we need some kind of engineering compromise to balance all of
// these factors.
tryOrNull("Unable to upload OTK") {
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
}
oneTimeKeyCheckInProgress = false
// Check if we need to forget a fallback key
val latestPublishedTime = getLastFallbackKeyPublishTime()
if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) {
// This should be called once you are reasonably certain that you will not receive any more messages
// that use the old fallback key
Timber.d("## forgetFallbackKey()")
olmDevice.forgetFallbackKey()
}
}
private suspend fun fetchOtkCount(): Int? {
return tryOrNull("Unable to get OTK count") {
val result = uploadKeysTask.execute(UploadKeysTask.Params(KeysUploadBody()))
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
}
}
/**
* Upload some the OTKs.
*
* @param keyCount the key count
* @param keyLimit the limit
* @return the number of uploaded keys
*/
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) {
// If we don't need to generate any more keys then we are done.
return 0
}
var keysThisLoop = 0
if (keyLimit > keyCount) {
// Creating keys can be an expensive operation so we limit the
// number we generate in one go to avoid blocking the application
// for too long.
keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
olmDevice.generateOneTimeKeys(keysThisLoop)
}
// We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed
val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey()
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
olmDevice.markKeysAsPublished()
if (hadUnpublishedFallbackKey) {
// It had an unpublished fallback key that was published just now
saveLastFallbackKeyPublishTime(clock.epochMillis())
}
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
// Maybe upload other keys
return keysThisLoop +
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) +
(if (hadUnpublishedFallbackKey) 1 else 0)
} else {
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
}
}
private fun saveLastFallbackKeyPublishTime(timeMillis: Long) {
storage.edit().putLong("last_fb_key_publish", timeMillis).apply()
}
private fun getLastFallbackKeyPublishTime(): Long {
return storage.getLong("last_fb_key_publish", 0)
}
/**
* Upload curve25519 one time keys.
*/
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
curve25519Map.forEach { (key_id, value) ->
val k = mutableMapOf<String, Any>()
k["key"] = value
// the key is also signed
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
k["signatures"] = objectSigner.signObject(canonicalJson)
oneTimeJson["signed_curve25519:$key_id"] = k
}
val fallbackJson = mutableMapOf<String, Any>()
val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
fallbackCurve25519Map.forEach { (key_id, key) ->
val k = mutableMapOf<String, Any>()
k["key"] = key
k["fallback"] = true
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
k["signatures"] = objectSigner.signObject(canonicalJson)
fallbackJson["signed_curve25519:$key_id"] = k
}
// For now, we set the device id explicitly, as we may not be using the
// same one as used in login.
val uploadParams = UploadKeysTask.Params(
KeysUploadBody(
deviceKeys = null,
oneTimeKeys = oneTimeJson,
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
)
)
return uploadKeysTask.executeRetry(uploadParams, 3)
}
companion object {
// max number of keys to upload at once
// Creating keys can be an expensive operation so we limit the
// number we generate in one go to avoid blocking the application
// for too long.
private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5
// frequency with which to check & upload one-time keys
private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute
}
}

View file

@ -1,526 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import timber.log.Timber
import java.util.Stack
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.system.measureTimeMillis
private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO)
/**
* This class is responsible for sending key requests to other devices when a message failed to decrypt.
* It's lifecycle is based on the sync pulse:
* - You can post queries for session, or report when you got a session
* - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices
* If a request failed it will be retried at the end of the next sync
*/
@SessionScope
internal class OutgoingKeyRequestManager @Inject constructor(
@SessionId private val sessionId: String,
@UserId private val myUserId: String,
private val cryptoStore: IMXCryptoStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoConfig: MXCryptoConfig,
private val inboundGroupSessionStore: InboundGroupSessionStore,
private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager,
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter
) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
// We only have one active key request per session, so we don't request if it's already requested
// But it could make sense to check more the backup, as it's evolving.
// We keep a stack as we consider that the key requested last is more likely to be on screen?
private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack<Pair<String, String>>()
fun requestKeyForEvent(event: Event, force: Boolean) {
val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return
val index = ratchetIndexForMessage(event) ?: 0
postRoomKeyRequest(body, targets, index, force)
}
private fun getRoomKeyRequestTargetForEvent(event: Event): Pair<Map<String, List<String>>, RoomKeyRequestBody>? {
val sender = event.senderId ?: return null
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return null.also {
Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content")
}
if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
val senderDevice = encryptedEventContent.deviceId
val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
mapOf(
myUserId to listOf("*")
)
} else {
if (event.senderId == myUserId) {
mapOf(
myUserId to listOf("*")
)
} else {
// for the case where you share the key with a device that has a broken olm session
// The other user might Re-shares a megolm session key with devices if the key has already been
// sent to them.
mapOf(
myUserId to listOf("*"),
// We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700
// so in this case query to all
sender to listOf(senderDevice ?: "*")
)
}
}
val requestBody = RoomKeyRequestBody(
roomId = event.roomId,
algorithm = encryptedEventContent.algorithm,
senderKey = encryptedEventContent.senderKey,
sessionId = encryptedEventContent.sessionId
)
return recipients to requestBody
}
private fun ratchetIndexForMessage(event: Event): Int? {
val encryptedContent = event.content.toModel<EncryptedEventContent>() ?: return null
if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let {
tryOrNull {
val megolmVersion = it.read()
if (megolmVersion != 3) return@tryOrNull null
/** Int tag */
if (it.read() != 8) return@tryOrNull null
it.read()
}
}
}
fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean = false) {
outgoingRequestScope.launch {
sequencer.post {
internalQueueRequest(requestBody, recipients, fromIndex, force)
}
}
}
/**
* Typically called when we the session as been imported or received meanwhile.
*/
fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) {
outgoingRequestScope.launch {
sequencer.post {
internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex)
}
}
}
fun onSelfCrossSigningTrustChanged(newTrust: Boolean) {
if (newTrust) {
// we were previously not cross signed, but we are now
// so there is now more chances to get better replies for existing request
// Let's forget about sent request so that next time we try to decrypt we will resend requests
// We don't resend all because we don't want to generate a bulk of traffic
outgoingRequestScope.launch {
sequencer.post {
cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT)
}
sequencer.post {
delay(1000)
perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true)
}
}
}
}
fun onRoomKeyForwarded(
sessionId: String,
algorithm: String,
roomId: String,
senderKey: String,
fromDevice: String?,
fromIndex: Int,
event: Event
) {
Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex")
outgoingRequestScope.launch {
sequencer.post {
cryptoStore.updateOutgoingRoomKeyReply(
roomId = roomId,
sessionId = sessionId,
algorithm = algorithm,
senderKey = senderKey,
fromDevice = fromDevice,
// strip out encrypted stuff as it's just a trail?
event = event.copy(
type = event.getClearType(),
content = mapOf(
"chain_index" to fromIndex
)
)
)
}
}
}
fun onRoomKeyWithHeld(
sessionId: String,
algorithm: String,
roomId: String,
senderKey: String,
fromDevice: String?,
event: Event
) {
outgoingRequestScope.launch {
sequencer.post {
Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice")
Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}")
// We want to store withheld code from the sender of the message (owner of the megolm session), not from
// other devices that might gossip the key. If not the initial reason might be overridden
// by a request to one of our session.
event.getClearContent().toModel<RoomKeyWithHeldContent>()?.let { withheld ->
withContext(coroutineDispatchers.crypto) {
tryOrNull {
deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false)
}
cryptoStore.getUserDeviceList(event.senderId ?: "")
.also { devices ->
Timber.tag(loggerTag.value)
.v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}")
}
?.firstOrNull {
it.identityKey() == senderKey
}
}.also {
Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}")
}?.let {
if (it.userId == event.senderId) {
if (fromDevice != null) {
if (it.deviceId == fromDevice) {
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
cryptoStore.addWithHeldMegolmSession(withheld)
}
} else {
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
cryptoStore.addWithHeldMegolmSession(withheld)
}
}
}
}
// Here we store the replies from a given request
cryptoStore.updateOutgoingRoomKeyReply(
roomId = roomId,
sessionId = sessionId,
algorithm = algorithm,
senderKey = senderKey,
fromDevice = fromDevice,
event = event
)
}
}
}
/**
* Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those).
*/
fun requireProcessAllPendingKeyRequests() {
outgoingRequestScope.launch {
sequencer.post {
internalProcessPendingKeyRequests()
}
}
}
private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) {
// do we have known requests for that session??
Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId")
val knownRequest = cryptoStore.getOutgoingRoomKeyRequest(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
roomId = roomId,
sessionId = sessionId,
senderKey = senderKey
)
if (knownRequest.isEmpty()) return Unit.also {
Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested")
}
if (knownRequest.size > 1) {
// It's worth logging, there should be only one
Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId")
}
knownRequest.forEach { request ->
when (request.state) {
OutgoingRoomKeyRequestState.UNSENT -> {
if (request.fromIndex >= localKnownChainIndex) {
// we have a good index we can cancel
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
}
}
OutgoingRoomKeyRequestState.SENT -> {
// It was already sent, and index satisfied we can cancel
if (request.fromIndex >= localKnownChainIndex) {
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
// It is already marked to be cancelled
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
if (request.fromIndex >= localKnownChainIndex) {
// we just want to cancel now
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
}
}
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
// was already canceled
// if we need a better index, should we resend?
}
}
}
}
fun close() {
try {
outgoingRequestScope.cancel("User Terminate")
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
}
}
private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean) {
if (!cryptoStore.isKeyGossipingEnabled()) {
// we might want to try backup?
if (requestBody.roomId != null && requestBody.sessionId != null) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId)
}
Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled")
return
}
Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force")
val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}")
when (existing?.state) {
null -> {
// create a new one
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
}
OutgoingRoomKeyRequestState.UNSENT -> {
// nothing it's new or not yet handled
}
OutgoingRoomKeyRequestState.SENT -> {
// it was already requested
Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested")
if (force) {
// update to UNSENT
Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}")
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
} else {
if (existing.roomId != null && existing.sessionId != null) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId)
}
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
// request is canceled only if I got the keys so what to do here...
if (force) {
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
// It's already going to resend
}
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
if (force) {
cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId)
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
}
}
}
if (existing != null && existing.fromIndex >= fromIndex) {
// update the required index
cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex)
}
}
private suspend fun internalProcessPendingKeyRequests() {
val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates())
Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)")
measureTimeMillis {
toProcess.forEach {
when (it.state) {
OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it)
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it)
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it)
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED,
OutgoingRoomKeyRequestState.SENT -> {
// these are filtered out
}
}
}
}.let {
Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms")
}
val maxBackupCallsBySync = 60
var currentCalls = 0
measureTimeMillis {
while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) ->
// we want to rate limit that somehow :/
perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)
}
currentCalls++
}
}.let {
Timber.tag(loggerTag.value).v("Finish querying backup in $it ms")
}
}
private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) {
// In order to avoid generating to_device traffic, we can first check if the key is backed up
Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}")
val sessionId = request.sessionId ?: return
val roomId = request.roomId ?: return
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
// let's see what's the index
val knownIndex = tryOrNull {
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")
?.wrapper
?.session
?.firstKnownIndex
}
if (knownIndex != null && knownIndex <= request.fromIndex) {
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request")
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
return
}
}
// we need to send the request
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
body = request.requestBody
)
val contentMap = MXUsersDevicesMap<Any>()
request.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
val params = SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = request.requestId
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}")
// The request was sent, so update state
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT)
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}")
}
}
private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean {
Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}")
// we have to cancel this
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION
)
val contentMap = MXUsersDevicesMap<Any>()
request.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
val params = SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = request.requestId
)
return try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
// The request cancellation was sent, we don't delete yet because we want
// to keep trace of the sent replies
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED)
true
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}")
false
}
}
private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) {
if (handleRequestToCancel(request)) {
// this will create a new unsent request with no replies that will be process in the following call
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) }
}
}
}

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class RoomDecryptorProvider @Inject constructor(
private val olmDecryptionFactory: MXOlmDecryptionFactory,
private val megolmDecryptionFactory: MXMegolmDecryptionFactory
) {
// A map from algorithm to MXDecrypting instance, for each room
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
private val newSessionListeners = ArrayList<NewSessionListener>()
fun addNewSessionListener(listener: NewSessionListener) {
if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener)
}
fun removeSessionListener(listener: NewSessionListener) {
newSessionListeners.remove(listener)
}
/**
* Get a decryptor for a given room and algorithm.
* If we already have a decryptor for the given room and algorithm, return
* it. Otherwise try to instantiate it.
*
* @param roomId the room id
* @param algorithm the crypto algorithm
* @return the decryptor
* // TODO Create another method for the case of roomId is null
*/
fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
// sanity check
if (algorithm.isNullOrEmpty()) {
Timber.e("## getRoomDecryptor() : null algorithm")
return null
}
if (roomId != null && roomId.isNotEmpty()) {
synchronized(roomDecryptors) {
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
val alg = decryptors[algorithm]
if (alg != null) {
return alg
}
}
}
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
if (decryptingClass) {
val alg = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply {
this.newSessionListener = object : NewSessionListener {
override fun onNewSession(roomId: String?, sessionId: String) {
// PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor
newSessionListeners.toList().forEach {
try {
it.onNewSession(roomId, sessionId)
} catch (ignore: Throwable) {
}
}
}
}
}
else -> olmDecryptionFactory.create()
}
if (!roomId.isNullOrEmpty()) {
synchronized(roomDecryptors) {
roomDecryptors[roomId]?.put(algorithm, alg)
}
}
return alg
}
return null
}
fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
if (roomId == null || algorithm == null) {
return null
}
return roomDecryptors[roomId]?.get(algorithm)
}
}

View file

@ -1,60 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class RoomEncryptorsStore @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
private val olmEncryptionFactory: MXOlmEncryptionFactory,
) {
// MXEncrypting instance for each room.
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
fun put(roomId: String, alg: IMXEncrypting) {
synchronized(roomEncryptors) {
roomEncryptors.put(roomId, alg)
}
}
fun get(roomId: String): IMXEncrypting? {
return synchronized(roomEncryptors) {
val cache = roomEncryptors[roomId]
if (cache != null) {
return@synchronized cache
} else {
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
else -> null
}
alg?.let { roomEncryptors.put(roomId, it) }
return@synchronized alg
}
}
}
}

View file

@ -1,344 +0,0 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO)
@SessionScope
internal class SecretShareManager @Inject constructor(
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val cryptoCoroutineScope: CoroutineScope,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock,
) {
companion object {
private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes
}
/**
* Secret gossiping only occurs during a limited window period after interactive verification.
* We keep track of recent verification in memory for that purpose (no need to persist)
*/
private val recentlyVerifiedDevices = mutableMapOf<String, Long>()
private val verifMutex = Mutex()
/**
* Secrets are exchanged as part of interactive verification,
* so we can just store in memory.
*/
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
fun addListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfill gossiping requests.
* This should be called as fast as possible after a successful self interactive verification
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
cryptoCoroutineScope.launch {
verifMutex.withLock {
recentlyVerifiedDevices[deviceId] = clock.epochMillis()
}
}
}
suspend fun handleSecretRequest(toDevice: Event) {
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request")
}
// val (action, requestingDeviceId, requestId, secretName) = it
val secretName = request.secretName ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing secret name")
}
val userId = toDevice.senderId ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing senderId")
}
if (userId != credentials.userId) {
// secrets are only shared between our own devices
Timber.tag(loggerTag.value)
.e("Ignoring secret share request from other users $userId")
return
}
val deviceId = request.requestingDeviceId
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request norequestingDeviceId ")
}
if (deviceId == credentials.deviceId) {
return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Ignore request from self device")
}
}
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.e("Received secret share request from unknown device $deviceId")
}
val isRequestingDeviceTrusted = device.isVerified
val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId)
if (isRequestingDeviceTrusted && isRecentInteractiveVerification) {
// we can share the secret
val secretValue = when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64()
else -> null
}
if (secretValue == null) {
Timber.tag(loggerTag.value)
.i("The secret is unknown $secretName, passing to app layer")
val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() }
toList.onEach { listener ->
listener.onSecretShareRequest(request)
}
return
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to mapOf(
"request_id" to request.requestId,
"secret" to secretValue
)
)
// Is it possible that we don't have an olm session?
val devicesByUser = mapOf(device.userId to listOf(device))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Can't share secret ${request.secretName}: Failed to establish olm session")
return
}
val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("secret share: no session with this device $deviceId, probably because there were no one-time keys")
return
}
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload)
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try {
// raise the retries for secret
sendToDeviceTask.executeRetry(sendToDeviceParams, 6)
Timber.tag(loggerTag.value)
.i("successfully shared secret $secretName to ${device.shortDebugString()}")
// TODO add a trail for that in audit logs
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}")
}
} else {
Timber.tag(loggerTag.value)
.d(" Received secret share request from un-authorised device ${device.deviceId}")
}
}
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp = verifMutex.withLock {
recentlyVerifiedDevices[deviceId]
} ?: return false
val age = clock.epochMillis() - verifTimestamp
return age < SECRET_SHARE_WINDOW_DURATION
}
suspend fun requestSecretTo(deviceId: String, secretName: String) {
val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also {
Timber.tag(loggerTag.value)
.d("Can't request secret for $secretName unknown device $deviceId")
}
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
secretName = secretName,
requestId = createUniqueTxnId()
)
verifMutex.withLock {
outgoingSecretRequests.add(toDeviceContent)
}
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent)
val params = SendToDeviceTask.Params(
eventType = EventType.REQUEST_SECRET,
contentMap = contentMap
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(params)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}")
}
}
suspend fun requestMissingSecrets() {
// quick implementation for backward compatibility with rust, will request all secrets to all own devices
val secretNames = listOf(MASTER_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)
secretNames.forEach { secretName ->
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
secretName = secretName,
requestId = createUniqueTxnId()
)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(credentials.userId, "*", toDeviceContent)
val params = SendToDeviceTask.Params(
eventType = EventType.REQUEST_SECRET,
contentMap = contentMap
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(params)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName")
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to request secret $secretName")
}
}
}
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
Timber.tag(loggerTag.value)
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
if (!toDevice.isEncrypted()) {
// secret send messages must be encrypted
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
return
}
// no need to download keys, after a verification we already forced download
val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }
if (sendingDevice == null) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}")
return
}
// Was that sent by us?
if (sendingDevice.userId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
return
}
if (!sendingDevice.isVerified) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}")
return
}
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = verifMutex.withLock {
outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId }
}
// As per spec:
// Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to.
if (existingRequest?.secretName == null) {
Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
// we don't need to cancel the request as we only request to one device
// just forget about the request now
verifMutex.withLock {
outgoingSecretRequests.remove(existingRequest)
}
if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
}
}
}

View file

@ -1,182 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.actions
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.model.MXKey
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
private const val ONE_TIME_KEYS_RETRY_COUNT = 3
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
@SessionScope
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
private val olmDevice: MXOlmDevice,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask
) {
private val ensureMutex = Mutex()
/**
* We want to synchronize a bit here, because we are iterating to check existing olm session and
* also adding some.
*/
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
ensureMutex.withLock {
val results = MXUsersDevicesMap<MXOlmSessionResult>()
val deviceList = devicesByUser.flatMap { it.value }
Timber.tag(loggerTag.value)
.d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
if (force) {
// we take all devices and will query otk for them
devicesToCreateSessionWith.addAll(deviceList)
} else {
// only peek devices without active session
deviceList.forEach { deviceInfo ->
val deviceId = deviceInfo.deviceId
val userId = deviceInfo.userId
val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
}
// is there a session that as been already used?
val sessionId = olmDevice.getSessionId(key)
if (sessionId.isNullOrEmpty()) {
Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
devicesToCreateSessionWith.add(deviceInfo)
} else {
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
results.setObject(userId, deviceId, olmSessionResult)
}
}
}
if (devicesToCreateSessionWith.isEmpty()) {
// no session to create
return results
}
val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
devicesToCreateSessionWith.forEach {
setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
}
}
// Let's now claim one time keys
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim.map)
val oneTimeKeysForUsers = withContext(coroutineDispatchers.io) {
oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
}
val oneTimeKeys = MXUsersDevicesMap<MXKey>()
for ((userId, mapByUserId) in oneTimeKeysForUsers.oneTimeKeys.orEmpty()) {
for ((deviceId, deviceKey) in mapByUserId) {
val mxKey = MXKey.from(deviceKey)
if (mxKey != null) {
oneTimeKeys.setObject(userId, deviceId, mxKey)
} else {
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
}
}
}
// let now start olm session using the new otks
devicesToCreateSessionWith.forEach { deviceInfo ->
val userId = deviceInfo.userId
val deviceId = deviceInfo.deviceId
// Did we get an OTK
val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
if (oneTimeKey == null) {
Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
} else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
} else {
val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
if (olmSessionId != null) {
val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
results.setObject(userId, deviceId, olmSessionResult)
} else {
Timber
.tag(loggerTag.value)
.d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
}
}
}
return results
}
}
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
var sessionId: String? = null
val deviceId = deviceInfo.deviceId
val signKeyId = "ed25519:$deviceId"
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
val fingerprint = deviceInfo.fingerprint()
if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) {
var isVerified = false
var errorMessage: String? = null
try {
olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature)
isVerified = true
} catch (e: Exception) {
Timber.tag(loggerTag.value).d(
e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," +
" signature:$signature fingerprint:$fingerprint"
)
Timber.tag(loggerTag.value).e(
"verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " +
" - signable json ${oneTimeKey.signalableJSONDictionary()}"
)
errorMessage = e.message
}
// Check one-time key signature
if (isVerified) {
sessionId = deviceInfo.identityKey()?.let { identityKey ->
olmDevice.createOutboundSession(identityKey, oneTimeKey.value)
}
if (sessionId.isNullOrEmpty()) {
// Possibly a bad key
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
} else {
Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId")
}
} else {
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage")
}
}
return sessionId
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.actions
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import javax.inject.Inject
internal class EnsureOlmSessionsForUsersAction @Inject constructor(
private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
) {
/**
* Try to make sure we have established olm sessions for the given users.
* @param users a list of user ids.
*/
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = users.associateWith { userId ->
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
devices.filter {
// Don't bother setting up session to ourself
it.identityKey() != olmDevice.deviceCurve25519Key &&
// Don't bother setting up sessions with blocked users
!(it.trustLevel?.isVerified() ?: false)
}
}
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
}
}

View file

@ -1,132 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.actions
import androidx.annotation.WorkerThread
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
internal class MegolmSessionDataImporter @Inject constructor(
private val olmDevice: MXOlmDevice,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val clock: Clock,
) {
/**
* Import a list of megolm session keys.
* Must be call on the crypto coroutine thread
*
* @param megolmSessionsData megolm sessions.
* @param fromBackup true if the imported keys are already backed up on the server.
* @param progressListener the progress listener
* @return import room keys result
*/
@WorkerThread
fun handle(
megolmSessionsData: List<MegolmSessionData>,
fromBackup: Boolean,
progressListener: ProgressListener?
): ImportRoomKeysResult {
val t0 = clock.epochMillis()
val importedSession = mutableMapOf<String, MutableMap<String, MutableList<String>>>()
val totalNumbersOfKeys = megolmSessionsData.size
var lastProgress = 0
var totalNumbersOfImportedKeys = 0
progressListener?.onProgress(0, 100)
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)
megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)
if (null != decrypting) {
try {
val sessionId = megolmSessionData.sessionId ?: return@forEachIndexed
val senderKey = megolmSessionData.senderKey ?: return@forEachIndexed
val roomId = megolmSessionData.roomId ?: return@forEachIndexed
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
importedSession.getOrPut(roomId) { mutableMapOf() }
.getOrPut(senderKey) { mutableListOf() }
.add(sessionId)
totalNumbersOfImportedKeys++
// cancel any outstanding room key requests for this session
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
sessionId,
roomId,
senderKey,
tryOrNull {
olmInboundGroupSessionWrappers
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
?.session?.firstKnownIndex
?.toInt()
} ?: 0
)
// Have another go at decrypting events sent with this session
when (decrypting) {
is MXMegolmDecryption -> {
decrypting.onNewSession(megolmSessionData.roomId, senderKey, sessionId)
}
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
}
}
if (progressListener != null) {
val progress = 100 * (cpt + 1) / totalNumbersOfKeys
if (lastProgress != progress) {
lastProgress = progress
progressListener.onProgress(progress, 100)
}
}
}
// Do not back up the key if it comes from a backup recovery
if (fromBackup) {
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
}
val t1 = clock.epochMillis()
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys, importedSession)
}
}

View file

@ -1,89 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.actions
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.convertToUTF8
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
internal class MessageEncrypter @Inject constructor(
@UserId
private val userId: String,
@DeviceId
private val deviceId: String?,
private val olmDevice: MXOlmDevice
) {
/**
* Encrypt an event payload for a list of devices.
* This method must be called from the getCryptoHandler() thread.
*
* @param payloadFields fields to include in the encrypted payload.
* @param deviceInfos list of device infos to encrypt for.
* @return the content for an m.room.encrypted event.
*/
suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
val payloadJson = payloadFields.toMutableMap()
payloadJson["sender"] = userId
payloadJson["sender_device"] = deviceId!!
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!)
val ciphertext = mutableMapOf<String, Any>()
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
val sessionId = olmDevice.getSessionId(deviceKey)
if (!sessionId.isNullOrEmpty()) {
Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
payloadJson["recipient"] = deviceInfo.userId
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!!
}
}
return EncryptedMessage(
algorithm = MXCRYPTO_ALGORITHM_OLM,
senderKey = olmDevice.deviceCurve25519Key,
cipherText = ciphertext
)
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.actions
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.UserId
import timber.log.Timber
import javax.inject.Inject
internal class SetDeviceVerificationAction @Inject constructor(
private val cryptoStore: IMXCryptoStore,
@UserId private val userId: String,
private val defaultKeysBackupService: DefaultKeysBackupService
) {
suspend fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
val device = cryptoStore.getUserDevice(userId, deviceId)
// Sanity check
if (null == device) {
Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId")
return
}
if (device.isVerified != trustLevel.isVerified()) {
if (userId == this.userId) {
// If one of the user's own devices is being marked as verified / unverified,
// check the key backup status, since whether or not we use this depends on
// whether it has a signature from a verified device
defaultKeysBackupService.checkAndStartKeysBackup()
}
}
if (device.trustLevel != trustLevel) {
device.trustLevel = trustLevel
cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified)
}
}
}

View file

@ -1,47 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
/**
* An interface for decrypting data.
*/
internal interface IMXDecrypting {
/**
* Decrypt an event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error
*/
@Throws(MXCryptoError::class)
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/**
* Handle a key event.
*
* @param event the key event.
* @param defaultKeysBackupService the keys backup service
* @param forceAccept the keys backup service
*/
suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
}

View file

@ -1,39 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
/**
* An interface for encrypting data.
*/
internal interface IMXEncrypting {
/**
* Encrypt an event content according to the configuration of the room.
*
* @param eventContent the content of the event.
* @param eventType the type of the event.
* @param userIds the room members the event will be sent to.
* @return the encrypted content
*/
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
}

View file

@ -1,54 +0,0 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms
internal interface IMXGroupEncryption {
/**
* In Megolm, each recipient maintains a record of the ratchet value which allows
* them to decrypt any messages sent in the session after the corresponding point
* in the conversation. If this value is compromised, an attacker can similarly
* decrypt past messages which were encrypted by a key derived from the
* compromised or subsequent ratchet values. This gives 'partial' forward
* secrecy.
*
* To mitigate this issue, the application should offer the user the option to
* discard historical conversations, by winding forward any stored ratchet values,
* or discarding sessions altogether.
*/
fun discardSessionKey()
suspend fun preshareKey(userIds: List<String>)
/**
* Re-shares a session key with devices if the key has already been
* sent to them.
*
* @param groupSessionId The id of the outbound session to share.
* @param userId The id of the user who owns the target device.
* @param deviceId The id of the target device.
* @param senderKey The key of the originating device for the session.
*
* @return true in case of success
*/
suspend fun reshareKey(
groupSessionId: String,
userId: String,
deviceId: String,
senderKey: String
): Boolean
}

View file

@ -1,364 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice,
private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val liveEventManager: Lazy<StreamEventsManager>,
private val unrequestedForwardManager: UnRequestedForwardManager,
private val cryptoConfig: MXCryptoConfig,
private val clock: Clock,
) : IMXDecrypting {
var newSessionListener: NewSessionListener? = null
/**
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
* senderKey|sessionId to timelines to list of MatrixEvents.
*/
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
@Throws(MXCryptoError::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return decryptEvent(event, timeline, true)
}
@Throws(MXCryptoError::class)
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
if (encryptedEventContent.senderKey.isNullOrBlank() ||
encryptedEventContent.sessionId.isNullOrBlank() ||
encryptedEventContent.ciphertext.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
return runCatching {
olmDevice.decryptGroupMessage(
encryptedEventContent.ciphertext,
event.roomId,
timeline,
eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)
}
.fold(
{ olmDecryptionResult ->
// the decryption succeeds
if (olmDecryptionResult.payload != null) {
MXEventDecryptionResult(
clearEvent = olmDecryptionResult.payload,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty(),
messageVerificationState = olmDecryptionResult.verificationState,
).also {
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
}
} else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
},
{ throwable ->
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// So we know that session, but it's ratcheted and we can't decrypt at that index
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Check if partially withheld
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX",
null
)
}
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.OLM,
reason,
detailedReason
)
}
if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Check if it was withheld by sender to enrich error code
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
}
throw throwable
}
)
}
/**
* Helper for the real decryptEvent and for _retryDecryption. If
* requestKeysOnFail is true, we'll send an m.room_key_request when we fail
* to decrypt the event due to missing megolm keys.
*
* @param event the event
*/
private fun requestKeysForEvent(event: Event) {
outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
/**
* Handle a key event.
*
* @param event the key event.
* @param defaultKeysBackupService the keys backup service
* @param forceAccept if true will force to accept the forwarded key
*/
override suspend fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
var exportFormat = false
val roomKeyContent = event.getDecryptedContent()?.toModel<RoomKeyContent>() ?: return
val eventSenderKey: String = event.getSenderKey() ?: return Unit.also {
Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field")
}
// this device might not been downloaded now?
val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey)
lateinit var sessionInitiatorSenderKey: String
val trusted: Boolean
var keysClaimed: MutableMap<String, String> = HashMap()
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
return
}
if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) {
if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
return
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>()
?: return
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
forwardingCurve25519KeyChain.addAll(it)
}
forwardingCurve25519KeyChain.add(eventSenderKey)
exportFormat = true
sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
}
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field")
return
}
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
// checking if was requested once.
// should we check if the request is sort of active?
val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest(
roomId = forwardedRoomKeyContent.roomId.orEmpty(),
sessionId = forwardedRoomKeyContent.sessionId.orEmpty(),
algorithm = forwardedRoomKeyContent.algorithm.orEmpty(),
senderKey = forwardedRoomKeyContent.senderKey.orEmpty(),
).isEmpty()
trusted = false
if (!forceAccept && wasNotRequested) {
// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty()
unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis())
// Ignore unsolicited
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested")
return
}
// Check who sent the request, as we requested we have the device keys (no need to download)
val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey)
if (sessionThatIsSharing == null) {
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey")
return
}
val isOwnDevice = myUserId == sessionThatIsSharing.userId
val isDeviceVerified = sessionThatIsSharing.isVerified
val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey
val isLegitForward = (isOwnDevice && isDeviceVerified) ||
(!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator)
val shouldAcceptForward = forceAccept || isLegitForward
if (!shouldAcceptForward) {
Timber.tag(loggerTag.value)
.w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," +
" fromInitiator:$isFromSessionInitiator")
return
}
} else {
// It's a m.room_key so safe
trusted = true
sessionInitiatorSenderKey = eventSenderKey
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
// inherit the claimed ed25519 key from the setup message
keysClaimed = event.getKeysClaimed().toMutableMap()
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val addSessionResult = olmDevice.addInboundGroupSession(
sessionId = roomKeyContent.sessionId,
sessionKey = roomKeyContent.sessionKey,
roomId = roomKeyContent.roomId,
senderKey = sessionInitiatorSenderKey,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
keysClaimed = keysClaimed,
exportFormat = exportFormat,
sharedHistory = roomKeyContent.getSharedKey(),
trusted = trusted
).also {
Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it")
}
when (addSessionResult) {
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
else -> null
}?.let { index ->
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
outgoingKeyRequestManager.onRoomKeyForwarded(
sessionId = roomKeyContent.sessionId,
algorithm = roomKeyContent.algorithm ?: "",
roomId = roomKeyContent.roomId,
senderKey = sessionInitiatorSenderKey,
fromIndex = index,
fromDevice = fromDevice?.deviceId,
event = event
)
cryptoStore.saveIncomingForwardKeyAuditTrail(
roomId = roomKeyContent.roomId,
sessionId = roomKeyContent.sessionId,
senderKey = sessionInitiatorSenderKey,
algorithm = roomKeyContent.algorithm ?: "",
userId = event.senderId.orEmpty(),
deviceId = fromDevice?.deviceId.orEmpty(),
chainIndex = index.toLong()
)
// The index is used to decide if we cancel sent request or if we wait for a better key
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index)
}
}
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
Timber.tag(loggerTag.value)
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
defaultKeysBackupService.maybeBackupKeys()
onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId)
}
}
/**
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
*/
private fun RoomKeyContent.getSharedKey(): Boolean {
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
return sharedHistory ?: false
}
/**
* Check if the some messages can be decrypted with a new session.
*
* @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
* @param senderKey the session sender key
* @param sessionId the session id
*/
fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(roomId, sessionId)
}
}

View file

@ -1,52 +0,0 @@
/*
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
@UserId private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val eventsManager: Lazy<StreamEventsManager>,
private val unrequestedForwardManager: UnRequestedForwardManager,
private val mxCryptoConfig: MXCryptoConfig,
private val clock: Clock,
) {
fun create(): MXMegolmDecryption {
return MXMegolmDecryption(
olmDevice = olmDevice,
myUserId = myUserId,
outgoingKeyRequestManager = outgoingKeyRequestManager,
cryptoStore = cryptoStore,
liveEventManager = eventsManager,
unrequestedForwardManager = unrequestedForwardManager,
cryptoConfig = mxCryptoConfig,
clock = clock,
)
}
}

View file

@ -1,613 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.forEach
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.model.toDebugCount
import org.matrix.android.sdk.internal.crypto.model.toDebugString
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.convertToUTF8
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO)
internal class MXMegolmEncryption(
// The id of the room we will be sending to.
private val roomId: String,
private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val myUserId: String,
private val myDeviceId: String,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock,
) : IMXEncrypting, IMXGroupEncryption {
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
// that even if this is non-null, it may not be ready for use (in which
// case outboundSession.shareOperation will be non-null.)
private var outboundSession: MXOutboundSessionInfo? = null
init {
// restore existing outbound session if any
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
}
// Default rotation periods
// TODO Make it configurable via parameters
// Session rotation periods
private var sessionRotationPeriodMsgs: Int = 100
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
override suspend fun encryptEventContent(
eventContent: Content,
eventType: String,
userIds: List<String>
): Content {
val ts = clock.epochMillis()
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
/**
* When using in-room messages and the room has encryption enabled,
* clients should ensure that encryption does not hinder the verification.
* For example, if the verification messages are encrypted, clients must ensure that all the recipients
* unverified devices receive the keys necessary to decrypt the messages,
* even if they would normally not be given the keys to decrypt messages in the room.
*/
val shouldSendToUnverified = isVerificationEvent(eventType, eventContent)
val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified)
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
val outboundSession = ensureOutboundSession(devices.allowedDevices)
return encryptContent(outboundSession, eventType, eventContent)
.also {
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
// annoyingly we have to serialize again the saved outbound session to store message index :/
// if not we would see duplicate message index errors
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis")
}
}
private fun isVerificationEvent(eventType: String, eventContent: Content) =
EventType.isVerificationEvent(eventType) ||
(eventType == EventType.MESSAGE &&
eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST)
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
// offload to computation thread
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
devices.forEach { userId, deviceId, withheldCode ->
this.add(UserDevice(userId, deviceId) to withheldCode)
}
}.groupBy(
{ it.second },
{ it.first }
).forEach { (code, targets) ->
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
}
}
}
override fun discardSessionKey() {
outboundSession = null
olmDevice.discardOutboundGroupSessionForRoom(roomId)
}
override suspend fun preshareKey(userIds: List<String>) {
val ts = clock.epochMillis()
Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...")
val devices = getDevicesInRoom(userIds)
val outboundSession = ensureOutboundSession(devices.allowedDevices)
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis")
}
/**
* Prepare a new session.
*
* @return the session description
*/
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
val keysClaimedMap = mapOf(
"ed25519" to olmDevice.deviceEd25519Key!!
)
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
olmDevice.addInboundGroupSession(
sessionId = sessionId!!,
sessionKey = olmDevice.getSessionKey(sessionId)!!,
roomId = roomId,
senderKey = olmDevice.deviceCurve25519Key!!,
forwardingCurve25519KeyChain = emptyList(),
keysClaimed = keysClaimedMap,
exportFormat = false,
sharedHistory = sharedHistory,
trusted = true
)
cryptoCoroutineScope.launch {
defaultKeysBackupService.maybeBackupKeys()
}
return MXOutboundSessionInfo(
sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore),
clock = clock,
sharedHistory = sharedHistory
)
}
/**
* Ensure the outbound session.
*
* @param devicesInRoom the devices list
*/
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId")
var session = outboundSession
if (session == null ||
// Need to make a brand new session?
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
// Is there a room history visibility change since the last outboundSession
cryptoStore.shouldShareHistory(roomId) != session.sharedHistory ||
// Determine if we have shared with anyone we shouldn't have
session.sharedWithTooManyDevices(devicesInRoom)) {
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
session = prepareNewSessionInRoom()
outboundSession = session
}
val safeSession = session
val shareMap = HashMap<String, MutableList<CryptoDeviceInfo>>()/* userId */
val userIds = devicesInRoom.userIds
for (userId in userIds) {
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, deviceInfo).found) {
val devices = shareMap.getOrPut(userId) { ArrayList() }
devices.add(deviceInfo)
}
}
}
val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size }
Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})")
shareKey(safeSession, shareMap)
return safeSession
}
/**
* Share the device key to a list of users.
*
* @param session the session info
* @param devicesByUsers the devices map
*/
private suspend fun shareKey(
session: MXOutboundSessionInfo,
devicesByUsers: Map<String, List<CryptoDeviceInfo>>
) {
// nothing to send, the task is done
if (devicesByUsers.isEmpty()) {
Timber.tag(loggerTag.value).v("shareKey() : nothing more to do")
return
}
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
val subMap = HashMap<String, List<CryptoDeviceInfo>>()
var devicesCount = 0
for ((userId, devices) in devicesByUsers) {
subMap[userId] = devices
devicesCount += devices.size
if (devicesCount > 100) {
break
}
}
Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices)
}
/**
* Share the device keys of a an user.
*
* @param sessionInfo the session info
* @param devicesByUser the devices map
*/
private suspend fun shareUserDevicesKey(
sessionInfo: MXOutboundSessionInfo,
devicesByUser: Map<String, List<CryptoDeviceInfo>>
) {
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
}
val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
val payload = mapOf(
"type" to EventType.ROOM_KEY,
"content" to mapOf(
"algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
"room_id" to roomId,
"session_id" to sessionInfo.sessionId,
"session_key" to sessionKey,
"chain_index" to chainIndex,
"org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
)
)
var t0 = clock.epochMillis()
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
Timber.tag(loggerTag.value).v(
"""shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms"""
.trimMargin()
)
val contentMap = MXUsersDevicesMap<Any>()
var haveTargets = false
val userIds = results.userIds
val noOlmToNotify = mutableListOf<UserDevice>()
for (userId in userIds) {
val devicesToShareWith = devicesByUser[userId]
for ((deviceID) in devicesToShareWith!!) {
val sessionResult = results.getObject(userId, deviceID)
if (sessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
// MSC 2399
// send withheld m.no_olm: an olm session could not be established.
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld")
noOlmToNotify.add(UserDevice(userId, deviceID))
continue
}
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
haveTargets = true
}
}
// Add the devices we have shared with to session.sharedWithDevices.
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for ((_, devicesToShareWith) in devicesByUser) {
for (deviceInfo in devicesToShareWith) {
sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
// XXX is it needed to add it to the audit trail?
// For now decided that no, we are more interested by forward trail
}
}
if (haveTargets) {
t0 = clock.epochMillis()
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(sendToDeviceParams)
}
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
} catch (failure: Throwable) {
// What to do here...
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>")
}
} else {
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
}
if (noOlmToNotify.isNotEmpty()) {
// XXX offload?, as they won't read the message anyhow?
notifyKeyWithHeld(
noOlmToNotify,
sessionInfo.sessionId,
olmDevice.deviceCurve25519Key,
WithHeldCode.NO_OLM
)
}
}
private suspend fun notifyKeyWithHeld(
targets: List<UserDevice>,
sessionId: String,
senderKey: String?,
code: WithHeldCode
) {
Timber.tag(loggerTag.value).d(
"notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" +
" ${targets.joinToString { "${it.userId}|${it.deviceId}" }}"
)
val withHeldContent = RoomKeyWithHeldContent(
roomId = roomId,
senderKey = senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sessionId = sessionId,
codeString = code.value,
fromDevice = myDeviceId
)
val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD.stable,
MXUsersDevicesMap<Any>().apply {
targets.forEach {
setObject(it.userId, it.deviceId, withHeldContent)
}
}
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(params)
}
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}")
}
}
/**
* process the pending encryptions.
*/
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content {
// Everything is in place, encrypt all pending events
val payloadJson = HashMap<String, Any>()
payloadJson["room_id"] = roomId
payloadJson["type"] = eventType
payloadJson["content"] = eventContent
// Get canonical Json from
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString)
val map = HashMap<String, Any>()
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
map["sender_key"] = olmDevice.deviceCurve25519Key!!
map["ciphertext"] = ciphertext!!
map["session_id"] = session.sessionId
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
map["device_id"] = myDeviceId
session.useCount++
return map
}
/**
* Get the list of devices which can encrypt data to.
* This method must be called in getDecryptingThreadHandler() thread.
*
* @param userIds the user ids whose devices must be checked.
* @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if
* such devices are blocked in crypto settings
*/
private suspend fun getDevicesInRoom(userIds: List<String>, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo {
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
val keys = deviceListManager.downloadKeys(userIds, false)
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
cryptoStore.getBlockUnverifiedDevices(roomId)
val devicesInRoom = DeviceInRoomInfo()
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
for (userId in keys.userIds) {
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
for (deviceId in deviceIds) {
val deviceInfo = keys.getObject(userId, deviceId) ?: continue
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
// The device is not yet known by the user
unknownDevices.setObject(userId, deviceId, deviceInfo)
continue
}
if (deviceInfo.isBlocked) {
// Remove any blocked devices
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
continue
}
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) {
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
continue
}
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
// Don't bother sending to ourself
continue
}
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
}
}
if (unknownDevices.isEmpty) {
return devicesInRoom
} else {
throw MXCryptoError.UnknownDevice(unknownDevices)
}
}
override suspend fun reshareKey(
groupSessionId: String,
userId: String,
deviceId: String,
senderKey: String
): Boolean {
Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
// Get the chain index of the key we previously sent this device
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
if (!wasSessionSharedWithUser.found) {
// This session was never shared with this user
// Send a room key with held
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
return false
}
// if found chain index should not be null
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
.also {
Timber.tag(loggerTag.value).w("reshareKey: Null chain index")
}
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
null
}
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
return false
}
Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
val sessionHolder = try {
olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
return false
}
val export = sessionHolder.mutex.withLock {
sessionHolder.wrapper.exportKeys()
} ?: return false.also {
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
}
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
return try {
sendToDeviceTask.execute(sendToDeviceParams)
Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
true
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
false
}
}
@Throws
override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" }
Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
val userId = deviceInfo.userId
val deviceId = deviceInfo.deviceId
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm")
// process anyway?
null
}
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys")
return
}
val export = inboundSessionWrapper.mutex.withLock {
inboundSessionWrapper.wrapper.exportKeys()
} ?: return Unit.also {
Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}")
}
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload =
withContext(coroutineDispatchers.computation) {
messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
}
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.tag(loggerTag.value)
.d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(sendToDeviceParams)
}
}
data class DeviceInRoomInfo(
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
)
data class UserDevice(
val userId: String,
val deviceId: String
)
}

View file

@ -1,68 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock,
) {
fun create(roomId: String): MXMegolmEncryption {
return MXMegolmEncryption(
roomId = roomId,
olmDevice = olmDevice,
defaultKeysBackupService = defaultKeysBackupService,
cryptoStore = cryptoStore,
deviceListManager = deviceListManager,
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,
myUserId = userId,
myDeviceId = deviceId!!,
sendToDeviceTask = sendToDeviceTask,
messageEncrypter = messageEncrypter,
warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository,
coroutineDispatchers = coroutineDispatchers,
cryptoCoroutineScope = cryptoCoroutineScope,
clock = clock,
)
}
}

View file

@ -1,77 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
internal class MXOutboundSessionInfo(
// The id of the session
val sessionId: String,
val sharedWithHelper: SharedWithHelper,
private val clock: Clock,
// When the session was created
private val creationTime: Long = clock.epochMillis(),
val sharedHistory: Boolean = false
) {
// Number of times this session has been used
var useCount: Int = 0
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
var needsRotation = false
val sessionLifetime = clock.epochMillis() - creationTime
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
needsRotation = true
}
return needsRotation
}
/**
* Determine if this session has been shared with devices which it shouldn't have been.
*
* @param devicesInRoom the devices map
* @return true if we have shared the session with devices which aren't in devicesInRoom.
*/
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
val sharedWithDevices = sharedWithHelper.sharedWithDevices()
val userIds = sharedWithDevices.userIds
for (userId in userIds) {
if (null == devicesInRoom.getUserDeviceIds(userId)) {
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId")
return true
}
val deviceIds = sharedWithDevices.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
if (null == devicesInRoom.getObject(userId, deviceId)) {
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId")
return true
}
}
}
return false
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
internal class SharedWithHelper(
private val roomId: String,
private val sessionId: String,
private val cryptoStore: IMXCryptoStore
) {
fun sharedWithDevices(): MXUsersDevicesMap<Int> {
return cryptoStore.getSharedWithInfo(roomId, sessionId)
}
fun markedSessionAsShared(deviceInfo: CryptoDeviceInfo, chainIndex: Int) {
cryptoStore.markedSessionAsShared(
roomId = roomId,
sessionId = sessionId,
userId = deviceInfo.userId,
deviceId = deviceInfo.deviceId,
deviceIdentityKey = deviceInfo.identityKey() ?: "",
chainIndex = chainIndex
)
}
}

View file

@ -1,150 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.math.abs
private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000
@SessionScope
internal class UnRequestedForwardManager @Inject constructor(
private val deviceListManager: DeviceListManager,
) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
// For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups?
private val forwardedKeysPerRoom = mutableMapOf<String, MutableMap<String, MutableList<ForwardInfo>>>()
data class InviteInfo(
val roomId: String,
val fromMxId: String,
val timestamp: Long
)
data class ForwardInfo(
val event: Event,
val timestamp: Long
)
// roomId, local timestamp of invite
private val recentInvites = mutableListOf<InviteInfo>()
fun close() {
try {
scope.cancel("User Terminate")
} catch (failure: Throwable) {
Timber.w(failure, "Failed to shutDown UnrequestedForwardManager")
}
}
fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) {
Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp")
scope.launch {
sequencer.post {
if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) {
recentInvites.add(
InviteInfo(
roomId,
fromUserId,
localTimeStamp
)
)
}
}
}
}
fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) {
Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp")
scope.launch {
sequencer.post {
val claimSenderId = event.senderId.orEmpty()
val senderKey = event.getSenderKey()
// we might want to download keys, as this user might not be known yet, cache is ok
val ownerMxId =
tryOrNull {
deviceListManager.downloadKeys(listOf(claimSenderId), false)
.map[claimSenderId]
?.values
?.firstOrNull { it.identityKey() == senderKey }
?.userId
}
// Not sure what to do if the device has been deleted? I can't proove the mxid
if (ownerMxId == null || claimSenderId != ownerMxId) {
Timber.w("Mismatch senderId between event and olm owner")
return@post
}
forwardedKeysPerRoom
.getOrPut(roomId) { mutableMapOf() }
.getOrPut(ownerMxId) { mutableListOf() }
.add(ForwardInfo(event, localTimeStamp))
}
}
}
fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List<Event>) -> Unit) {
scope.launch {
sequencer.post {
// Prune outdated invites
recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS }
val cleanUpEvents = mutableListOf<Pair<String, String>>()
forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) ->
senderIdToForwardMap.forEach { (senderId, eventList) ->
// is there a matching invite in a valid timewindow?
val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId }
if (matchingInvite != null) {
Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}")
eventList.filter {
abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS
}.map {
it.event
}.takeIf { it.isNotEmpty() }?.let {
Timber.w("Re-processing forwarded_room_key_event that was not requested after invite")
scope.launch {
handleForwards.invoke(it)
}
}
cleanUpEvents.add(roomId to senderId)
}
}
}
cleanUpEvents.forEach { roomIdToSenderPair ->
forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear()
}
}
}
}
}

View file

@ -1,269 +0,0 @@
/*
* Copyright 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.olm
import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.convertFromUTF8
import timber.log.Timber
private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
internal class MXOlmDecryption(
// The olm device interface
private val olmDevice: MXOlmDevice,
// the matrix userId
private val userId: String
) :
IMXDecrypting {
@Throws(MXCryptoError::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
)
}
val cipherText = olmEventContent.ciphertext ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON
)
}
val senderKey = olmEventContent.senderKey ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.MISSING_SENDER_KEY,
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
)
}
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
}
// The message for myUser
@Suppress("UNCHECKED_CAST")
val message = messageAny as JsonDict
val decryptedPayload = decryptMessage(message, senderKey)
if (decryptedPayload == null) {
Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
}
val payloadString = convertFromUTF8(decryptedPayload)
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payload = adapter.fromJson(payloadString)
if (payload == null) {
Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
}
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
if (olmPayloadContent.recipient.isNullOrBlank()) {
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
}
if (olmPayloadContent.recipient != userId) {
Timber.tag(loggerTag.value).e(
"## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_RECIPIENT,
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)
)
}
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
Timber.tag(loggerTag.value).e(
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
" property; cannot prevent unknown-key attack"
)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
)
}
val ed25519 = recipientKeys["ed25519"]
if (ed25519 != olmDevice.deviceEd25519Key) {
Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
MXCryptoError.BAD_RECIPIENT_KEY_REASON
)
}
if (olmPayloadContent.sender.isNullOrBlank()) {
Timber.tag(loggerTag.value)
.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")
)
}
if (olmPayloadContent.sender != event.senderId) {
Timber.tag(loggerTag.value)
.e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.FORWARDED_MESSAGE,
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)
)
}
if (olmPayloadContent.roomId != event.roomId) {
Timber.tag(loggerTag.value)
.e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)
)
}
val keys = olmPayloadContent.keys ?: run {
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON
)
}
return MXEventDecryptionResult(
clearEvent = payload,
senderCurve25519Key = senderKey,
claimedEd25519Key = keys["ed25519"]
)
}
/**
* Attempt to decrypt an Olm message.
*
* @param message message object, with 'type' and 'body' fields.
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
* @return payload, if decrypted successfully.
*/
private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
val messageBody = message["body"] as? String ?: return null
val messageType = when (val typeAsVoid = message["type"]) {
is Double -> typeAsVoid.toInt()
is Int -> typeAsVoid
is Long -> typeAsVoid.toInt()
else -> return null
}
// Try each session in turn
// decryptionErrors = {};
val isPreKey = messageType == 0
// we want to synchronize on prekey if not we could end up create two olm sessions
// Not very clear but it looks like the js-sdk for consistency
return if (isPreKey) {
olmDevice.mutex.withLock {
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
}
} else {
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
}
}
private suspend fun reallyDecryptMessage(sessionIds: List<String>, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
for (sessionId in sessionIds) {
val payload = try {
olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
} catch (throwable: Exception) {
// As we are trying one by one, we don't really care of the error here
Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
null
}
if (null != payload) {
Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
return payload
} else {
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
if (foundSession) {
// Decryption failed, but it was a prekey message matching this
// session, so it should have worked.
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
return null
}
}
}
if (messageType != 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.isEmpty()) {
Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
} else {
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
}
return null
}
// prekey message which doesn't match any existing sessions: make a new
// session.
// XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey")
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
if (null == res) {
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
return null
}
Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
return res["payload"]
}
}

View file

@ -1,34 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.olm
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject
internal class MXOlmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
@UserId private val userId: String
) {
fun create(): MXOlmDecryption {
return MXOlmDecryption(
olmDevice,
userId
)
}
}

View file

@ -1,80 +0,0 @@
/*
* Copyright 2019 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms.olm
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
internal class MXOlmEncryption(
private val roomId: String,
private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val messageEncrypter: MessageEncrypter,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction
) :
IMXEncrypting {
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
// pick the list of recipients based on the membership list.
//
// TODO there is a race condition here! What if a new user turns up
ensureSession(userIds)
val deviceInfos = ArrayList<CryptoDeviceInfo>()
for (userId in userIds) {
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
for (device in devices) {
val key = device.identityKey()
if (key == olmDevice.deviceCurve25519Key) {
// Don't bother setting up session to ourself
continue
}
if (device.isBlocked) {
// Don't bother setting up sessions with blocked users
continue
}
deviceInfos.add(device)
}
}
val messageMap = mapOf(
"room_id" to roomId,
"type" to eventType,
"content" to eventContent
)
messageEncrypter.encryptMessage(messageMap, deviceInfos)
return messageMap.toContent()
}
/**
* Ensure that the session.
*
* @param users the user ids list
*/
private suspend fun ensureSession(users: List<String>) {
deviceListManager.downloadKeys(users, false)
ensureOlmSessionsForUsersAction.handle(users)
}
}

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.algorithms.olm
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import javax.inject.Inject
internal class MXOlmEncryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore,
private val messageEncrypter: MessageEncrypter,
private val deviceListManager: DeviceListManager,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction
) {
fun create(roomId: String): MXOlmEncryption {
return MXOlmEncryption(
roomId,
olmDevice,
cryptoStore,
messageEncrypter,
deviceListManager,
ensureOlmSessionsForUsersAction
)
}
}

View file

@ -1,93 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
data class Params(
val activeMemberUserIds: List<String>,
val isDirectRoom: Boolean
)
}
internal class DefaultComputeTrustTask @Inject constructor(
private val cryptoStore: IMXCryptoStore,
@UserId private val userId: String,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : ComputeTrustTask {
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) {
// The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (params.isDirectRoom) {
params.activeMemberUserIds.filter { it != userId }
} else {
params.activeMemberUserIds
}
val allTrustedUserIds = listToCheck
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { cryptoStore.getUserDeviceList(it) }
.flatten()
.let { allDevices ->
if (getMyCrossSigningKeys() != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method
allDevices.any { !it.isVerified }
}
}
.let { hasWarning ->
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (listToCheck.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted
} else {
RoomEncryptionTrustLevel.Default
}
}
}
}
}
private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(otherUserId)
}
private fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
}
}

View file

@ -1,91 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import javax.inject.Inject
/**
* Holds the OlmPkSigning for cross signing.
* Can be injected without having to get the full cross signing service
*/
@SessionScope
internal class CrossSigningOlm @Inject constructor(
private val cryptoStore: IMXCryptoStore,
) {
enum class KeyType {
SELF,
USER,
MASTER
}
var olmUtility: OlmUtility = OlmUtility()
var masterPkSigning: OlmPkSigning? = null
var userPkSigning: OlmPkSigning? = null
var selfSigningPkSigning: OlmPkSigning? = null
fun release() {
olmUtility.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
}
fun signObject(type: KeyType, strToSign: String): Map<String, String> {
val myKeys = cryptoStore.getMyCrossSigningInfo()
val pubKey = when (type) {
KeyType.SELF -> myKeys?.selfSigningKey()
KeyType.USER -> myKeys?.userKey()
KeyType.MASTER -> myKeys?.masterKey()
}?.unpaddedBase64PublicKey
val pkSigning = when (type) {
KeyType.SELF -> selfSigningPkSigning
KeyType.USER -> userPkSigning
KeyType.MASTER -> masterPkSigning
}
if (pubKey == null || pkSigning == null) {
throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning")
}
val signature = pkSigning.sign(strToSign)
return mapOf(
"ed25519:$pubKey" to signature
)
}
fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map<String, Map<String, String>>) {
val myKeys = cryptoStore.getMyCrossSigningInfo()
?: throw NoSuchElementException("Cross Signing not configured")
val myUserID = myKeys.userId
val pubKey = when (type) {
KeyType.SELF -> myKeys.selfSigningKey()
KeyType.USER -> myKeys.userKey()
KeyType.MASTER -> myKeys.masterKey()
}?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured")
val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me
?.get("ed25519:$pubKey")
require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" }
// Check that Alice USK signature of Bob MSK is valid
olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
}
}

View file

@ -1,819 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class DefaultCrossSigningService @Inject constructor(
@UserId private val myUserId: String,
@SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val initializeCrossSigningTask: InitializeCrossSigningTask,
private val uploadSignaturesTask: UploadSignaturesTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val crossSigningOlm: CrossSigningOlm,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
init {
try {
// Try to get stored keys if they exist
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
Timber.i("## CrossSigning - Found Existing self signed keys")
Timber.i("## CrossSigning - Checking if private keys are known")
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
privateKeysInfo.master
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading master key success")
} else {
Timber.w("## CrossSigning - Public master key does not match the private key")
pkSigning.releaseSigning()
// TODO untrust?
}
}
privateKeysInfo.user
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading User Signing key success")
} else {
Timber.w("## CrossSigning - Public User key does not match the private key")
pkSigning.releaseSigning()
// TODO untrust?
}
}
privateKeysInfo.selfSigned
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading Self Signing key success")
} else {
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
pkSigning.releaseSigning()
// TODO untrust?
}
}
}
// Recover local trust in case private key are there?
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
}
}
} catch (e: Throwable) {
// Mmm this kind of a big issue
Timber.e(e, "Failed to initialize Cross Signing")
}
deviceListManager.addListener(this)
}
fun release() {
crossSigningOlm.release()
deviceListManager.removeListener(this)
}
protected fun finalize() {
release()
}
/**
* - Make 3 key pairs (MSK, USK, SSK)
* - Save the private keys with proper security
* - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures.
*/
override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) {
Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params(
interactiveAuthInterceptor = uiaInterceptor
)
val data = initializeCrossSigningTask
.execute(params)
val crossSigningInfo = MXCrossSigningInfo(
myUserId,
listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo),
true
)
withContext(coroutineDispatchers.crypto) {
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
setUserKeysAsTrusted(myUserId, true)
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
}
}
override suspend fun onSecretMSKGossip(mskPrivateKey: String) {
Timber.i("## CrossSigning - onSecretSSKGossip")
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known")
}
mskPrivateKey.fromBase64()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading MSK success")
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
return
} else {
Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key")
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}")
pkSigning.releaseSigning()
}
}
}
override suspend fun onSecretSSKGossip(sskPrivateKey: String) {
Timber.i("## CrossSigning - onSecretSSKGossip")
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known")
}
sskPrivateKey.fromBase64()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading SSK success")
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
return
} else {
Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key")
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}")
pkSigning.releaseSigning()
}
}
}
override suspend fun onSecretUSKGossip(uskPrivateKey: String) {
Timber.i("## CrossSigning - onSecretUSKGossip")
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ")
}
uskPrivateKey.fromBase64()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading USK success")
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
return
} else {
Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key")
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
}
override suspend fun checkTrustFromPrivateKeys(
masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?
): UserTrustResult {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
var masterKeyIsTrusted = false
var userKeyIsTrusted = false
var selfSignedKeyIsTrusted = false
masterKeyPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
masterKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
uskKeyPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
userKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
sskPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
selfSignedKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
return UserTrustResult.Failure("Keys not trusted $mxCrossSigningInfo") // UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
} else {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
val checkSelfTrust = checkSelfTrust()
if (checkSelfTrust.isVerified()) {
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
setUserKeysAsTrusted(myUserId, true)
}
return checkSelfTrust
}
}
/**
*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK
*
*
* USK
* .
*/
override suspend fun isUserTrusted(otherUserId: String): Boolean {
return withContext(coroutineDispatchers.io) {
cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
}
}
override suspend fun isCrossSigningVerified(): Boolean {
return withContext(coroutineDispatchers.io) {
checkSelfTrust().isVerified()
}
}
/**
* Will not force a download of the key, but will verify signatures trust chain.
*/
override suspend fun checkUserTrust(otherUserId: String): UserTrustResult {
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
if (otherUserId == myUserId) {
return checkSelfTrust()
}
// I trust a user if I trust his master key
// I can trust the master key if it is signed by my user key
// TODO what if the master key is signed by a device key that i have verified
// First let's get my user key
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
}
override fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
if (!myCrossSigningInfo.isTrusted()) {
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
// Let's get the other user master key
val otherMasterKey = otherInfo?.masterKey()
?: return UserTrustResult.Failure("Unknown MSK for ${otherInfo?.userId}") // UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(myUserId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
return UserTrustResult.Failure("MSK not signed by my USK $otherMasterKey") // UserTrustResult.KeyNotSigned(otherMasterKey)
}
// Check that Alice USK signature of Bob MSK is valid
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
masterKeySignaturesMadeByMyUserKey,
myUserKey.unpaddedBase64PublicKey,
otherMasterKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.Failure("Invalid signature $masterKeySignaturesMadeByMyUserKey") // UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
}
return UserTrustResult.Success
}
private fun checkSelfTrust(): UserTrustResult {
// Special case when it's me,
// I have to check that MSK -> USK -> SSK
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId))
}
override fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
// Special case when it's me,
// I have to check that MSK -> USK -> SSK
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
val myMasterKey = myCrossSigningInfo?.masterKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
// Is the master key trusted
// 1) check if I know the private key
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
?.master
?.fromBase64()
var isMaterKeyTrusted = false
if (myMasterKey.trustLevel?.locallyVerified == true) {
isMaterKeyTrusted = true
} else if (masterPrivateKey != null) {
// Check if private match public
var olmPkSigning: OlmPkSigning? = null
try {
olmPkSigning = OlmPkSigning()
val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey)
isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK
} catch (failure: Throwable) {
Timber.e(failure)
}
olmPkSigning?.releaseSigning()
} else {
// Maybe it's signed by a locally trusted device?
myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) ->
val potentialDeviceId = key.removePrefix("ed25519:")
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity?
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
isMaterKeyTrusted = true
return@forEach
} catch (failure: Throwable) {
// log
Timber.w(failure, "Signature not valid?")
}
}
}
}
if (!isMaterKeyTrusted) {
return UserTrustResult.Failure("Keys not trusted $myCrossSigningInfo") // UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
val myUserKey = myCrossSigningInfo.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
?.get(myUserId) // Signatures made by me
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $myUserId, USK not signed by MSK")
return UserTrustResult.Failure("USK not signed by MSK") // UserTrustResult.KeyNotSigned(myUserKey)
}
// Check that Alice USK signature of Alice MSK is valid
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
userKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
myUserKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.Failure("Invalid MSK signature of USK") // UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
}
val mySSKey = myCrossSigningInfo.selfSigningKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
?.get(myUserId) // Signatures made by me
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $myUserId, SSK not signed by MSK")
return UserTrustResult.Failure("SSK not signed by MSK") // UserTrustResult.KeyNotSigned(mySSKey)
}
// Check that Alice USK signature of Alice MSK is valid
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
ssKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
mySSKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.Failure("Invalid signature $ssKeySignaturesMadeByMyMasterKey") // UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
}
return UserTrustResult.Success
}
override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return withContext(coroutineDispatchers.io) {
cryptoStore.getCrossSigningInfo(otherUserId)
}
}
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
return cryptoStore.getLiveCrossSigningInfo(userId)
}
override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return withContext(coroutineDispatchers.io) {
cryptoStore.getMyCrossSigningInfo()
}
}
override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
return withContext(coroutineDispatchers.io) {
cryptoStore.getCrossSigningPrivateKeys()
}
}
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
return cryptoStore.getLiveCrossSigningPrivateKeys()
}
override fun canCrossSign(): Boolean {
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null &&
cryptoStore.getCrossSigningPrivateKeys()?.user != null
}
override fun allPrivateKeysKnown(): Boolean {
return checkSelfTrust().isVerified() &&
cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
}
override suspend fun trustUser(otherUserId: String) {
withContext(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - Mark user $otherUserId as trusted ")
// We should have this user keys
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
if (otherMasterKeys == null) {
throw Throwable("## CrossSigning - Other master signing key is not known")
}
val myKeys = getUserCrossSigningKeys(myUserId)
?: throw Throwable("## CrossSigning - CrossSigning is not setup for this account")
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || crossSigningOlm.userPkSigning == null) {
throw Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")
}
// Sign the other MasterKey with our UserSigning key
val newSignature = JsonCanonicalizer.getCanonicalJson(
Map::class.java,
otherMasterKeys.signalableJSONDictionary()
).let { crossSigningOlm.userPkSigning?.sign(it) }
?: // race??
throw Throwable("## CrossSigning - Failed to sign")
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK")
val uploadQuery = UploadSignatureQueryBuilder()
.withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature))
.build()
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery))
// Local echo for device cross trust, to avoid having to wait for a notification of key change
cryptoStore.getUserDeviceList(otherUserId)?.forEach { device ->
val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}
override suspend fun markMyMasterKeyAsTrusted() {
withContext(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust()
// re-verify all trusts
onUsersDeviceUpdate(listOf(myUserId))
}
}
override suspend fun trustDevice(deviceId: String) {
withContext(coroutineDispatchers.crypto) {
// This device should be yours
val device = cryptoStore.getUserDevice(myUserId, deviceId)
?: throw IllegalArgumentException("This device [$deviceId] is not known, or not yours")
val myKeys = getUserCrossSigningKeys(myUserId)
?: throw Throwable("CrossSigning is not setup for this account")
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) {
throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")
}
// Sign with self signing
val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable())
?: throw Throwable("Failed to sign")
val toUpload = device.copy(
signatures = mapOf(
myUserId
to
mapOf(
"ed25519:$ssPubKey" to newSignature
)
)
)
val uploadQuery = UploadSignatureQueryBuilder()
.withDeviceInfo(toUpload)
.build()
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadQuery))
}
}
override suspend fun shieldForGroup(userIds: List<String>): RoomEncryptionTrustLevel {
// Not used in kotlin SDK?
TODO("Not yet implemented")
}
override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
val myKeys = getUserCrossSigningKeys(myUserId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
val otherKeys = getUserCrossSigningKeys(otherUserId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId))
// TODO should we force verification ?
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
// Check if the trust chain is valid
/*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*/
val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return legacyFallbackTrust(
locallyTrusted,
DeviceTrustResult.MissingDeviceSignature(
otherDeviceId, otherKeys.selfSigningKey()
?.unpaddedBase64PublicKey
?: ""
)
)
// Check bob's device is signed by bob's SSK
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
}
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
}
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
// TODO should we force verification ?
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
// Check if the trust chain is valid
/*
*
* ALICE BOB
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*/
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return legacyFallbackTrust(
locallyTrusted,
DeviceTrustResult.MissingDeviceSignature(
otherDevice.deviceId, otherKeys.selfSigningKey()
?.unpaddedBase64PublicKey
?: ""
)
)
// Check bob's device is signed by bob's SSK
try {
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
}
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
}
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
return if (locallyTrusted == true) {
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
} else {
crossSignTrustFail
}
}
override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
runBlocking {
checkTrustAndAffectedRoomShields(userIds)
}
}
override suspend fun checkTrustAndAffectedRoomShields(userIds: List<String>) {
Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}")
val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(userIds)
)
val workerData = WorkerParamsFactory.toData(workerParams)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
.setInputData(workerData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
workManagerProvider.workManager
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue()
}
private suspend fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == myUserId && currentTrust != trusted) {
// notify key requester
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
cryptoStore.updateUsersTrust {
users.add(it)
// called within a real transaction, has to block
runBlocking {
checkUserTrust(it).isVerified()
}
}
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}
}
}

View file

@ -1,28 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
internal fun CryptoDeviceInfo.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
}
internal fun CryptoCrossSigningKey.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
}

View file

@ -1,390 +0,0 @@
/*
* Copyright (c) 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
import org.matrix.android.sdk.internal.database.awaitTransaction
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
override val lastFailureMessage: String? = null,
// Kept for compatibility, but not used anymore (can be used for pending Worker)
val updatedUserIds: List<String>? = null,
// Passing a long list of userId can break the Work Manager due to data size limitation.
// so now we use a temporary file to store the data
val filename: String? = null
) : SessionWorkerParams
@Inject lateinit var crossSigningService: CrossSigningService
// It breaks the crypto store contract, but we need to batch things :/
@CryptoDatabase
@Inject lateinit var cryptoRealmConfiguration: RealmConfiguration
@SessionDatabase
@Inject lateinit var sessionRealmConfiguration: RealmConfiguration
@UserId
@Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@Inject lateinit var cryptoSessionInfoProvider: CryptoSessionInfoProvider
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
// @Inject lateinit var cryptoStore: IMXCryptoStore
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
val sId = myUserId.take(5)
Timber.v("## CrossSigning - UpdateTrustWorker started..")
val workerParams = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) }
?: return Result.success().also {
Timber.w("## CrossSigning - UpdateTrustWorker failed to get params")
cleanup(params)
}
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker userIds:${workerParams.userIds.logLimit()}, roomIds:${workerParams.roomIds.orEmpty().logLimit()}")
val userList = workerParams.userIds
// List should not be empty, but let's avoid go further in case of empty list
if (userList.isNotEmpty()) {
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/
Timber.v("## CrossSigning [$sId]- Updating trust for users: ${userList.logLimit()}")
updateTrust(userList)
}
val roomsToCheck = workerParams.roomIds ?: cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(userList)
Timber.v("## CrossSigning [$sId]- UpdateTrustWorker roomShield to check:${roomsToCheck.logLimit()}")
var myCrossSigningInfo: MXCrossSigningInfo?
Realm.getInstance(cryptoRealmConfiguration).use { realm ->
myCrossSigningInfo = getCrossSigningInfo(realm, myUserId)
}
// So Cross Signing keys trust is updated, device trust is updated
// We can now update room shields? in the session DB?
updateRoomShieldInSummaries(roomsToCheck, myCrossSigningInfo)
cleanup(params)
return Result.success()
}
private suspend fun updateTrust(userListParam: List<String>) {
val sId = myUserId.take(5)
var userList = userListParam
var myCrossSigningInfo: MXCrossSigningInfo?
// First we check that the users MSK are trusted by mine
// After that we check the trust chain for each devices of each users
awaitTransaction(cryptoRealmConfiguration) { cryptoRealm ->
// By mapping here to model, this object is not live
// I should update it if needed
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
var myTrustResult: UserTrustResult? = null
if (userList.contains(myUserId)) {
Timber.d("## CrossSigning [$sId]- Clear all trust as a change on my user was detected")
// i am in the list.. but i don't know exactly the delta of change :/
// If it's my cross signing keys we should refresh all trust
// do it anyway ?
userList = cryptoRealm.where(CrossSigningInfoEntity::class.java)
.findAll()
.mapNotNull { it.userId }
// check right now my keys and mark it as trusted as other trust depends on it
val myDevices = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, myUserId)
.findFirst()
?.devices
?.map { CryptoMapper.mapToModel(it) }
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices)
updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
// update model reference
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
}
val otherInfos = userList.associateWith { userId ->
getCrossSigningInfo(cryptoRealm, userId)
}
val trusts = otherInfos.mapValues { entry ->
when (entry.key) {
myUserId -> myTrustResult
else -> {
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
Timber.v("## CrossSigning [$sId]- user:${entry.key} result:$it")
}
}
}
}
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
// i have all the new trusts, update DB
trusts.forEach {
val verified = it.value?.isVerified() == true
Timber.v("[$myUserId] ## CrossSigning [$sId]- Updating user trust: ${it.key} to $verified")
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
}
// Ok so now we have to check device trust for all these users..
Timber.v("## CrossSigning [$sId]- Updating devices cross trust users: ${trusts.keys.logLimit()}")
trusts.keys.forEach { userId ->
val devicesEntities = cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?.devices
val trustMap = devicesEntities?.associateWith { device ->
runBlocking {
crossSigningService.checkDeviceTrust(userId, device.deviceId ?: "", CryptoMapper.mapToModel(device).trustLevel?.locallyVerified)
}
}
// Update trust if needed
devicesEntities?.forEach { device ->
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
Timber.v("## CrossSigning [$sId]- Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
Timber.d("## CrossSigning [$sId]- Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
// need to save
val trustEntity = device.trustLevelEntity
if (trustEntity == null) {
device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
it.locallyVerified = false
it.crossSignedVerified = crossSignedVerified
}
} else {
trustEntity.crossSignedVerified = crossSignedVerified
}
} else {
Timber.v("## CrossSigning [$sId]- Trust unchanged for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
}
}
}
}
}
private suspend fun updateRoomShieldInSummaries(roomList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
val sId = myUserId.take(5)
Timber.d("## CrossSigning [$sId]- Updating shields for impacted rooms... ${roomList.logLimit()}")
awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction")
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
roomList.forEach { roomId ->
Timber.v("## CrossSigning [$sId]- Checking room $roomId")
RoomSummaryEntity.where(sessionRealm, roomId)
// .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
.findFirst()
?.let { roomSummary ->
Timber.v("## CrossSigning [$sId]- Check shield state for room $roomId")
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
try {
val updatedTrust = computeRoomShield(
myCrossSigningInfo,
cryptoRealm,
allActiveRoomMembers,
roomSummary
)
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
Timber.d("## CrossSigning [$sId]- Shield change detected for $roomId -> $updatedTrust")
roomSummary.roomEncryptionTrustLevel = updatedTrust
} else {
Timber.v("## CrossSigning [$sId]- Shield unchanged for $roomId -> $updatedTrust")
}
} catch (failure: Throwable) {
Timber.e(failure)
}
}
}
}
}
Timber.d("## CrossSigning - Updating shields for impacted rooms - END")
}
private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
return cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
?.let { mapCrossSigningInfoEntity(it) }
}
private fun cleanup(params: Params) {
params.filename
?.let { updateTrustWorkerDataRepository.delete(it) }
}
private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) {
cryptoRealm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
.findFirst()
?.let { userKeyInfo ->
userKeyInfo
.crossSigningKeys
.forEach { key ->
// optimization to avoid trigger updates when there is no change..
if (key.trustLevelEntity?.isVerified() != verified) {
Timber.d("## CrossSigning - Trust change for $userId : $verified")
val level = key.trustLevelEntity
if (level == null) {
key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
it.locallyVerified = verified
it.crossSignedVerified = verified
}
} else {
level.locallyVerified = verified
level.crossSignedVerified = verified
}
}
}
if (verified) {
userKeyInfo.wasUserVerifiedOnce = true
}
}
}
private fun computeRoomShield(
myCrossSigningInfo: MXCrossSigningInfo?,
cryptoRealm: Realm,
activeMemberUserIds: List<String>,
roomSummaryEntity: RoomSummaryEntity
): RoomEncryptionTrustLevel {
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
// The set of “all users” depends on the type of room:
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) {
activeMemberUserIds.filter { it != myUserId }
} else {
activeMemberUserIds
}
val allTrustedUserIds = listToCheck
.filter { userId ->
getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
}
val resetTrust = listToCheck
.filter { userId ->
val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId)
crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true
}
return if (allTrustedUserIds.isEmpty()) {
if (resetTrust.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
RoomEncryptionTrustLevel.Warning
}
} else {
// If one of the verified user as an untrusted device -> warning
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { userId ->
cryptoRealm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
.findFirst()
?.devices
?.map { CryptoMapper.mapToModel(it) }
}
.flatten()
.let { allDevices ->
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}")
if (myCrossSigningInfo != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method
allDevices.any { !it.isVerified }
}
}
.let { hasWarning ->
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (resetTrust.isEmpty()) {
if (listToCheck.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted
} else {
RoomEncryptionTrustLevel.Default
}
} else {
RoomEncryptionTrustLevel.Warning
}
}
}
}
}
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
val userId = xsignInfo.userId ?: ""
return MXCrossSigningInfo(
userId = userId,
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
crossSigningKeysMapper.map(userId, it)
},
wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
)
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
/**
* Sent by Bob to accept a verification from a previously sent m.key.verification.start message.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationAccept(
/**
* string to identify the transaction.
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
* Alices device should record this ID and use it in future messages in this transaction.
*/
@Json(name = "transaction_id")
override val transactionId: String? = null,
/**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device.
*/
@Json(name = "key_agreement_protocol")
override val keyAgreementProtocol: String? = null,
/**
* The hash algorithm that Bobs device has selected to use, out of the list proposed by Alices device.
*/
@Json(name = "hash")
override val hash: String? = null,
/**
* The message authentication code that Bobs device has selected to use, out of the list proposed by Alices device.
*/
@Json(name = "message_authentication_code")
override val messageAuthenticationCode: String? = null,
/**
* An array of short authentication string methods that Bobs client (and Bob) understands. Must be a subset of the list proposed by Alices device.
*/
@Json(name = "short_authentication_string")
override val shortAuthenticationStrings: List<String>? = null,
/**
* The hash (encoded as unpadded base64) of the concatenation of the devices ephemeral public key (QB, encoded as unpadded base64)
* and the canonical JSON representation of the m.key.verification.start message.
*/
@Json(name = "commitment")
override var commitment: String? = null
) : SendToDeviceObject, VerificationInfoAccept {
override fun toSendToDeviceObject() = this
companion object : VerificationInfoAcceptFactory {
override fun create(
tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>
): VerificationInfoAccept {
return KeyVerificationAccept(
transactionId = tid,
keyAgreementProtocol = keyAgreementProtocol,
hash = hash,
commitment = commitment,
messageAuthenticationCode = messageAuthenticationCode,
shortAuthenticationStrings = shortAuthenticationStrings
)
}
}
}

View file

@ -1,57 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
/**
* To device event sent by either party to cancel a key verification.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationCancel(
/**
* the transaction ID of the verification to cancel.
*/
@Json(name = "transaction_id")
override val transactionId: String? = null,
/**
* machine-readable reason for cancelling, see #CancelCode.
*/
override val code: String? = null,
/**
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
*/
override val reason: String? = null
) : SendToDeviceObject, VerificationInfoCancel {
companion object {
fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel {
return KeyVerificationCancel(
tid,
cancelCode.value,
cancelCode.humanReadable
)
}
}
override fun toSendToDeviceObject() = this
}

View file

@ -1,32 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone
/**
* Requests a key verification with another user's devices.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationDone(
@Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoDone {
override fun toSendToDeviceObject() = this
}

View file

@ -1,48 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
/**
* Sent by both devices to send their ephemeral Curve25519 public key to the other device.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationKey(
/**
* The ID of the transaction that the message is part of.
*/
@Json(name = "transaction_id") override val transactionId: String? = null,
/**
* The devices ephemeral public key, as an unpadded base64 string.
*/
@Json(name = "key") override val key: String? = null
) : SendToDeviceObject, VerificationInfoKey {
companion object : VerificationInfoKeyFactory {
override fun create(tid: String, pubKey: String): KeyVerificationKey {
return KeyVerificationKey(tid, pubKey)
}
}
override fun toSendToDeviceObject() = this
}

View file

@ -1,42 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
/**
* Sent by both devices to send the MAC of their device key to the other device.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationMac(
@Json(name = "transaction_id") override val transactionId: String? = null,
@Json(name = "mac") override val mac: Map<String, String>? = null,
@Json(name = "keys") override val keys: String? = null
) : SendToDeviceObject, VerificationInfoMac {
override fun toSendToDeviceObject(): SendToDeviceObject? = this
companion object : VerificationInfoMacFactory {
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
return KeyVerificationMac(tid, mac, keys)
}
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
/**
* Requests a key verification with another user's devices.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationReady(
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>?,
@Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoReady {
override fun toSendToDeviceObject() = this
override fun toEventContent() = toContent()
}

View file

@ -1,38 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
/**
* Requests a key verification with another user's devices.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationRequest(
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>,
@Json(name = "timestamp") override val timestamp: Long?,
@Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoRequest {
override fun toSendToDeviceObject() = this
override fun toEventContent() = toContent()
}

View file

@ -1,45 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
/**
* Sent by Alice to initiate an interactive key verification.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationStart(
@Json(name = "from_device") override val fromDevice: String? = null,
@Json(name = "method") override val method: String? = null,
@Json(name = "transaction_id") override val transactionId: String? = null,
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
@Json(name = "hashes") override val hashes: List<String>? = null,
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null,
// For QR code verification
@Json(name = "secret") override val sharedSecret: String? = null
) : SendToDeviceObject, VerificationInfoStart {
override fun toCanonicalJson(): String {
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
}
override fun toSendToDeviceObject() = this
}

View file

@ -1,498 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
/**
* The crypto data store.
*/
internal interface IMXCryptoStore : IMXCommonCryptoStore {
/**
* @return the device id
*/
fun getDeviceId(): String
/**
* @return the olm account
*/
fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T
fun getOrCreateOlmAccount(): OlmAccount
/**
* Retrieve the known inbound group sessions.
*
* @return the list of all known group sessions, to export them.
*/
fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
/**
* Retrieve the known inbound group sessions for the specified room.
*
* @param roomId The roomId that the sessions will be returned
* @return the list of all known group sessions, for the provided roomId
*/
fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
/**
* Enable or disable key gossiping.
* Default is true.
* If set to false this device won't send key_request nor will accept key forwarded
*/
fun enableKeyGossiping(enable: Boolean)
fun isKeyGossipingEnabled(): Boolean
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun enableShareKeyOnInvite(enable: Boolean)
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun isShareKeysOnInviteEnabled(): Boolean
/**
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
*
* @return the room Ids list
*/
fun getRoomsListBlacklistUnverifiedDevices(): List<String>
/**
* Get the current keys backup version.
*/
fun getKeyBackupVersion(): String?
/**
* Set the current keys backup version.
*
* @param keyBackupVersion the keys backup version or null to delete it
*/
fun setKeyBackupVersion(keyBackupVersion: String?)
/**
* Get the current keys backup local data.
*/
fun getKeysBackupData(): KeysBackupDataEntity?
/**
* Set the keys backup local data.
*
* @param keysBackupData the keys backup local data, or null to erase data
*/
fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?)
/**
* @return the devices statuses map (userId -> tracking status)
*/
fun getDeviceTrackingStatuses(): Map<String, Int>
/**
* Indicate if the store contains data for the passed account.
*
* @return true means that the user enabled the crypto in a previous session
*/
fun hasData(): Boolean
/**
* Delete the crypto store for the passed credentials.
*/
fun deleteStore()
/**
* Store the device id.
*
* @param deviceId the device id
*/
fun storeDeviceId(deviceId: String)
/**
* Store the end to end account for the logged-in user.
*/
fun saveOlmAccount()
/**
* Retrieve a device for a user.
*
* @param userId the user's id.
* @param deviceId the device id.
* @return the device
*/
fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo?
/**
* Retrieve a device by its identity key.
*
* @param identityKey the device identity key (`MXDeviceInfo.identityKey`)
* @return the device or null if not found
*/
fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo?
/**
* Store the known devices for a user.
*
* @param userId The user's id.
* @param devices A map from device id to 'MXDevice' object for the device.
*/
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
/**
* Store the cross signing keys for the user userId.
*/
fun storeUserIdentity(
userId: String,
userIdentity: UserIdentity
)
/**
* Retrieve the known devices for a user.
*
* @param userId The user's id.
* @return The devices map if some devices are known, else null
*/
fun getUserDevices(userId: String): Map<String, CryptoDeviceInfo>?
fun getUserDeviceList(userId: String): List<CryptoDeviceInfo>?
// fun getUserDeviceListFlow(userId: String): Flow<List<CryptoDeviceInfo>>
fun getLiveDeviceList(userId: String): LiveData<List<CryptoDeviceInfo>>
fun getLiveDeviceList(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
// TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>>
/**
* Store the crypto algorithm for a room.
*
* @param roomId the id of the room.
* @param algorithm the algorithm.
*/
fun storeRoomAlgorithm(roomId: String, algorithm: String?)
fun shouldShareHistory(roomId: String): Boolean
/**
* Store a session between the logged-in user and another device.
*
* @param olmSessionWrapper the end-to-end session.
* @param deviceKey the public key of the other device.
*/
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
/**
* Retrieve all end-to-end session ids between our own device and another
* device.
*
* @param deviceKey the public key of the other device.
* @return A set of sessionId, or null if device is not known
*/
fun getDeviceSessionIds(deviceKey: String): List<String>?
/**
* Retrieve an end-to-end session between our own device and another
* device.
*
* @param sessionId the session Id.
* @param deviceKey the public key of the other device.
* @return The Base64 end-to-end session, or null if not found
*/
fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper?
/**
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist.
*
* @param deviceKey the public key of the other device.
* @return last used sessionId, or null if not found
*/
fun getLastUsedSessionId(deviceKey: String): String?
/**
* Store inbound group sessions.
*
* @param sessions the inbound group sessions to store.
*/
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
/**
* Retrieve an inbound group session, filtering shared history.
*
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @param sharedHistory filter inbound session with respect to shared history field
* @return an inbound group session.
*/
fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper?
/**
* Get the current outbound group session for this encrypted room.
*/
fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper?
/**
* Get the current outbound group session for this encrypted room.
*/
fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?)
/**
* Remove an inbound group session.
*
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
*/
fun removeInboundGroupSession(sessionId: String, senderKey: String)
/* ==========================================================================================
* Keys backup
* ========================================================================================== */
/**
* Mark all inbound group sessions as not backed up.
*/
fun resetBackupMarkers()
/**
* Mark inbound group sessions as backed up on the user homeserver.
*
* @param olmInboundGroupSessionWrappers the sessions
*/
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
/**
* Retrieve inbound group sessions that are not yet backed up.
*
* @param limit the maximum number of sessions to return.
* @return an array of non backed up inbound group sessions.
*/
fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
/**
* Number of stored inbound group sessions.
*
* @param onlyBackedUp if true, count only session marked as backed up.
* @return a count.
*/
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
/**
* Save the device statuses.
*
* @param deviceTrackingStatuses the device tracking statuses
*/
fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map<String, Int>)
/**
* Get the tracking status of a specified userId devices.
*
* @param userId the user id
* @param defaultValue the default value
* @return the tracking status
*/
fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int
/**
* Look for an existing outgoing room key request, and if none is found, return null.
*
* @param requestBody the request body
* @return an OutgoingRoomKeyRequest instance or null
*/
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
/**
* Look for an existing outgoing room key request, and if none is found, add a new one.
*
* @param requestBody the request
* @param recipients list of recipients
* @param fromIndex start index
* @return either the same instance as passed in, or the existing one.
*/
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int): OutgoingKeyRequest
fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState)
fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int)
fun updateOutgoingRoomKeyReply(
roomId: String,
sessionId: String,
algorithm: String,
senderKey: String,
fromDevice: String?,
event: Event
)
fun deleteOutgoingRoomKeyRequest(requestId: String)
fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState)
fun saveIncomingKeyRequestAuditTrail(
requestId: String,
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
fromUser: String,
fromDevice: String
)
fun saveWithheldAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
code: WithHeldCode,
userId: String,
deviceId: String
)
fun saveForwardKeyAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?
)
fun saveIncomingForwardKeyAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?
)
fun addNewSessionListener(listener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)
// =============================================
// CROSS SIGNING
// =============================================
/**
* Gets the current crosssigning info.
*/
fun getMyCrossSigningInfo(): MXCrossSigningInfo?
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
fun getCrossSigningInfo(userId: String): MXCrossSigningInfo?
fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>>
// fun getCrossSigningInfoFlow(userId: String): Flow<Optional<MXCrossSigningInfo>>
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
fun storeMSKPrivateKey(msk: String?)
fun storeSSKPrivateKey(ssk: String?)
fun storeUSKPrivateKey(usk: String?)
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
// fun getCrossSigningPrivateKeysFlow(): Flow<Optional<PrivateKeysInfo>>
fun getGlobalCryptoConfig(): GlobalCryptoConfig
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?)
fun clearOtherUserTrust()
fun updateUsersTrust(check: (String) -> Boolean)
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
fun markedSessionAsShared(
roomId: String?,
sessionId: String,
userId: String,
deviceId: String,
deviceIdentityKey: String,
chainIndex: Int
)
/**
* Query for information on this session sharing history.
* @return SharedSessionResult
* if found is true then this session was initialy shared with that user|device,
* in this case chainIndex is not nullindicates the ratchet position.
* In found is false, chainIndex is null
*/
fun getSharedSessionInfo(roomId: String?, sessionId: String, deviceInfo: CryptoDeviceInfo): SharedSessionResult
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
// Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
fun getGossipingEvents(): List<AuditTrail>
fun setDeviceKeysUploaded(uploaded: Boolean)
fun areDeviceKeysUploaded(): Boolean
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
/**
* Store a bunch of data related to the users. @See [UserDataToStore].
*/
fun storeData(userDataToStore: UserDataToStore)
}

View file

@ -1,87 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo001Legacy
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo002Legacy
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo003RiotX
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo004
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo005
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo006
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo007
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo008
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo009
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo010
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo011
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo020
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo021
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
/**
* Schema version history:
* 0, 1, 2: legacy Riot-Android;
* 3: migrate to RiotX schema;
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6).
*/
internal class RealmCryptoStoreMigration @Inject constructor(
private val clock: Clock,
) : MatrixRealmMigration(
dbName = "Crypto",
schemaVersion = 21L,
) {
/**
* Forces all RealmCryptoStoreMigration instances to be equal.
* Avoids Realm throwing when multiple instances of the migration are set.
*/
override fun equals(other: Any?) = other is RealmCryptoStoreMigration
override fun hashCode() = 5000
override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
if (oldVersion < 1) MigrateCryptoTo001Legacy(realm).perform()
if (oldVersion < 2) MigrateCryptoTo002Legacy(realm).perform()
if (oldVersion < 3) MigrateCryptoTo003RiotX(realm).perform()
if (oldVersion < 4) MigrateCryptoTo004(realm).perform()
if (oldVersion < 5) MigrateCryptoTo005(realm).perform()
if (oldVersion < 6) MigrateCryptoTo006(realm).perform()
if (oldVersion < 7) MigrateCryptoTo007(realm).perform()
if (oldVersion < 8) MigrateCryptoTo008(realm, clock).perform()
if (oldVersion < 9) MigrateCryptoTo009(realm).perform()
if (oldVersion < 10) MigrateCryptoTo010(realm).perform()
if (oldVersion < 11) MigrateCryptoTo011(realm).perform()
if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
if (oldVersion < 19) MigrateCryptoTo019(realm).perform()
if (oldVersion < 20) MigrateCryptoTo020(realm).perform()
if (oldVersion < 21) MigrateCryptoTo021(realm).perform()
}
}

View file

@ -1,190 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.tasks
import dagger.Lazy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
import org.matrix.android.sdk.api.session.uia.UiaResult
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import timber.log.Timber
import javax.inject.Inject
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params(
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
)
data class Result(
val masterKeyPK: String,
val userKeyPK: String,
val selfSigningKeyPK: String,
val masterKeyInfo: CryptoCrossSigningKey,
val userKeyInfo: CryptoCrossSigningKey,
val selfSignedKeyInfo: CryptoCrossSigningKey
)
}
internal class DefaultInitializeCrossSigningTask @Inject constructor(
@UserId private val userId: String,
private val olmDevice: MXOlmDevice,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask
) : InitializeCrossSigningTask {
override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result {
var masterPkOlm: OlmPkSigning? = null
var userSigningPkOlm: OlmPkSigning? = null
var selfSigningPkOlm: OlmPkSigning? = null
try {
// =================
// MASTER KEY
// =================
masterPkOlm = OlmPkSigning()
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
// =================
// USER KEY
// =================
userSigningPkOlm = OlmPkSigning()
val uskPrivateKey = OlmPkSigning.generateSeed()
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
// Sign userSigningKey with master
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.build()
.canonicalSignable()
.let { masterPkOlm.sign(it) }
// =================
// SELF SIGNING KEY
// =================
selfSigningPkOlm = OlmPkSigning()
val sskPrivateKey = OlmPkSigning.generateSeed()
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
// Sign selfSigningKey with master
val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.build()
.canonicalSignable()
.let { masterPkOlm.sign(it) }
// I need to upload the keys
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
.key(masterPublicKey)
.build()
val uploadSigningKeysParams = UploadSigningKeysTask.Params(
masterKey = mskCrossSigningKeyInfo,
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.signature(userId, masterPublicKey, signedUSK)
.build(),
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.signature(userId, masterPublicKey, signedSSK)
.build(),
userAuthParam = null
// userAuthParam = params.authParams
)
try {
uploadSigningKeysTask.execute(uploadSigningKeysParams)
} catch (failure: Throwable) {
if (params.interactiveAuthInterceptor == null ||
handleUIA(
failure = failure,
interceptor = params.interactiveAuthInterceptor,
retryBlock = { authUpdate ->
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
}
) != UiaResult.SUCCESS
) {
Timber.d("## UIA: propagate failure")
throw failure
}
}
// Sign the current device with SSK
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
val myDevice = myDeviceInfoHolder.get().myDevice
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
.also {
it[userId] = (it[userId]
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
}
myDevice.copy(signatures = updateSignatures).let {
uploadSignatureQueryBuilder.withDeviceInfo(it)
}
// sign MSK with device key (migration) and upload signatures
val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
olmDevice.signMessage(message)?.let { sign ->
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
?: HashMap()).also {
it[userId] = (it[userId]
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
}
mskCrossSigningKeyInfo.copy(
signatures = mskUpdatedSignatures
).let {
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
}
}
// TODO should we ignore failure of that?
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build()))
return InitializeCrossSigningTask.Result(
masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(),
userKeyPK = uskPrivateKey.toBase64NoPadding(),
selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(),
masterKeyInfo = uploadSigningKeysParams.masterKey,
userKeyInfo = uploadSigningKeysParams.userKey,
selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey
)
} finally {
masterPkOlm?.releaseSigning()
userSigningPkOlm?.releaseSigning()
selfSigningPkOlm?.releaseSigning()
}
}
}

View file

@ -1,82 +0,0 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString
internal class KotlinQRVerification(
private val channel: Channel<VerificationIntent>,
var qrCodeData: QrCodeData?,
override val method: VerificationMethod,
override val transactionId: String,
override val otherUserId: String,
override val otherDeviceId: String?,
override val isIncoming: Boolean,
var state: QRCodeVerificationState,
val isToDevice: Boolean
) : QrCodeVerificationTransaction {
override fun state() = state
override val qrCodeText: String?
get() = qrCodeData?.toEncodedString()
//
// var userMSKKeyToTrust: String? = null
// var deviceKeysToTrust = mutableListOf<String>()
// override suspend fun userHasScannedOtherQrCode(otherQrCodeText: String) {
// TODO("Not yet implemented")
// }
override suspend fun otherUserScannedMyQrCode() {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionConfirmCodeWasScanned(otherUserId, transactionId, deferred)
)
deferred.await()
}
override suspend fun otherUserDidNotScannedMyQrCode() {
val deferred = CompletableDeferred<Unit>()
channel.send(
// TODO what cancel code??
VerificationIntent.ActionCancel(transactionId, deferred)
)
deferred.await()
}
override suspend fun cancel() {
cancel(CancelCode.User)
}
override suspend fun cancel(code: CancelCode) {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionCancel(transactionId, deferred)
)
deferred.await()
}
override fun isToDeviceTransport() = isToDevice
}

View file

@ -1,476 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V1
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KEY_AGREEMENT_V2
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_AGREEMENT_PROTOCOLS
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_HASHES
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_MACS
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.KNOWN_SHORT_CODES
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction.Companion.SAS_MAC_SHA256_LONGKDF
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationAcceptContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationKeyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationMacContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationKey
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationMac
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationReady
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import org.matrix.olm.OlmSAS
import timber.log.Timber
import java.util.Locale
internal class KotlinSasTransaction(
private val channel: Channel<VerificationIntent>,
override val transactionId: String,
override val otherUserId: String,
private val myUserId: String,
private val myTrustedMSK: String?,
override var otherDeviceId: String?,
private val myDeviceId: String,
private val myDeviceFingerprint: String,
override val isIncoming: Boolean,
val startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null,
val isToDevice: Boolean,
var state: SasTransactionState,
val olmSAS: OlmSAS,
) : SasVerificationTransaction {
override val method: VerificationMethod
get() = VerificationMethod.SAS
companion object {
fun sasStart(inRoom: Boolean, fromDevice: String, requestId: String): VerificationInfoStart {
return if (inRoom) {
MessageVerificationStartContent(
fromDevice = fromDevice,
hashes = KNOWN_HASHES,
keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS,
messageAuthenticationCodes = KNOWN_MACS,
shortAuthenticationStrings = KNOWN_SHORT_CODES,
method = VERIFICATION_METHOD_SAS,
relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = requestId
),
sharedSecret = null
)
} else {
KeyVerificationStart(
fromDevice,
VERIFICATION_METHOD_SAS,
requestId,
KNOWN_AGREEMENT_PROTOCOLS,
KNOWN_HASHES,
KNOWN_MACS,
KNOWN_SHORT_CODES,
null
)
}
}
fun sasAccept(
inRoom: Boolean,
requestId: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>,
): VerificationInfoAccept {
return if (inRoom) {
MessageVerificationAcceptContent.create(
requestId,
keyAgreementProtocol,
hash,
commitment,
messageAuthenticationCode,
shortAuthenticationStrings
)
} else {
KeyVerificationAccept.create(
requestId,
keyAgreementProtocol,
hash,
commitment,
messageAuthenticationCode,
shortAuthenticationStrings
)
}
}
fun sasReady(
inRoom: Boolean,
requestId: String,
methods: List<String>,
fromDevice: String,
): VerificationInfoReady {
return if (inRoom) {
MessageVerificationReadyContent.create(
requestId,
methods,
fromDevice,
)
} else {
KeyVerificationReady(
fromDevice = fromDevice,
methods = methods,
transactionId = requestId,
)
}
}
fun sasKeyMessage(
inRoom: Boolean,
requestId: String,
pubKey: String,
): VerificationInfoKey {
return if (inRoom) {
MessageVerificationKeyContent.create(tid = requestId, pubKey = pubKey)
} else {
KeyVerificationKey.create(tid = requestId, pubKey = pubKey)
}
}
fun sasMacMessage(
inRoom: Boolean,
requestId: String,
validVerificationInfoMac: ValidVerificationInfoMac
): VerificationInfoMac {
return if (inRoom) {
MessageVerificationMacContent.create(
tid = requestId,
keys = validVerificationInfoMac.keys,
mac = validVerificationInfoMac.mac
)
} else {
KeyVerificationMac.create(
tid = requestId,
keys = validVerificationInfoMac.keys,
mac = validVerificationInfoMac.mac
)
}
}
}
override fun toString(): String {
return "KotlinSasTransaction(transactionId=$transactionId, state=$state, otherUserId=$otherUserId, otherDeviceId=$otherDeviceId, isToDevice=$isToDevice)"
}
// To override finalize(), all you need to do is simply declare it, without using the override keyword:
protected fun finalize() {
releaseSAS()
}
private fun releaseSAS() {
// finalization logic
olmSAS.releaseSas()
}
var accepted: ValidVerificationInfoAccept? = null
var otherKey: String? = null
var shortCodeBytes: ByteArray? = null
var myMac: ValidVerificationInfoMac? = null
var theirMac: ValidVerificationInfoMac? = null
var verifiedSuccessInfo: MacVerificationResult.Success? = null
override fun state() = this.state
// override fun supportsEmoji(): Boolean {
// return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true
// }
override fun getEmojiCodeRepresentation(): List<EmojiRepresentation> {
return shortCodeBytes?.getEmojiCodeRepresentation().orEmpty()
}
override fun getDecimalCodeRepresentation(): String? {
return shortCodeBytes?.getDecimalCodeRepresentation()
}
override suspend fun userHasVerifiedShortCode() {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionSASCodeMatches(transactionId, deferred)
)
deferred.await()
}
override suspend fun acceptVerification() {
// nop
// as we are using verification request accept is automatic
}
override suspend fun shortCodeDoesNotMatch() {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionSASCodeDoesNotMatch(transactionId, deferred)
)
deferred.await()
}
override suspend fun cancel() {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionCancel(transactionId, deferred)
)
deferred.await()
}
override suspend fun cancel(code: CancelCode) {
val deferred = CompletableDeferred<Unit>()
channel.send(
VerificationIntent.ActionCancel(transactionId, deferred)
)
deferred.await()
}
override fun isToDeviceTransport() = isToDevice
fun calculateSASBytes(otherKey: String) {
this.otherKey = otherKey
olmSAS.setTheirPublicKey(otherKey)
shortCodeBytes = when (accepted!!.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = buildString {
append("MATRIX_KEY_VERIFICATION_SAS")
if (isIncoming) {
append(otherUserId)
append(otherDeviceId)
append(myUserId)
append(myDeviceId)
append(olmSAS.publicKey)
} else {
append(myUserId)
append(myDeviceId)
append(otherUserId)
append(otherDeviceId)
}
append(transactionId)
}
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
olmSAS.generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
val sasInfo = buildString {
append("MATRIX_KEY_VERIFICATION_SAS|")
if (isIncoming) {
append(otherUserId).append('|')
append(otherDeviceId).append('|')
append(otherKey).append('|')
append(myUserId).append('|')
append(myDeviceId).append('|')
append(olmSAS.publicKey).append('|')
} else {
append(myUserId).append('|')
append(myDeviceId).append('|')
append(olmSAS.publicKey).append('|')
append(otherUserId).append('|')
append(otherDeviceId).append('|')
append(otherKey).append('|')
}
append(transactionId)
}
olmSAS.generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
throw IllegalArgumentException()
}
}
}
fun computeMyMac(): ValidVerificationInfoMac {
val baseInfo = buildString {
append("MATRIX_KEY_VERIFICATION_MAC")
append(myUserId)
append(myDeviceId)
append(otherUserId)
append(otherDeviceId)
append(transactionId)
}
// Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key.
// It should now contain both the device key and the MSK.
// So when Alice and Bob verify with SAS, the verification will verify the MSK.
val keyMap = HashMap<String, String>()
val keyId = "ed25519:$myDeviceId"
val macString = macUsingAgreedMethod(myDeviceFingerprint, baseInfo + keyId)
if (macString.isNullOrBlank()) {
// Should not happen
Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.")
throw IllegalStateException("Invalid mac for transaction ${transactionId}")
}
keyMap[keyId] = macString
if (myTrustedMSK != null) {
val crossSigningKeyId = "ed25519:$myTrustedMSK"
macUsingAgreedMethod(myTrustedMSK, baseInfo + crossSigningKeyId)?.let { mskMacString ->
keyMap[crossSigningKeyId] = mskMacString
}
}
val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS")
if (keyStrings.isNullOrBlank()) {
// Should not happen
Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.")
throw IllegalStateException("Invalid key mac for transaction ${transactionId}")
}
return ValidVerificationInfoMac(
transactionId,
keyMap,
keyStrings
).also {
myMac = it
}
}
sealed class MacVerificationResult {
object MismatchKeys : MacVerificationResult()
data class MismatchMacDevice(val deviceId: String) : MacVerificationResult()
object MismatchMacCrossSigning : MacVerificationResult()
object NoDevicesVerified : MacVerificationResult()
data class Success(val verifiedDeviceId: List<String>, val otherMskTrusted: Boolean) : MacVerificationResult()
}
fun verifyMacs(
theirMacSafe: ValidVerificationInfoMac,
otherUserKnownDevices: List<CryptoDeviceInfo>,
otherMasterKey: String?
): MacVerificationResult {
Timber.v("## SAS verifying macs for id:$transactionId")
// Bobs device calculates the HMAC (as above) of its copies of Alices keys given in the message (as identified by their key ID),
// as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message.
// Bobs device compares these with the HMAC values given in the m.key.verification.mac message.
// If everything matches, then consider Alices device keys as verified.
val baseInfo = buildString {
append("MATRIX_KEY_VERIFICATION_MAC")
append(otherUserId)
append(otherDeviceId)
append(myUserId)
append(myDeviceId)
append(transactionId)
}
val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",")
val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS")
if (theirMacSafe.keys != keyStrings) {
// WRONG!
return MacVerificationResult.MismatchKeys
}
val verifiedDevices = ArrayList<String>()
// cannot be empty because it has been validated
theirMacSafe.mac.keys.forEach { entry ->
val keyIDNoPrefix = entry.removePrefix("ed25519:")
val otherDeviceKey = otherUserKnownDevices
.firstOrNull { it.deviceId == keyIDNoPrefix }
?.fingerprint()
if (otherDeviceKey == null) {
Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
// just ignore and continue
return@forEach
}
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + entry)
if (mac != theirMacSafe.mac[entry]) {
// WRONG!
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
// cancel(CancelCode.MismatchedKeys)
return MacVerificationResult.MismatchMacDevice(keyIDNoPrefix)
}
verifiedDevices.add(keyIDNoPrefix)
}
var otherMasterKeyIsVerified = false
if (otherMasterKey != null) {
// Did the user signed his master key
theirMacSafe.mac.keys.forEach {
val keyIDNoPrefix = it.removePrefix("ed25519:")
if (keyIDNoPrefix == otherMasterKey) {
// Check the signature
val mac = macUsingAgreedMethod(otherMasterKey, baseInfo + it)
if (mac != theirMacSafe.mac[it]) {
// WRONG!
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
return MacVerificationResult.MismatchMacCrossSigning
} else {
otherMasterKeyIsVerified = true
}
}
}
}
// if none of the keys could be verified, then error because the app
// should be informed about that
if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) {
Timber.e("## SAS Verification: No devices verified")
return MacVerificationResult.NoDevicesVerified
}
return MacVerificationResult.Success(
verifiedDevices,
otherMasterKeyIsVerified
).also {
// store and will persist when transaction is actually done
verifiedSuccessInfo = it
}
}
private fun macUsingAgreedMethod(message: String, info: String): String? {
return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) {
SAS_MAC_SHA256_LONGKDF -> olmSAS.calculateMacLongKdf(message, info)
SAS_MAC_SHA256 -> olmSAS.calculateMac(message, info)
else -> null
}
}
}

View file

@ -1,131 +0,0 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import org.matrix.android.sdk.internal.crypto.verification.qrcode.QrCodeData
import org.matrix.android.sdk.internal.crypto.verification.qrcode.toEncodedString
internal class KotlinVerificationRequest(
val requestId: String,
val incoming: Boolean,
val otherUserId: String,
var state: EVerificationState,
val ageLocalTs: Long
) {
var roomId: String? = null
var qrCodeData: QrCodeData? = null
var targetDevices: List<String>? = null
var requestInfo: ValidVerificationInfoRequest? = null
var readyInfo: ValidVerificationInfoReady? = null
var cancelCode: CancelCode? = null
// fun requestId() = requestId
//
// fun incoming() = incoming
//
// fun otherUserId() = otherUserId
//
// fun roomId() = roomId
//
// fun targetDevices() = targetDevices
//
// fun state() = state
//
// fun ageLocalTs() = ageLocalTs
fun otherDeviceId(): String? {
return if (incoming) {
requestInfo?.fromDevice
} else {
readyInfo?.fromDevice
}
}
fun cancelCode(): CancelCode? = cancelCode
/**
* SAS is supported if I support it and the other party support it.
*/
private fun isSasSupported(): Boolean {
return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse()
}
/**
* Other can show QR code if I can scan QR code and other can show QR code.
*/
private fun otherCanShowQrCode(): Boolean {
return if (incoming) {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse()
} else {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse()
}
}
/**
* Other can scan QR code if I can show QR code and other can scan QR code.
*/
private fun otherCanScanQrCode(): Boolean {
return if (incoming) {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse()
} else {
requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() &&
readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse()
}
}
fun qrCodeText() = qrCodeData?.toEncodedString()
override fun toString(): String {
return toPendingVerificationRequest().toString()
}
fun toPendingVerificationRequest(): PendingVerificationRequest {
return PendingVerificationRequest(
ageLocalTs = ageLocalTs,
state = state,
isIncoming = incoming,
otherUserId = otherUserId,
roomId = roomId,
transactionId = requestId,
cancelConclusion = cancelCode,
isFinished = isFinished(),
handledByOtherSession = state == EVerificationState.HandledByOtherSession,
targetDevices = targetDevices,
qrCodeText = qrCodeText(),
isSasSupported = isSasSupported(),
weShouldShowScanOption = otherCanShowQrCode(),
weShouldDisplayQRCode = otherCanScanQrCode(),
otherDeviceId = otherDeviceId()
)
}
fun isFinished() = state == EVerificationState.Cancelled || state == EVerificationState.Done
}

View file

@ -1,35 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility
import org.matrix.olm.OlmSAS
import javax.inject.Inject
// Mainly for testing purpose to ease mocking
internal class VerificationCryptoPrimitiveProvider @Inject constructor() {
fun provideOlmSas(): OlmSAS {
return OlmSAS()
}
fun sha256(toHash: String): String {
return withOlmUtility {
it.sha256(toHash)
}
}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
import org.matrix.android.sdk.api.session.events.model.Content
internal interface VerificationInfo<ValidObjectType> {
fun toEventContent(): Content? = null
fun toSendToDeviceObject(): SendToDeviceObject? = null
fun asValidObject(): ValidObjectType?
/**
* String to identify the transaction.
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
* Alices device should record this ID and use it in future messages in this transaction.
*/
val transactionId: String?
}

View file

@ -1,83 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
internal interface VerificationInfoAccept : VerificationInfo<ValidVerificationInfoAccept> {
/**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device.
*/
val keyAgreementProtocol: String?
/**
* The hash algorithm that Bobs device has selected to use, out of the list proposed by Alices device.
*/
val hash: String?
/**
* The message authentication code that Bobs device has selected to use, out of the list proposed by Alices device.
*/
val messageAuthenticationCode: String?
/**
* An array of short authentication string methods that Bobs client (and Bob) understands. Must be a subset of the list proposed by Alices device.
*/
val shortAuthenticationStrings: List<String>?
/**
* The hash (encoded as unpadded base64) of the concatenation of the devices ephemeral public key (QB, encoded as unpadded base64)
* and the canonical JSON representation of the m.key.verification.start message.
*/
var commitment: String?
override fun asValidObject(): ValidVerificationInfoAccept? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null
val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null
val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null
val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null
val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoAccept(
validTransactionId,
validKeyAgreementProtocol,
validHash,
validMessageAuthenticationCode,
validShortAuthenticationStrings,
validCommitment
)
}
}
internal interface VerificationInfoAcceptFactory {
fun create(
tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>
): VerificationInfoAccept
}
internal data class ValidVerificationInfoAccept(
val transactionId: String,
val keyAgreementProtocol: String,
val hash: String,
val messageAuthenticationCode: String,
val shortAuthenticationStrings: List<String>,
var commitment: String?
)

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