Merge branch 'develop' into feature/ons/poll_tests
* develop: (150 commits) Removes changelog file Fix PR comment Adds changelog file Refactors MessageBubbleView Updating changelog copy making use of the fake overrides for testing extracting the personalization complete emitting to a dedicated function making use of binding api instead of manual findviewbyid using consistent method naming for setting the capabilities override taking the personalization feature flag into account when calculating if personalization is supported - also removes a legacy loading workaround for the account creation step, we're navigating to a new screen AccountCreated so we have to stop the loading adding changelog entry using correct label for the avatar capability debug override forwarding to the profile picture flow when display name changing isn't supported but pictures are when personalising the profile formatting dynamically switching the onboarding flow based on the capabilities of the homeserver - when avatars can't be changed we complete the personlisation flow hiding the toolbar back button and handling system back as take the user home if the display name personalisation is not supported adding test around account creation via dummy dynamically changing the account created layout based on if the homeserver supports personalisation adding entry points for injecting and overriding the homeserver capabilities extracting method for the handling of the profile picture selection ...
This commit is contained in:
commit
dd3178c36e
|
@ -14,50 +14,6 @@ env:
|
||||||
-Porg.gradle.jvmargs=-Xmx4g
|
-Porg.gradle.jvmargs=-Xmx4g
|
||||||
-Porg.gradle.parallel=false
|
-Porg.gradle.parallel=false
|
||||||
jobs:
|
jobs:
|
||||||
# Build Android Tests [Matrix SDK]
|
|
||||||
build-android-test-matrix-sdk:
|
|
||||||
name: Matrix SDK - Build Android Tests
|
|
||||||
runs-on: macos-latest
|
|
||||||
# No concurrency required, runs every time on a schedule.
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
distribution: 'adopt'
|
|
||||||
java-version: 11
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Build Android Tests for matrix-sdk-android
|
|
||||||
run: ./gradlew clean matrix-sdk-android:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
|
||||||
|
|
||||||
# Build Android Tests [Matrix APP]
|
|
||||||
build-android-test-app:
|
|
||||||
name: App - Build Android Tests
|
|
||||||
runs-on: macos-latest
|
|
||||||
# No concurrency required, runs every time on a schedule.
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
distribution: 'adopt'
|
|
||||||
java-version: 11
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Build Android Tests for vector
|
|
||||||
run: ./gradlew clean vector:assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
|
||||||
|
|
||||||
# Run Android Tests
|
# Run Android Tests
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Matrix SDK - Running Integration Tests
|
name: Matrix SDK - Running Integration Tests
|
||||||
|
@ -87,11 +43,11 @@ jobs:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Start synapse server
|
- name: Start synapse server
|
||||||
run: |
|
uses: michaelkaye/setup-matrix-synapse@v0.3.0
|
||||||
pip install matrix-synapse
|
with:
|
||||||
curl https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh -o start.sh
|
uploadLogs: true
|
||||||
chmod 777 start.sh
|
httpPort: 8080
|
||||||
./start.sh --no-rate-limit
|
disableRateLimiting: true
|
||||||
# package: org.matrix.android.sdk.session
|
# package: org.matrix.android.sdk.session
|
||||||
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
- name: Run integration tests for Matrix SDK [org.matrix.android.sdk.session] API[${{ matrix.api-level }}]
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2
|
||||||
|
@ -274,10 +230,11 @@ jobs:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
- name: Start synapse server
|
- name: Start synapse server
|
||||||
run: |
|
uses: michaelkaye/setup-matrix-synapse@v0.3.0
|
||||||
pip install matrix-synapse
|
with:
|
||||||
curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
|
uploadLogs: true
|
||||||
| sed s/127.0.0.1/0.0.0.0/g | sed 's/http:\/\/localhost/http:\/\/10.0.2.2/g' | bash -s -- --no-rate-limit
|
httpPort: 8080
|
||||||
|
disableRateLimiting: true
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
@ -366,9 +323,6 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- integration-tests
|
- integration-tests
|
||||||
- ui-tests
|
- ui-tests
|
||||||
# - unit-tests
|
|
||||||
- build-android-test-matrix-sdk
|
|
||||||
- build-android-test-app
|
|
||||||
- sonarqube
|
- sonarqube
|
||||||
if: always() && github.event_name != 'workflow_dispatch'
|
if: always() && github.event_name != 'workflow_dispatch'
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
|
|
|
@ -12,6 +12,30 @@ env:
|
||||||
-Porg.gradle.parallel=false
|
-Porg.gradle.parallel=false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# Build Android Tests
|
||||||
|
build-android-tests:
|
||||||
|
name: Build Android Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('build-android-tests-{0}', github.ref) }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: 11
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-
|
||||||
|
- name: Build Android Tests
|
||||||
|
run: ./gradlew clean assembleAndroidTest $CI_GRADLE_ARG_PROPERTIES --stacktrace
|
||||||
|
|
||||||
unit-tests:
|
unit-tests:
|
||||||
name: Run Unit Tests
|
name: Run Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -41,3 +65,20 @@ jobs:
|
||||||
( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
|
( github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository )
|
||||||
with:
|
with:
|
||||||
files: ./**/build/test-results/**/*.xml
|
files: ./**/build/test-results/**/*.xml
|
||||||
|
|
||||||
|
# Notify the channel about runs against develop or main that have failures, as PRs should have caught these first.
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- unit-tests
|
||||||
|
- build-android-tests
|
||||||
|
if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' ) && failure() }}
|
||||||
|
steps:
|
||||||
|
- uses: michaelkaye/matrix-hookshot-action@v0.3.0
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
matrix_access_token: ${{ secrets.ELEMENT_ANDROID_NOTIFICATION_ACCESS_TOKEN }}
|
||||||
|
matrix_room_id: ${{ secrets.ELEMENT_ANDROID_INTERNAL_ROOM_ID }}
|
||||||
|
text_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}{{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||||
|
html_template: "Build is broken for ${{ github.ref }}: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion }} {{name}} <font color='{{color conclusion }}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>emojis</w>
|
<w>emojis</w>
|
||||||
<w>fdroid</w>
|
<w>fdroid</w>
|
||||||
|
<w>ganfra</w>
|
||||||
<w>gplay</w>
|
<w>gplay</w>
|
||||||
<w>hmac</w>
|
<w>hmac</w>
|
||||||
<w>homeserver</w>
|
<w>homeserver</w>
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
<w>ktlint</w>
|
<w>ktlint</w>
|
||||||
<w>linkified</w>
|
<w>linkified</w>
|
||||||
<w>linkify</w>
|
<w>linkify</w>
|
||||||
|
<w>manu</w>
|
||||||
<w>megolm</w>
|
<w>megolm</w>
|
||||||
<w>msisdn</w>
|
<w>msisdn</w>
|
||||||
<w>msisdns</w>
|
<w>msisdns</w>
|
||||||
|
|
46
CHANGES.md
46
CHANGES.md
|
@ -1,3 +1,49 @@
|
||||||
|
Changes in Element v1.4.4 (2022-03-09)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Adds animated typing indicator to the bottom of the timeline ([#3296](https://github.com/vector-im/element-android/issues/3296))
|
||||||
|
- Removes the topic and typing information from the room's top bar ([#4642](https://github.com/vector-im/element-android/issues/4642))
|
||||||
|
- Add possibility to save media from Gallery + reorder choices in message context menu ([#5005](https://github.com/vector-im/element-android/issues/5005))
|
||||||
|
- Improves settings error dialog messaging when changing avatar or display name fails ([#5418](https://github.com/vector-im/element-android/issues/5418))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Open direct message screen when clicking on DM button in the space members list ([#4319](https://github.com/vector-im/element-android/issues/4319))
|
||||||
|
- Fix incorrect media cache size in settings ([#5394](https://github.com/vector-im/element-android/issues/5394))
|
||||||
|
- Setting an avatar when creating a room had no effect ([#5402](https://github.com/vector-im/element-android/issues/5402))
|
||||||
|
- Fix reactions summary crash when reopening a room ([#5463](https://github.com/vector-im/element-android/issues/5463))
|
||||||
|
- Fixing room titles overlapping the room image in the room toolbar ([#5468](https://github.com/vector-im/element-android/issues/5468))
|
||||||
|
|
||||||
|
In development 🚧
|
||||||
|
----------------
|
||||||
|
- Starts the FTUE account personalisation flow by adding an account created screen behind a feature flag ([#5158](https://github.com/vector-im/element-android/issues/5158))
|
||||||
|
|
||||||
|
SDK API changes ⚠️
|
||||||
|
------------------
|
||||||
|
- Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive. ([#5330](https://github.com/vector-im/element-android/issues/5330))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Improve Bubble layouts rendering ([#5303](https://github.com/vector-im/element-android/issues/5303))
|
||||||
|
- Continue improving realm usage (potentially helping with storage and RAM usage) ([#5330](https://github.com/vector-im/element-android/issues/5330))
|
||||||
|
- Update reaction button layout. ([#5313](https://github.com/vector-im/element-android/issues/5313))
|
||||||
|
- Adds forceLoginFallback feature flag and usages to FTUE login and registration ([#5325](https://github.com/vector-im/element-android/issues/5325))
|
||||||
|
- Override task affinity to prevent unknown activities running in our app tasks. ([#4498](https://github.com/vector-im/element-android/issues/4498))
|
||||||
|
- Tentatively fixing the UI sanity test being unable to click on the space menu items ([#5269](https://github.com/vector-im/element-android/issues/5269))
|
||||||
|
- Moves attachment-viewer, diff-match-patch, and multipicker modules to subfolders under library ([#5309](https://github.com/vector-im/element-android/issues/5309))
|
||||||
|
- Log the `since` token used and `next_batch` token returned when doing an incremental sync. ([#5312](https://github.com/vector-im/element-android/issues/5312), [#5318](https://github.com/vector-im/element-android/issues/5318))
|
||||||
|
- Upgrades material dependency version from 1.4.0 to 1.5.0 ([#5392](https://github.com/vector-im/element-android/issues/5392))
|
||||||
|
- Using app name instead of hardcoded "Element" for exported keys filename ([#5326](https://github.com/vector-im/element-android/issues/5326))
|
||||||
|
- Upgrade the plugin which generate strings with template from 1.2.2 to 2.0.0 ([#5348](https://github.com/vector-im/element-android/issues/5348))
|
||||||
|
- Remove about 700 unused strings and their translations ([#5352](https://github.com/vector-im/element-android/issues/5352))
|
||||||
|
- Creates dedicated VectorOverrides for forcing behaviour for local testing/development ([#5361](https://github.com/vector-im/element-android/issues/5361))
|
||||||
|
- Cleanup unused threads build configurations ([#5379](https://github.com/vector-im/element-android/issues/5379))
|
||||||
|
- Notify element-android channel each time a nightly build completes. ([#5314](https://github.com/vector-im/element-android/issues/5314))
|
||||||
|
- Iterate on badge / unread indicator color ([#5456](https://github.com/vector-im/element-android/issues/5456))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.4.2 (2022-02-22 Palindrome Day!)
|
Changes in Element v1.4.2 (2022-02-22 Palindrome Day!)
|
||||||
======================================================
|
======================================================
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Typing notifications moved from the header to the bottom of the timeline.
|
|
|
@ -1 +0,0 @@
|
||||||
Open direct message screen when clicking on DM button in the space members list
|
|
|
@ -1 +0,0 @@
|
||||||
Override task affinity to prevent unknown activities running in our app tasks.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Improve headers UI in Rooms/Messages lists
|
|
@ -1 +0,0 @@
|
||||||
Update the top bar in a room: remove topic and typing information
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add colors for shield vector drawable
|
|
@ -1 +0,0 @@
|
||||||
Add possibility to save media from Gallery + reorder choices in message context menu
|
|
|
@ -1 +0,0 @@
|
||||||
Starts the FTUE account personalisation flow by adding an account created screen behind a feature flag
|
|
|
@ -1 +0,0 @@
|
||||||
Tentatively fixing the UI sanity test being unable to click on the space menu items
|
|
|
@ -1 +0,0 @@
|
||||||
Improve Bubble layouts rendering.
|
|
|
@ -1 +0,0 @@
|
||||||
Moves attachment-viewer, diff-match-patch, and multipicker modules to subfolders under library
|
|
|
@ -1 +0,0 @@
|
||||||
Log the `since` token used and `next_batch` token returned when doing an incremental sync.
|
|
|
@ -1 +0,0 @@
|
||||||
Update reaction button layout.
|
|
|
@ -1 +0,0 @@
|
||||||
Notify element-android channel each time a nightly build completes.
|
|
|
@ -1 +0,0 @@
|
||||||
Log the `since` token used and `next_batch` token returned when doing an incremental sync.
|
|
|
@ -1 +0,0 @@
|
||||||
Adds forceLoginFallback feature flag and usages to FTUE login and registration
|
|
|
@ -1 +0,0 @@
|
||||||
[Export e2ee keys] use appName instead of element
|
|
|
@ -1 +0,0 @@
|
||||||
Continue improving realm usage.
|
|
|
@ -1 +0,0 @@
|
||||||
Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Selected space highlight changed in left panel
|
|
@ -1 +0,0 @@
|
||||||
Upgrade the plugin which generate strings with template from 1.2.2 to 2.0.0
|
|
|
@ -1 +0,0 @@
|
||||||
Remove about 700 unused strings and their translations
|
|
|
@ -1 +0,0 @@
|
||||||
Creates dedicated VectorOverrides for forcing behaviour for local testing/development
|
|
|
@ -0,0 +1 @@
|
||||||
|
Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities
|
|
@ -1 +0,0 @@
|
||||||
Cleanup unused threads build configurations
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add top margin before our first message
|
|
@ -1 +0,0 @@
|
||||||
Upgrades material dependency version from 1.4.0 to 1.5.0
|
|
|
@ -1 +0,0 @@
|
||||||
Fix incorrect media cache size in settings
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add a custom view to display a picker for share location options
|
|
@ -1 +0,0 @@
|
||||||
[Create room] Setting an avatar when creating a room had no effect
|
|
|
@ -1 +0,0 @@
|
||||||
Improves settings error dialog messaging when changing avatar or display name fails
|
|
|
@ -0,0 +1 @@
|
||||||
|
Adds stable room hierarchy endpoint with a fallback to the unstable one
|
|
@ -0,0 +1 @@
|
||||||
|
Fix missing messages when loading messages forwards
|
|
@ -1 +0,0 @@
|
||||||
Iterate on badge / unread indicator color
|
|
|
@ -0,0 +1 @@
|
||||||
|
Use ColorPrimary for attachmentGalleryButton tint
|
|
@ -0,0 +1 @@
|
||||||
|
Read receipt in wrong order
|
|
@ -58,6 +58,7 @@ ext.libs = [
|
||||||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||||
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||||
|
'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
|
||||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||||
|
@ -141,4 +142,4 @@ ext.libs = [
|
||||||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
||||||
'junit' : "junit:junit:4.13.2"
|
'junit' : "junit:junit:4.13.2"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: typing indicator UI updates. Various bug fixes and stability improvements.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.4
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
|
distributionSha256Sum=a9a7b7baba105f6557c9dcf9c3c6e8f7e57e6b49889c5f1d133f015d0727e4be
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.ContextMenu
|
import android.view.ContextMenu
|
||||||
import android.view.Menu
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -77,10 +76,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
|
||||||
menuInfo: ContextMenu.ContextMenuInfo?
|
menuInfo: ContextMenu.ContextMenuInfo?
|
||||||
) {
|
) {
|
||||||
if (copyValue != null) {
|
if (copyValue != null) {
|
||||||
val menuItem = menu?.add(
|
val menuItem = menu?.add(R.string.copy_value)
|
||||||
Menu.NONE, R.id.copy_value,
|
|
||||||
Menu.NONE, R.string.copy_value
|
|
||||||
)
|
|
||||||
val clipService =
|
val clipService =
|
||||||
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||||
menuItem?.setOnMenuItemClickListener {
|
menuItem?.setOnMenuItemClickListener {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/copy_value"
|
|
||||||
android:title="@string/copy_value" />
|
|
||||||
|
|
||||||
</menu>
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="copy_value">Copy Value</string>
|
<string name="copy_value">Copy Value</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -71,19 +71,6 @@
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:text="Destructive disabled" />
|
android:text="Destructive disabled" />
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Vector.Button.Unelevated.Bot"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Bot" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Vector.Button.Unelevated.Bot"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:enabled="false"
|
|
||||||
android:text="Bot disabled" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="@style/Widget.Vector.Button.Outlined"
|
style="@style/Widget.Vector.Button.Outlined"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -98,19 +85,6 @@
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:text="Outline disabled" />
|
android:text="Outline disabled" />
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Poll " />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
style="@style/Widget.Vector.Button.Outlined.Poll"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:enabled="false"
|
|
||||||
android:text="Poll disabled" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Dark"
|
style="@style/Widget.Vector.Button.Outlined.SocialLogin.Google.Light"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Continue with XXX" />
|
android:text="Continue with XXX" />
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:color="#4285F4" android:state_enabled="true"/>
|
|
||||||
<item android:color="@color/vctr_disabled_view_color_light" android:state_enabled="false"/>
|
|
||||||
<item android:color="#3367D6" android:state_pressed="true"/>
|
|
||||||
<item android:color="#4285F4" android:state_focused="true"/>
|
|
||||||
</selector>
|
|
|
@ -1,32 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="DialpadKeyNumberStyle">
|
|
||||||
<item name="android:textColor">?attr/vctr_content_primary</item>
|
|
||||||
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
|
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
|
||||||
<item name="android:layout_width">wrap_content</item>
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
|
|
||||||
<item name="android:gravity">center</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="DialpadKeyLettersStyle">
|
|
||||||
<item name="android:textColor">?attr/vctr_content_secondary</item>
|
|
||||||
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
|
|
||||||
<item name="android:fontFamily">sans-serif-regular</item>
|
|
||||||
<item name="android:layout_width">wrap_content</item>
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="android:gravity">center_horizontal</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
|
|
||||||
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
|
|
||||||
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
|
|
||||||
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
|
|
||||||
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -9,10 +9,6 @@
|
||||||
<color name="soft_resource_limit_exceeded">#2f9edb</color>
|
<color name="soft_resource_limit_exceeded">#2f9edb</color>
|
||||||
<color name="hard_resource_limit_exceeded">?colorError</color>
|
<color name="hard_resource_limit_exceeded">?colorError</color>
|
||||||
|
|
||||||
<!-- Button color -->
|
|
||||||
<color name="button_bot_background_color">#14368BD6</color>
|
|
||||||
<color name="button_bot_enabled_text_color">@color/palette_azure</color>
|
|
||||||
|
|
||||||
<!-- Notification (do not depends on theme) -->
|
<!-- Notification (do not depends on theme) -->
|
||||||
<color name="notification_accent_color">@color/palette_azure</color>
|
<color name="notification_accent_color">@color/palette_azure</color>
|
||||||
<color name="key_share_req_accent_color">@color/palette_melon</color>
|
<color name="key_share_req_accent_color">@color/palette_melon</color>
|
||||||
|
@ -22,7 +18,6 @@
|
||||||
<color name="bg_call_screen_blur">#99000000</color>
|
<color name="bg_call_screen_blur">#99000000</color>
|
||||||
<color name="bg_call_screen">#27303A</color>
|
<color name="bg_call_screen">#27303A</color>
|
||||||
|
|
||||||
<color name="vctr_notice_secondary">#FF61708B</color>
|
|
||||||
<color name="vctr_notice_secondary_alpha12">#1E61708B</color>
|
<color name="vctr_notice_secondary_alpha12">#1E61708B</color>
|
||||||
|
|
||||||
<!-- Other useful color -->
|
<!-- Other useful color -->
|
||||||
|
@ -83,16 +78,6 @@
|
||||||
<color name="vctr_touch_guard_bg_dark">#BF000000</color>
|
<color name="vctr_touch_guard_bg_dark">#BF000000</color>
|
||||||
<color name="vctr_touch_guard_bg_black">#BF000000</color>
|
<color name="vctr_touch_guard_bg_black">#BF000000</color>
|
||||||
|
|
||||||
<attr name="vctr_attachment_selector_background" format="color" />
|
|
||||||
<color name="vctr_attachment_selector_background_light">#FFFFFFFF</color>
|
|
||||||
<color name="vctr_attachment_selector_background_dark">#FF22262E</color>
|
|
||||||
<color name="vctr_attachment_selector_background_black">#FF090A0C</color>
|
|
||||||
|
|
||||||
<attr name="vctr_attachment_selector_border" format="color" />
|
|
||||||
<color name="vctr_attachment_selector_border_light">#FFE9EDF1</color>
|
|
||||||
<color name="vctr_attachment_selector_border_dark">#FF22262E</color>
|
|
||||||
<color name="vctr_attachment_selector_border_black">#FF090A0C</color>
|
|
||||||
|
|
||||||
<attr name="vctr_room_active_widgets_banner_bg" format="color" />
|
<attr name="vctr_room_active_widgets_banner_bg" format="color" />
|
||||||
<color name="vctr_room_active_widgets_banner_bg_light">#EBEFF5</color>
|
<color name="vctr_room_active_widgets_banner_bg_light">#EBEFF5</color>
|
||||||
<color name="vctr_room_active_widgets_banner_bg_dark">#27303A</color>
|
<color name="vctr_room_active_widgets_banner_bg_dark">#27303A</color>
|
||||||
|
@ -107,9 +92,7 @@
|
||||||
<color name="vctr_waiting_background_color_light">#AAAAAAAA</color>
|
<color name="vctr_waiting_background_color_light">#AAAAAAAA</color>
|
||||||
<color name="vctr_waiting_background_color_dark">#55555555</color>
|
<color name="vctr_waiting_background_color_dark">#55555555</color>
|
||||||
|
|
||||||
<attr name="vctr_disabled_view_color" format="color" />
|
|
||||||
<color name="vctr_disabled_view_color_light">#EEEEEE</color>
|
<color name="vctr_disabled_view_color_light">#EEEEEE</color>
|
||||||
<color name="vctr_disabled_view_color_dark">#61708B</color>
|
|
||||||
|
|
||||||
<attr name="vctr_reaction_background_off" format="color" />
|
<attr name="vctr_reaction_background_off" format="color" />
|
||||||
<color name="vctr_reaction_background_off_light">#FFF3F8FD</color>
|
<color name="vctr_reaction_background_off_light">#FFF3F8FD</color>
|
||||||
|
@ -139,4 +122,14 @@
|
||||||
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
||||||
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
||||||
|
|
||||||
|
<!-- Location sharing colors -->
|
||||||
|
<attr name="vctr_live_location" format="color" />
|
||||||
|
<color name="vctr_live_location_light">@color/palette_prune</color>
|
||||||
|
<color name="vctr_live_location_dark">@color/palette_prune</color>
|
||||||
|
|
||||||
|
<!-- Shield colors -->
|
||||||
|
<color name="shield_color_trust">#0DBD8B</color>
|
||||||
|
<color name="shield_color_black">#17191C</color>
|
||||||
|
<color name="shield_color_warning">#FF4B55</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -9,20 +9,11 @@
|
||||||
<dimen name="layout_vertical_margin_big">32dp</dimen>
|
<dimen name="layout_vertical_margin_big">32dp</dimen>
|
||||||
|
|
||||||
<dimen name="profile_avatar_size">50dp</dimen>
|
<dimen name="profile_avatar_size">50dp</dimen>
|
||||||
<dimen name="floating_action_button_margin">16dp</dimen>
|
|
||||||
<dimen name="navigation_view_height">196dp</dimen>
|
|
||||||
<dimen name="navigation_avatar_top_margin">44dp</dimen>
|
|
||||||
<dimen name="item_decoration_left_margin">72dp</dimen>
|
|
||||||
<dimen name="item_event_message_state_size">16dp</dimen>
|
<dimen name="item_event_message_state_size">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="item_event_message_media_button_size">32dp</dimen>
|
<dimen name="item_event_message_media_button_size">32dp</dimen>
|
||||||
|
|
||||||
<dimen name="chat_avatar_size">40dp</dimen>
|
|
||||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
|
||||||
|
|
||||||
<dimen name="quote_width">4dp</dimen>
|
|
||||||
<dimen name="quote_gap">8dp</dimen>
|
<dimen name="quote_gap">8dp</dimen>
|
||||||
<dimen name="drawable_padding_small">8dp</dimen>
|
|
||||||
|
|
||||||
<item name="dialog_width_ratio" format="float" type="dimen">0.75</item>
|
<item name="dialog_width_ratio" format="float" type="dimen">0.75</item>
|
||||||
|
|
||||||
|
@ -70,4 +61,7 @@
|
||||||
|
|
||||||
<item name="ftue_auth_profile_picture_height" format="float" type="dimen">0.15</item>
|
<item name="ftue_auth_profile_picture_height" format="float" type="dimen">0.15</item>
|
||||||
<item name="ftue_auth_profile_picture_icon_height" format="float" type="dimen">0.05</item>
|
<item name="ftue_auth_profile_picture_icon_height" format="float" type="dimen">0.05</item>
|
||||||
</resources>
|
|
||||||
|
<!-- Location sharing -->
|
||||||
|
<dimen name="location_sharing_option_default_padding">10dp</dimen>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
|
||||||
|
|
||||||
<!-- Define all the colors used across the Element Android project
|
<!--
|
||||||
Source: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1338%3A17947 -->
|
Define all the colors used across the Element Android project
|
||||||
|
Source: https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1338%3A17947
|
||||||
|
Some colors are not used, but we want the palette to be complete so we ignore the lint error
|
||||||
|
UnusedResources for all the resources in this file
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- For all themes -->
|
<!-- For all themes -->
|
||||||
<color name="palette_azure">#368BD6</color>
|
<color name="palette_azure">#368BD6</color>
|
||||||
|
@ -15,6 +19,7 @@
|
||||||
<color name="palette_element_green">#0DBD8B</color>
|
<color name="palette_element_green">#0DBD8B</color>
|
||||||
<color name="palette_white">#FFFFFF</color>
|
<color name="palette_white">#FFFFFF</color>
|
||||||
<color name="palette_vermilion">#FF5B55</color>
|
<color name="palette_vermilion">#FF5B55</color>
|
||||||
|
<!-- (unused) -->
|
||||||
<color name="palette_ems">#7E69FF</color>
|
<color name="palette_ems">#7E69FF</color>
|
||||||
<color name="palette_aqua">#2DC2C5</color>
|
<color name="palette_aqua">#2DC2C5</color>
|
||||||
<color name="palette_prune">#5C56F5</color>
|
<color name="palette_prune">#5C56F5</color>
|
||||||
|
@ -27,6 +32,7 @@
|
||||||
<color name="palette_gray_150">#8D97A5</color>
|
<color name="palette_gray_150">#8D97A5</color>
|
||||||
<color name="palette_gray_200">#737D8C</color>
|
<color name="palette_gray_200">#737D8C</color>
|
||||||
<color name="palette_black_900">#17191C</color>
|
<color name="palette_black_900">#17191C</color>
|
||||||
|
<!-- (unused) -->
|
||||||
<color name="palette_ice">#F4F9FD</color>
|
<color name="palette_ice">#F4F9FD</color>
|
||||||
|
|
||||||
<!-- For dark themes -->
|
<!-- For dark themes -->
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
<attr name="vctr_system" format="color" />
|
<attr name="vctr_system" format="color" />
|
||||||
<color name="element_system_light">@color/palette_gray_25</color>
|
<color name="element_system_light">@color/palette_gray_25</color>
|
||||||
<color name="element_system_dark">@color/palette_black_950</color>
|
<color name="element_system_dark">@color/palette_black_950</color>
|
||||||
<color name="element_system_black">@color/palette_black_950</color>
|
|
||||||
|
|
||||||
<color name="element_background_light">@color/palette_white</color>
|
<color name="element_background_light">@color/palette_white</color>
|
||||||
<color name="element_background_dark">@color/palette_black_800</color>
|
<color name="element_background_dark">@color/palette_black_800</color>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="LocationSharingOptionView">
|
||||||
|
<attr name="locShareIcon" format="reference" />
|
||||||
|
<attr name="locShareIconBackground" format="reference" />
|
||||||
|
<attr name="locShareIconBackgroundTint" format="color" />
|
||||||
|
<attr name="locShareIconPadding" format="dimension" />
|
||||||
|
<attr name="locShareIconDescription" format="string" />
|
||||||
|
<attr name="locShareTitle" format="string" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="AttachmentTypeSelectorButton">
|
|
||||||
<item name="android:layout_width">56dp</item>
|
|
||||||
<item name="android:layout_height">56dp</item>
|
|
||||||
<item name="android:scaleType">center</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AttachmentTypeSelectorLabel">
|
|
||||||
<item name="android:layout_width">wrap_content</item>
|
|
||||||
<item name="android:layout_height">wrap_content</item>
|
|
||||||
<item name="android:textColor">?vctr_content_primary</item>
|
|
||||||
<item name="android:textSize">14sp</item>
|
|
||||||
<item name="android:layout_marginTop">8dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -33,24 +33,6 @@
|
||||||
<!-- Keep default colors from the theme -->
|
<!-- Keep default colors from the theme -->
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Unelevated" parent="Widget.MaterialComponents.Button.UnelevatedButton">
|
|
||||||
<item name="android:paddingLeft">16dp</item>
|
|
||||||
<item name="android:paddingRight">16dp</item>
|
|
||||||
<item name="android:minWidth">94dp</item>
|
|
||||||
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
|
|
||||||
<item name="cornerRadius">8dp</item>
|
|
||||||
<item name="lineHeight">24sp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Unelevated.Bot">
|
|
||||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayBot</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="VectorMaterialThemeOverlayBot">
|
|
||||||
<item name="colorPrimary">@color/button_bot_background_color</item>
|
|
||||||
<item name="colorOnPrimary">@color/button_bot_enabled_text_color</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Text" parent="Widget.MaterialComponents.Button.TextButton">
|
<style name="Widget.Vector.Button.Text" parent="Widget.MaterialComponents.Button.TextButton">
|
||||||
<item name="colorControlHighlight">?colorSecondary</item>
|
<item name="colorControlHighlight">?colorSecondary</item>
|
||||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item>
|
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayPositive</item>
|
||||||
|
@ -83,9 +65,4 @@
|
||||||
<item name="colorPrimary">?colorOnPrimary</item>
|
<item name="colorPrimary">?colorOnPrimary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Outlined.Poll">
|
|
||||||
<item name="android:minHeight">44dp</item>
|
|
||||||
<item name="cornerRadius">10dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,5 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="UnusedResources">
|
||||||
|
|
||||||
|
<!-- This file contain styles to override the style from the library android-dialer-1.2.5.aar
|
||||||
|
This is why we ignore the lint error UnusedResources -->
|
||||||
|
|
||||||
<style name="DialpadKeyNumberStyle">
|
<style name="DialpadKeyNumberStyle">
|
||||||
<item name="android:textColor">?vctr_content_primary</item>
|
<item name="android:textColor">?vctr_content_primary</item>
|
||||||
|
|
|
@ -28,11 +28,6 @@
|
||||||
<item name="android:textColor">@color/black_54</item>
|
<item name="android:textColor">@color/black_54</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Google.Dark">
|
|
||||||
<item name="android:backgroundTint">@color/button_social_google_background_selector_dark</item>
|
|
||||||
<item name="android:textColor">@android:color/white</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Outlined.SocialLogin.Github">
|
<style name="Widget.Vector.Button.Outlined.SocialLogin.Github">
|
||||||
<item name="icon">@drawable/ic_social_github</item>
|
<item name="icon">@drawable/ic_social_github</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,8 +11,6 @@
|
||||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
||||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_black</item>
|
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_black</item>
|
||||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_black</item>
|
|
||||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_black</item>
|
|
||||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_black</item>
|
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_black</item>
|
||||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_black</item>
|
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_black</item>
|
||||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_black</item>
|
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_black</item>
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
||||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_dark</item>
|
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_dark</item>
|
||||||
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_dark</item>
|
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_dark</item>
|
||||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_dark</item>
|
|
||||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_dark</item>
|
|
||||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_dark</item>
|
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_dark</item>
|
||||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_dark</item>
|
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_dark</item>
|
||||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_dark</item>
|
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_dark</item>
|
||||||
|
@ -145,6 +143,8 @@
|
||||||
|
|
||||||
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
||||||
|
|
||||||
|
<!-- Location sharing -->
|
||||||
|
<item name="vctr_live_location">@color/vctr_live_location_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
<style name="Theme.Vector.Dark" parent="Base.Theme.Vector.Dark" />
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
||||||
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_light</item>
|
<item name="vctr_touch_guard_bg">@color/vctr_touch_guard_bg_light</item>
|
||||||
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_light</item>
|
<item name="vctr_keys_backup_banner_accent_color">@color/vctr_keys_backup_banner_accent_color_light</item>
|
||||||
<item name="vctr_attachment_selector_background">@color/vctr_attachment_selector_background_light</item>
|
|
||||||
<item name="vctr_attachment_selector_border">@color/vctr_attachment_selector_border_light</item>
|
|
||||||
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_light</item>
|
<item name="vctr_room_active_widgets_banner_bg">@color/vctr_room_active_widgets_banner_bg_light</item>
|
||||||
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_light</item>
|
<item name="vctr_room_active_widgets_banner_text">@color/vctr_room_active_widgets_banner_text_light</item>
|
||||||
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_light</item>
|
<item name="vctr_reaction_background_off">@color/vctr_reaction_background_off_light</item>
|
||||||
|
@ -146,6 +144,8 @@
|
||||||
|
|
||||||
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
<item name="android:actionButtonStyle">@style/Widget.Vector.ActionButton</item>
|
||||||
|
|
||||||
|
<!-- Location sharing -->
|
||||||
|
<item name="vctr_live_location">@color/vctr_live_location_light</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
<style name="Theme.Vector.Light" parent="Base.Theme.Vector.Light" />
|
||||||
|
|
|
@ -31,12 +31,11 @@ android {
|
||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.4.4\""
|
buildConfigField "String", "SDK_VERSION", "\"1.4.6\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\""
|
||||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
consumerProguardFiles 'proguard-rules.pro'
|
consumerProguardFiles 'proguard-rules.pro'
|
||||||
|
@ -167,7 +166,7 @@ dependencies {
|
||||||
implementation libs.apache.commonsImaging
|
implementation libs.apache.commonsImaging
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.45'
|
||||||
|
|
||||||
testImplementation libs.tests.junit
|
testImplementation libs.tests.junit
|
||||||
testImplementation 'org.robolectric:robolectric:4.7.3'
|
testImplementation 'org.robolectric:robolectric:4.7.3'
|
||||||
|
@ -175,7 +174,7 @@ dependencies {
|
||||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||||
testImplementation libs.mockk.mockk
|
testImplementation libs.mockk.mockk
|
||||||
testImplementation libs.tests.kluent
|
testImplementation libs.tests.kluent
|
||||||
implementation libs.jetbrains.coroutinesAndroid
|
testImplementation libs.jetbrains.coroutinesTest
|
||||||
// Plant Timber tree for test
|
// Plant Timber tree for test
|
||||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||||
// Transitively required for mocking realm as monarchy doesn't expose Rx
|
// Transitively required for mocking realm as monarchy doesn't expose Rx
|
||||||
|
|
|
@ -23,7 +23,7 @@ object TestConstants {
|
||||||
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
||||||
|
|
||||||
// Time out to use when waiting for server response.
|
// Time out to use when waiting for server response.
|
||||||
private const val AWAIT_TIME_OUT_MILLIS = 30_000
|
private const val AWAIT_TIME_OUT_MILLIS = 60_000
|
||||||
|
|
||||||
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
||||||
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
||||||
|
|
|
@ -0,0 +1,649 @@
|
||||||
|
/*
|
||||||
|
* 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 android.util.Log
|
||||||
|
import androidx.test.filters.LargeTest
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.amshove.kluent.fail
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
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.toModel
|
||||||
|
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
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
|
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
@LargeTest
|
||||||
|
class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
|
private val testHelper = CommonTestHelper(context())
|
||||||
|
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple test that create an e2ee room.
|
||||||
|
* Some new members are added, and a message is sent.
|
||||||
|
* We check that the message is e2e and can be decrypted.
|
||||||
|
*
|
||||||
|
* Additional users join, we check that they can't decrypt history
|
||||||
|
*
|
||||||
|
* Alice sends a new message, then check that the new one can be decrypted
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testSendingE2EEMessages() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
|
// add some more users and invite them
|
||||||
|
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
||||||
|
.map {
|
||||||
|
testHelper.createAccount(it, SessionTestParams(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "All accounts created")
|
||||||
|
// we want to invite them in the room
|
||||||
|
otherAccounts.forEach {
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
|
||||||
|
aliceRoomPOV.invite(it.myUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All user should accept invite
|
||||||
|
otherAccounts.forEach { otherSession ->
|
||||||
|
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
||||||
|
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that alice see them as joined (not really necessary?)
|
||||||
|
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "All users have joined the room")
|
||||||
|
Log.v("#E2E TEST", "Alice is sending the message")
|
||||||
|
|
||||||
|
val text = "This is my message"
|
||||||
|
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
|
||||||
|
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
||||||
|
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||||
|
|
||||||
|
// All should be able to decrypt
|
||||||
|
otherAccounts.forEach { otherSession ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new user to the room, and check that he can't decrypt
|
||||||
|
val newAccount = listOf("adam") // , "adam", "manu")
|
||||||
|
.map {
|
||||||
|
testHelper.createAccount(it, SessionTestParams(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
newAccount.forEach {
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
|
||||||
|
aliceRoomPOV.invite(it.myUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAccount.forEach {
|
||||||
|
waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
|
||||||
|
|
||||||
|
// wait a bit
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(3_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that messages are encrypted (uisi)
|
||||||
|
newAccount.forEach { otherSession ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
|
||||||
|
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
|
||||||
|
}
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
|
||||||
|
timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let alice send a new message
|
||||||
|
Log.v("#E2E TEST", "Alice sends a new message")
|
||||||
|
|
||||||
|
val secondMessage = "2 This is my message"
|
||||||
|
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
|
||||||
|
|
||||||
|
// new members should be able to decrypt it
|
||||||
|
newAccount.forEach { otherSession ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
|
||||||
|
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
|
||||||
|
}
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||||
|
secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
otherAccounts.forEach {
|
||||||
|
testHelper.signOutAndClose(it)
|
||||||
|
}
|
||||||
|
newAccount.forEach { testHelper.signOutAndClose(it) }
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(testHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick test for basic key backup
|
||||||
|
* 1. Create e2e between Alice and Bob
|
||||||
|
* 2. Alice sends 3 messages, using 3 different sessions
|
||||||
|
* 3. Ensure bob can decrypt
|
||||||
|
* 4. Create backup for bob and upload keys
|
||||||
|
*
|
||||||
|
* 5. Sign out alice and bob to ensure no gossiping will happen
|
||||||
|
*
|
||||||
|
* 6. Let bob sign in with a new session
|
||||||
|
* 7. Ensure history is UISI
|
||||||
|
* 8. Import backup
|
||||||
|
* 9. Check that new session can decrypt
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testBasicBackupImport() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "Create and start key backup for bob ...")
|
||||||
|
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
|
||||||
|
val keyBackupPassword = "FooBarBaz"
|
||||||
|
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
|
||||||
|
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
||||||
|
}
|
||||||
|
val version = testHelper.doSync<KeysVersion> {
|
||||||
|
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
|
}
|
||||||
|
Log.v("#E2E TEST", "... Key backup started and enabled for bob")
|
||||||
|
// Bob session should now have
|
||||||
|
|
||||||
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
|
// let's send a few message to bob
|
||||||
|
val sentEventIds = mutableListOf<String>()
|
||||||
|
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||||
|
messagesText.forEach { text ->
|
||||||
|
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||||
|
sentEventIds.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we want more so let's discard the session
|
||||||
|
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||||
|
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(1_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.v("#E2E TEST", "Bob received all and can decrypt")
|
||||||
|
|
||||||
|
// Let's wait a bit to be sure that bob has backed up the session
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "Force key backup for Bob...")
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
bobKeysBackupService.backupAllGroupSessions(
|
||||||
|
null,
|
||||||
|
TestMatrixCallback(latch, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.v("#E2E TEST", "... Key backup done for Bob")
|
||||||
|
|
||||||
|
// Now lets logout both alice and bob to ensure that we won't have any gossiping
|
||||||
|
|
||||||
|
val bobUserId = bobSession.myUserId
|
||||||
|
Log.v("#E2E TEST", "Logout alice and bob...")
|
||||||
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
testHelper.signOutAndClose(bobSession)
|
||||||
|
Log.v("#E2E TEST", "..Logout alice and bob...")
|
||||||
|
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(1_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new session for bob
|
||||||
|
Log.v("#E2E TEST", "Create a new session for Bob")
|
||||||
|
val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
// check that bob can't currently decrypt
|
||||||
|
Log.v("#E2E TEST", "check that bob can't currently decrypt")
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
|
||||||
|
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
|
||||||
|
}
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.ENCRYPTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// after initial sync events are not decrypted, so we have to try manually
|
||||||
|
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||||
|
|
||||||
|
// Let's now import keys from backup
|
||||||
|
|
||||||
|
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
||||||
|
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||||
|
keysBackupService.getVersion(version.version, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||||
|
keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
|
||||||
|
keyBackupPassword,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(3, importedResult.totalNumberOfKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure bob can now decrypt
|
||||||
|
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
|
|
||||||
|
testHelper.signOutAndClose(newBobSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a new verified session that was not supposed to get the keys initially will
|
||||||
|
* get them from an older one.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testSimpleGossip() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
|
cryptoTestHelper.initializeCrossSigning(bobSession)
|
||||||
|
|
||||||
|
// let's send a few message to bob
|
||||||
|
val sentEventIds = mutableListOf<String>()
|
||||||
|
val messagesText = listOf("1. Hello", "2. Bob")
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
|
messagesText.forEach { text ->
|
||||||
|
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||||
|
sentEventIds.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure bob can decrypt
|
||||||
|
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
|
||||||
|
|
||||||
|
// Let's now add a new bob session
|
||||||
|
// Create a new session for bob
|
||||||
|
Log.v("#E2E TEST", "Create a new session for Bob")
|
||||||
|
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
// check that new bob can't currently decrypt
|
||||||
|
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||||
|
|
||||||
|
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||||
|
|
||||||
|
// Try to request
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||||
|
newBobSession.cryptoService().requestRoomKeyForEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait a bit
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||||
|
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
|
||||||
|
|
||||||
|
// Now mark new bob session as verified
|
||||||
|
|
||||||
|
bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
|
||||||
|
newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
|
||||||
|
|
||||||
|
// now let new session re-request
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||||
|
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait a bit
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(testHelper)
|
||||||
|
testHelper.signOutAndClose(newBobSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that if a better key is forwarded (lower index, it is then used)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testForwardBetterKey() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
||||||
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
|
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
|
||||||
|
|
||||||
|
// let's send a few message to bob
|
||||||
|
var firstEventId: String
|
||||||
|
val firstMessage = "1. Hello"
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
|
firstMessage.let { text ->
|
||||||
|
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||||
|
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure bob can decrypt
|
||||||
|
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||||
|
|
||||||
|
// Let's add a new unverified session from bob
|
||||||
|
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
// check that new bob can't currently decrypt
|
||||||
|
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||||
|
ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||||
|
|
||||||
|
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
||||||
|
var secondEventId: String
|
||||||
|
val secondMessage = "2. New Device?"
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
|
secondMessage.let { text ->
|
||||||
|
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||||
|
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that both messages have same sessionId (it's just that we don't have index 0)
|
||||||
|
val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||||
|
val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||||
|
|
||||||
|
val firstSessionId = firstEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
val secondSessionId = secondEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
|
||||||
|
Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
|
||||||
|
|
||||||
|
// Confirm we can decrypt one but not the other
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
|
fail("Should not be able to decrypt event")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||||
|
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
fail("Should be able to decrypt event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's verify bobs session, and re-request keys
|
||||||
|
bobSessionWithBetterKey.cryptoService()
|
||||||
|
.verificationService()
|
||||||
|
.markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
|
||||||
|
|
||||||
|
newBobSession.cryptoService()
|
||||||
|
.verificationService()
|
||||||
|
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
||||||
|
|
||||||
|
// now let new session request
|
||||||
|
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||||
|
|
||||||
|
// wait a bit
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
delay(10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// old session should have shared the key at earliest known index now
|
||||||
|
// we should be able to decrypt both
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
fail("Should be able to decrypt first event now $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
fail("Should be able to decrypt event $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(testHelper)
|
||||||
|
testHelper.signOutAndClose(newBobSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
||||||
|
aliceRoomPOV.sendTextMessage(text)
|
||||||
|
var sentEventId: String? = null
|
||||||
|
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||||
|
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
|
||||||
|
timeline.start()
|
||||||
|
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val decryptedMsg = timeline.getSnapshot()
|
||||||
|
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||||
|
.also { list ->
|
||||||
|
val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
|
||||||
|
Log.v("#E2E TEST", "Timeline snapshot is $message")
|
||||||
|
}
|
||||||
|
.filter { it.root.sendState == SendState.SYNCED }
|
||||||
|
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
|
||||||
|
sentEventId = decryptedMsg?.eventId
|
||||||
|
decryptedMsg != null
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.dispose()
|
||||||
|
}
|
||||||
|
return sentEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
otherAccounts.map {
|
||||||
|
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
|
||||||
|
}.all {
|
||||||
|
it == Membership.JOIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||||
|
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
|
||||||
|
if (it) {
|
||||||
|
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runBlockingTest(60_000) {
|
||||||
|
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||||
|
try {
|
||||||
|
otherSession.joinRoom(e2eRoomID)
|
||||||
|
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
|
||||||
|
// it's ok we will wait after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
|
||||||
|
testHelper.waitWithLatch {
|
||||||
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||||
|
roomSummary != null && roomSummary.membership == Membership.JOIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
||||||
|
sentEventIds.forEachIndexed { index, sentEventId ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
session.cryptoService().decryptEvent(event, "").let { result ->
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.getClearType() == EventType.MESSAGE &&
|
||||||
|
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
|
timelineEvent != null &&
|
||||||
|
timelineEvent.isEncrypted() &&
|
||||||
|
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
newBobSession.cryptoService().decryptEvent(event, "")
|
||||||
|
fail("Should not be able to decrypt event")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||||
|
if (expectedError == null) {
|
||||||
|
Assert.assertNotNull(errorType)
|
||||||
|
} else {
|
||||||
|
assertEquals(expectedError, errorType, "Message expected to be UISI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest {
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun ensure_outbound_session_happy_path() {
|
fun ensure_outbound_session_happy_path() {
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val e2eRoomID = testData.roomId
|
val e2eRoomID = testData.roomId
|
||||||
|
@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest {
|
||||||
// Just send a real message as test
|
// Just send a real message as test
|
||||||
val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
|
val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
|
||||||
|
|
||||||
assertEquals(megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId, "Unexpected megolm session")
|
assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId,)
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
||||||
|
|
|
@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest {
|
||||||
* -> This is automatically fixed after SDKs restarted the olm session
|
* -> This is automatically fixed after SDKs restarted the olm session
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun testUnwedging() {
|
fun testUnwedging() {
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
|
|
||||||
|
@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest {
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
|
||||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||||
|
val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
|
||||||
// bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
|
||||||
// aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
|
||||||
|
|
||||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest {
|
||||||
Timber.i("## CRYPTO | testUnwedging: 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().getMyDevice().identityKey()!!)
|
aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
|
||||||
|
olmDevice.clearOlmSessionCache()
|
||||||
Thread.sleep(6_000)
|
Thread.sleep(6_000)
|
||||||
|
|
||||||
// Force new session, and key share
|
// Force new session, and key share
|
||||||
|
@ -227,8 +224,10 @@ class UnwedgingTest : InstrumentedTest {
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
// we should get back the key and be able to decrypt
|
// we should get back the key and be able to decrypt
|
||||||
val result = tryOrNull {
|
val result = testHelper.runBlockingTest {
|
||||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
tryOrNull {
|
||||||
|
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
||||||
result != null
|
result != null
|
||||||
|
|
|
@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest {
|
||||||
assert(receivedEvent!!.isEncrypted())
|
assert(receivedEvent!!.isEncrypted())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
commonTestHelper.runBlockingTest {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
}
|
||||||
fail("should fail")
|
fail("should fail")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
}
|
}
|
||||||
|
@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
commonTestHelper.runBlockingTest {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
}
|
||||||
fail("should fail")
|
fail("should fail")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
}
|
}
|
||||||
|
@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
commonTestHelper.runBlockingTest {
|
||||||
|
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||||
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
fail("should have been able to decrypt")
|
fail("should have been able to decrypt")
|
||||||
}
|
}
|
||||||
|
@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest {
|
||||||
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
||||||
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
||||||
|
|
||||||
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
|
var dRes = tryOrNull {
|
||||||
|
commonTestHelper.runBlockingTest {
|
||||||
|
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert(dRes == null)
|
assert(dRes == null)
|
||||||
|
|
||||||
|
@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest {
|
||||||
Thread.sleep(3_000)
|
Thread.sleep(3_000)
|
||||||
|
|
||||||
// With the bug the first session would have improperly reshare that key :/
|
// With the bug the first session would have improperly reshare that key :/
|
||||||
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
|
dRes = tryOrNull {
|
||||||
|
commonTestHelper.runBlockingTest {
|
||||||
|
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
|
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
|
||||||
assert(dRes?.clearEvent == null)
|
assert(dRes?.clearEvent == null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest {
|
||||||
// Bob should not be able to decrypt because the keys is withheld
|
// Bob should not be able to decrypt because the keys is withheld
|
||||||
try {
|
try {
|
||||||
// .. might need to wait a bit for stability?
|
// .. might need to wait a bit for stability?
|
||||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
testHelper.runBlockingTest {
|
||||||
|
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||||
|
}
|
||||||
Assert.fail("This session should not be able to decrypt")
|
Assert.fail("This session should not be able to decrypt")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest {
|
||||||
// Previous message should still be undecryptable (partially withheld session)
|
// Previous message should still be undecryptable (partially withheld session)
|
||||||
try {
|
try {
|
||||||
// .. might need to wait a bit for stability?
|
// .. might need to wait a bit for stability?
|
||||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
testHelper.runBlockingTest {
|
||||||
|
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||||
|
}
|
||||||
Assert.fail("This session should not be able to decrypt")
|
Assert.fail("This session should not be able to decrypt")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest {
|
||||||
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
|
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
|
||||||
try {
|
try {
|
||||||
// .. might need to wait a bit for stability?
|
// .. might need to wait a bit for stability?
|
||||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
testHelper.runBlockingTest {
|
||||||
|
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||||
|
}
|
||||||
Assert.fail("This session should not be able to decrypt")
|
Assert.fail("This session should not be able to decrypt")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
|
@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest {
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
|
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
|
||||||
// try to decrypt and force key request
|
// try to decrypt and force key request
|
||||||
tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
tryOrNull {
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
bobSecondSession.cryptoService().decryptEvent(it.root, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||||
timeLineEvent != null
|
timeLineEvent != null
|
||||||
|
|
|
@ -121,7 +121,7 @@ interface CryptoService {
|
||||||
fun discardOutboundSession(roomId: String)
|
fun discardOutboundSession(roomId: String)
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||||
|
|
||||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
|
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
@ -216,6 +217,11 @@ interface RoomService {
|
||||||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a flow on the number of rooms.
|
||||||
|
*/
|
||||||
|
fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO Doc
|
* TODO Doc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
interface UpdatableLivePageResult {
|
interface UpdatableLivePageResult {
|
||||||
val livePagedList: LiveData<PagedList<RoomSummary>>
|
val livePagedList: LiveData<PagedList<RoomSummary>>
|
||||||
|
|
||||||
fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
|
|
||||||
|
|
||||||
val liveBoundaries: LiveData<ResultBoundaries>
|
val liveBoundaries: LiveData<ResultBoundaries>
|
||||||
|
var queryParams: RoomSummaryQueryParams
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ResultBoundaries(
|
data class ResultBoundaries(
|
||||||
|
|
|
@ -38,7 +38,7 @@ interface TimelineService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a snapshot of TimelineEvent event with eventId.
|
* Returns a snapshot of TimelineEvent event with eventId.
|
||||||
* At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
* At the opposite of getTimelineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
||||||
* @param eventId the eventId to get the TimelineEvent
|
* @param eventId the eventId to get the TimelineEvent
|
||||||
*/
|
*/
|
||||||
fun getTimelineEvent(eventId: String): TimelineEvent?
|
fun getTimelineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
|
@ -434,6 +434,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
||||||
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unwedge if needed
|
||||||
|
try {
|
||||||
|
eventDecryptor.unwedgeDevicesIfNeeded()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
|
||||||
|
}
|
||||||
|
|
||||||
// There is a limit of to_device events returned per sync.
|
// There is a limit of to_device events returned per sync.
|
||||||
// If we are in a case of such limited to_device sync we can't try to generate/upload
|
// If we are in a case of such limited to_device sync we can't try to generate/upload
|
||||||
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
|
// new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
|
||||||
|
@ -723,7 +731,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
return internalDecryptEvent(event, timeline)
|
return internalDecryptEvent(event, timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -746,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @return the MXEventDecryptionResult data, or null in case of error
|
* @return the MXEventDecryptionResult data, or null in case of error
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
return eventDecryptor.decryptEvent(event, timeline)
|
return eventDecryptor.decryptEvent(event, timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1364,6 +1372,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
val cryptoStoreForTesting = cryptoStore
|
val cryptoStoreForTesting = cryptoStore
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
val olmDeviceForTest = olmDevice
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
|
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,13 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
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.EnsureOlmSessionsForDevicesAction
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -40,6 +39,8 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class EventDecryptor @Inject constructor(
|
internal class EventDecryptor @Inject constructor(
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
|
@ -47,13 +48,22 @@ internal class EventDecryptor @Inject constructor(
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
|
private val deviceListManager: DeviceListManager,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val cryptoStore: IMXCryptoStore
|
private val cryptoStore: IMXCryptoStore
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// The date of the last time we forced establishment
|
/**
|
||||||
// of a new session for each user:device.
|
* Rate limit unwedge attempt, should we persist that?
|
||||||
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
*/
|
||||||
|
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
|
||||||
|
|
||||||
|
data class WedgedDeviceInfo(
|
||||||
|
val userId: String,
|
||||||
|
val senderKey: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt an event
|
* Decrypt an event
|
||||||
|
@ -63,7 +73,7 @@ internal class EventDecryptor @Inject constructor(
|
||||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
return internalDecryptEvent(event, timeline)
|
return internalDecryptEvent(event, timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,38 +101,32 @@ internal class EventDecryptor @Inject constructor(
|
||||||
* @return the MXEventDecryptionResult data, or null in case of error
|
* @return the MXEventDecryptionResult data, or null in case of error
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
val eventContent = event.content
|
val eventContent = event.content
|
||||||
if (eventContent == null) {
|
if (eventContent == null) {
|
||||||
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||||
} else {
|
} else {
|
||||||
val algorithm = eventContent["algorithm"]?.toString()
|
val algorithm = eventContent["algorithm"]?.toString()
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||||
if (alg == null) {
|
if (alg == null) {
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||||
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
return alg.decryptEvent(event, timeline)
|
return alg.decryptEvent(event, timeline)
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
} catch (mxCryptoError: MXCryptoError) {
|
||||||
Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||||
if (mxCryptoError is MXCryptoError.Base &&
|
if (mxCryptoError is MXCryptoError.Base &&
|
||||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||||
// need to find sending device
|
// need to find sending device
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
val olmContent = event.content.toModel<OlmEventContent>()
|
||||||
val olmContent = event.content.toModel<OlmEventContent>()
|
if (event.senderId != null && olmContent?.senderKey != null) {
|
||||||
cryptoStore.getUserDevices(event.senderId ?: "")
|
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
||||||
?.values
|
} else {
|
||||||
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
||||||
?.let {
|
|
||||||
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,53 +136,91 @@ internal class EventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// coroutineDispatchers.crypto scope
|
private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
|
||||||
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
val info = WedgedDeviceInfo(senderId, senderKey)
|
||||||
val deviceKey = deviceInfo.identityKey()
|
if (!wedgedDevices.contains(info)) {
|
||||||
|
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
|
||||||
|
wedgedDevices.add(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
// 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 = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
val toUnwedge = wedgedDevices.filter {
|
||||||
Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
|
||||||
|
|
||||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
toUnwedge
|
||||||
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
.chunked(100) // safer to chunk if we ever have lots of wedged devices
|
||||||
|
.forEach { wedgedList ->
|
||||||
// offload this from crypto thread (?)
|
val groupedByUserId = wedgedList.groupBy { it.userId }
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
// lets download keys if needed
|
||||||
runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
|
withContext(coroutineDispatchers.io) {
|
||||||
onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
|
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
|
||||||
onFailure = {
|
|
||||||
Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap<MXOlmSessionResult>, deviceInfo: CryptoDeviceInfo, senderId: String) {
|
// find the matching devices
|
||||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
|
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 blank message on that session so the other side knows about it.
|
// Now send a dummy message on that session so the other side knows about it.
|
||||||
// (The keyshare request is sent in the clear so that won't do)
|
val payloadJson = mapOf(
|
||||||
// We send this first such that, as long as the toDevice messages arrive in the
|
"type" to EventType.DUMMY
|
||||||
// same order we sent them, the other end will get this first, set up the new session,
|
)
|
||||||
// then get the keyshare request and send the key over this new session (because it
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
// is the session it has most recently received a message on).
|
sessionToUse.map.values
|
||||||
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
// now let's send that
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
withContext(coroutineDispatchers.io) {
|
||||||
Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
|
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
||||||
withContext(coroutineDispatchers.io) {
|
}
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
} catch (failure: Throwable) {
|
||||||
try {
|
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
|
||||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
|
||||||
} catch (failure: Throwable) {
|
}
|
||||||
Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -28,6 +30,13 @@ import java.util.Timer
|
||||||
import java.util.TimerTask
|
import java.util.TimerTask
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class InboundGroupSessionHolder(
|
||||||
|
val wrapper: OlmInboundGroupSessionWrapper2,
|
||||||
|
val mutex: Mutex = Mutex()
|
||||||
|
)
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to cache and batch store operations on inbound group session store.
|
* 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
|
* Because it is used in the decrypt flow, that can be called quite rapidly
|
||||||
|
@ -42,12 +51,13 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
val senderKey: String
|
val senderKey: String
|
||||||
)
|
)
|
||||||
|
|
||||||
private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
|
private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
|
||||||
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
|
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
|
||||||
if (evicted && oldValue != null) {
|
if (oldValue != null) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
|
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
||||||
store.storeInboundGroupSessions(listOf(oldValue))
|
store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
|
||||||
|
oldValue.wrapper.olmInboundGroupSession?.releaseSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,27 +69,50 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
fun clear() {
|
||||||
synchronized(sessionCache) {
|
sessionCache.evictAll()
|
||||||
val known = sessionCache[CacheKey(sessionId, senderKey)]
|
|
||||||
Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
|
|
||||||
return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
|
|
||||||
Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
|
|
||||||
sessionCache.put(CacheKey(sessionId, senderKey), it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) {
|
fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
|
||||||
Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
|
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}")
|
||||||
|
dirtySession.remove(old.wrapper)
|
||||||
|
store.removeInboundGroupSession(sessionId, senderKey)
|
||||||
|
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||||
|
|
||||||
|
// release removed session
|
||||||
|
old.wrapper.olmInboundGroupSession?.releaseSession()
|
||||||
|
|
||||||
|
internalStoreGroupSession(new, 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}")
|
||||||
// We want to batch this a bit for performances
|
// We want to batch this a bit for performances
|
||||||
dirtySession.add(wrapper)
|
dirtySession.add(holder.wrapper)
|
||||||
|
|
||||||
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
||||||
// first time seen, put it in memory cache while waiting for batch insert
|
// 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
|
// If it's already known, no need to update cache it's already there
|
||||||
sessionCache.put(CacheKey(sessionId, senderKey), wrapper)
|
sessionCache.put(CacheKey(sessionId, senderKey), holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
timerTask?.cancel()
|
timerTask?.cancel()
|
||||||
|
@ -96,7 +129,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
||||||
dirtySession.clear()
|
dirtySession.clear()
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
|
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
store.storeInboundGroupSessions(toSave)
|
store.storeInboundGroupSessions(toSave)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
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.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
@ -40,6 +45,8 @@ import timber.log.Timber
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
// The libolm wrapper.
|
// The libolm wrapper.
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class MXOlmDevice @Inject constructor(
|
internal class MXOlmDevice @Inject constructor(
|
||||||
|
@ -47,9 +54,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* The store where crypto data is saved.
|
* The store where crypto data is saved.
|
||||||
*/
|
*/
|
||||||
private val store: IMXCryptoStore,
|
private val store: IMXCryptoStore,
|
||||||
|
private val olmSessionStore: OlmSessionStore,
|
||||||
private val inboundGroupSessionStore: InboundGroupSessionStore
|
private val inboundGroupSessionStore: InboundGroupSessionStore
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val mutex = Mutex()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the Curve25519 key for the account.
|
* @return the Curve25519 key for the account.
|
||||||
*/
|
*/
|
||||||
|
@ -93,26 +103,26 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
try {
|
try {
|
||||||
store.getOrCreateOlmAccount()
|
store.getOrCreateOlmAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmUtility = OlmUtility()
|
olmUtility = OlmUtility()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : OlmUtility failed with error")
|
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error")
|
||||||
olmUtility = null
|
olmUtility = null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +131,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
||||||
try {
|
try {
|
||||||
return store.getOlmAccount().oneTimeKeys()
|
return store.doWithOlmAccount { it.oneTimeKeys() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## getOneTimeKeys() : failed")
|
Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@ -133,7 +143,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return The maximum number of one-time keys the olm account can store.
|
* @return The maximum number of one-time keys the olm account can store.
|
||||||
*/
|
*/
|
||||||
fun getMaxNumberOfOneTimeKeys(): Long {
|
fun getMaxNumberOfOneTimeKeys(): Long {
|
||||||
return store.getOlmAccount().maxOneTimeKeys()
|
return store.doWithOlmAccount { it.maxOneTimeKeys() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,9 +153,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun getFallbackKey(): MutableMap<String, MutableMap<String, String>>? {
|
fun getFallbackKey(): MutableMap<String, MutableMap<String, String>>? {
|
||||||
try {
|
try {
|
||||||
return store.getOlmAccount().fallbackKey()
|
return store.doWithOlmAccount { it.fallbackKey() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("## getFallbackKey() : failed")
|
Timber.tag(loggerTag.value).e("## getFallbackKey() : failed")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -158,12 +168,14 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
fun generateFallbackKeyIfNeeded(): Boolean {
|
fun generateFallbackKeyIfNeeded(): Boolean {
|
||||||
try {
|
try {
|
||||||
if (!hasUnpublishedFallbackKey()) {
|
if (!hasUnpublishedFallbackKey()) {
|
||||||
store.getOlmAccount().generateFallbackKey()
|
store.doWithOlmAccount {
|
||||||
store.saveOlmAccount()
|
it.generateFallbackKey()
|
||||||
|
store.saveOlmAccount()
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("## generateFallbackKey() : failed")
|
Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed")
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -174,10 +186,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
fun forgetFallbackKey() {
|
fun forgetFallbackKey() {
|
||||||
try {
|
try {
|
||||||
store.getOlmAccount().forgetFallbackKey()
|
store.doWithOlmAccount {
|
||||||
store.saveOlmAccount()
|
it.forgetFallbackKey()
|
||||||
|
store.saveOlmAccount()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("## forgetFallbackKey() : failed")
|
Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +204,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
it.groupSession.releaseSession()
|
it.groupSession.releaseSession()
|
||||||
}
|
}
|
||||||
outboundGroupSessionCache.clear()
|
outboundGroupSessionCache.clear()
|
||||||
|
inboundGroupSessionStore.clear()
|
||||||
|
olmSessionStore.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -200,9 +216,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun signMessage(message: String): String? {
|
fun signMessage(message: String): String? {
|
||||||
try {
|
try {
|
||||||
return store.getOlmAccount().signMessage(message)
|
return store.doWithOlmAccount { it.signMessage(message) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## signMessage() : failed")
|
Timber.tag(loggerTag.value).e(e, "## signMessage() : failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@ -213,10 +229,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun markKeysAsPublished() {
|
fun markKeysAsPublished() {
|
||||||
try {
|
try {
|
||||||
store.getOlmAccount().markOneTimeKeysAsPublished()
|
store.doWithOlmAccount {
|
||||||
store.saveOlmAccount()
|
it.markOneTimeKeysAsPublished()
|
||||||
|
store.saveOlmAccount()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## markKeysAsPublished() : failed")
|
Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,10 +245,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun generateOneTimeKeys(numKeys: Int) {
|
fun generateOneTimeKeys(numKeys: Int) {
|
||||||
try {
|
try {
|
||||||
store.getOlmAccount().generateOneTimeKeys(numKeys)
|
store.doWithOlmAccount {
|
||||||
store.saveOlmAccount()
|
it.generateOneTimeKeys(numKeys)
|
||||||
|
store.saveOlmAccount()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## generateOneTimeKeys() : failed")
|
Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,12 +263,14 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return the session id for the outbound session.
|
* @return the session id for the outbound session.
|
||||||
*/
|
*/
|
||||||
fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
|
fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
|
||||||
Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
|
Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
|
||||||
var olmSession: OlmSession? = null
|
var olmSession: OlmSession? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
|
store.doWithOlmAccount { olmAccount ->
|
||||||
|
olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey)
|
||||||
|
}
|
||||||
|
|
||||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
||||||
|
|
||||||
|
@ -257,14 +279,14 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
// this session
|
// this session
|
||||||
olmSessionWrapper.onMessageReceived()
|
olmSessionWrapper.onMessageReceived()
|
||||||
|
|
||||||
store.storeSession(olmSessionWrapper, theirIdentityKey)
|
olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey)
|
||||||
|
|
||||||
val sessionIdentifier = olmSession.sessionIdentifier()
|
val sessionIdentifier = olmSession.sessionIdentifier()
|
||||||
|
|
||||||
Timber.v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier")
|
Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier")
|
||||||
return sessionIdentifier
|
return sessionIdentifier
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createOutboundSession() failed")
|
Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed")
|
||||||
|
|
||||||
olmSession?.releaseSession()
|
olmSession?.releaseSession()
|
||||||
}
|
}
|
||||||
|
@ -281,34 +303,38 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
|
* @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
|
||||||
*/
|
*/
|
||||||
fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? {
|
fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? {
|
||||||
Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
|
Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
|
||||||
|
|
||||||
var olmSession: OlmSession? = null
|
var olmSession: OlmSession? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
olmSession = OlmSession()
|
olmSession = OlmSession()
|
||||||
olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
|
store.doWithOlmAccount { olmAccount ->
|
||||||
|
olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : the session creation failed")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
store.getOlmAccount().removeOneTimeKeys(olmSession)
|
store.doWithOlmAccount { olmAccount ->
|
||||||
store.saveOlmAccount()
|
olmAccount.removeOneTimeKeys(olmSession)
|
||||||
|
store.saveOlmAccount()
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## createInboundSession() : ciphertext: $ciphertext")
|
Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
|
||||||
try {
|
try {
|
||||||
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
||||||
Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
||||||
}
|
}
|
||||||
|
|
||||||
val olmMessage = OlmMessage()
|
val olmMessage = OlmMessage()
|
||||||
|
@ -324,9 +350,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
// This counts as a received message: set last received message time to now
|
// This counts as a received message: set last received message time to now
|
||||||
olmSessionWrapper.onMessageReceived()
|
olmSessionWrapper.onMessageReceived()
|
||||||
|
|
||||||
store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : decryptMessage failed")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = HashMap<String, String>()
|
val res = HashMap<String, String>()
|
||||||
|
@ -343,7 +369,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
return res
|
return res
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## createInboundSession() : OlmSession creation failed")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed")
|
||||||
|
|
||||||
olmSession?.releaseSession()
|
olmSession?.releaseSession()
|
||||||
}
|
}
|
||||||
|
@ -357,8 +383,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
||||||
* @return a list of known session ids for the device.
|
* @return a list of known session ids for the device.
|
||||||
*/
|
*/
|
||||||
fun getSessionIds(theirDeviceIdentityKey: String): List<String>? {
|
fun getSessionIds(theirDeviceIdentityKey: String): List<String> {
|
||||||
return store.getDeviceSessionIds(theirDeviceIdentityKey)
|
return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -368,7 +394,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return the session id, or null if no established session.
|
* @return the session id, or null if no established session.
|
||||||
*/
|
*/
|
||||||
fun getSessionId(theirDeviceIdentityKey: String): String? {
|
fun getSessionId(theirDeviceIdentityKey: String): String? {
|
||||||
return store.getLastUsedSessionId(theirDeviceIdentityKey)
|
return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -379,30 +405,30 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param payloadString the payload to be encrypted and sent
|
* @param payloadString the payload to be encrypted and sent
|
||||||
* @return the cipher text
|
* @return the cipher text
|
||||||
*/
|
*/
|
||||||
fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
|
suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
|
||||||
var res: MutableMap<String, Any>? = null
|
|
||||||
val olmMessage: OlmMessage
|
|
||||||
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
||||||
|
|
||||||
if (olmSessionWrapper != null) {
|
if (olmSessionWrapper != null) {
|
||||||
try {
|
try {
|
||||||
Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
|
Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
|
||||||
// Timber.v("## encryptMessage() : payloadString: " + payloadString);
|
|
||||||
|
|
||||||
olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString)
|
val olmMessage = olmSessionWrapper.mutex.withLock {
|
||||||
store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
olmSessionWrapper.olmSession.encryptMessage(payloadString)
|
||||||
res = HashMap()
|
}
|
||||||
|
return mapOf(
|
||||||
res["body"] = olmMessage.mCipherText
|
"body" to olmMessage.mCipherText,
|
||||||
res["type"] = olmMessage.mType
|
"type" to olmMessage.mType,
|
||||||
} catch (e: Exception) {
|
).also {
|
||||||
Timber.e(e, "## encryptMessage() : failed")
|
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId")
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
|
Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -414,7 +440,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param sessionId the id of the active session.
|
* @param sessionId the id of the active session.
|
||||||
* @return the decrypted payload.
|
* @return the decrypted payload.
|
||||||
*/
|
*/
|
||||||
fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
|
@kotlin.jvm.Throws
|
||||||
|
suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
|
||||||
var payloadString: String? = null
|
var payloadString: String? = null
|
||||||
|
|
||||||
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
||||||
|
@ -424,13 +451,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
olmMessage.mCipherText = ciphertext
|
olmMessage.mCipherText = ciphertext
|
||||||
olmMessage.mType = messageType.toLong()
|
olmMessage.mType = messageType.toLong()
|
||||||
|
|
||||||
try {
|
payloadString =
|
||||||
payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage)
|
olmSessionWrapper.mutex.withLock {
|
||||||
olmSessionWrapper.onMessageReceived()
|
olmSessionWrapper.olmSession.decryptMessage(olmMessage).also {
|
||||||
store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
olmSessionWrapper.onMessageReceived()
|
||||||
} catch (e: Exception) {
|
}
|
||||||
Timber.e(e, "## decryptMessage() : decryptMessage failed")
|
}
|
||||||
}
|
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payloadString
|
return payloadString
|
||||||
|
@ -469,7 +496,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
|
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
|
||||||
return session.sessionIdentifier()
|
return session.sessionIdentifier()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "createOutboundGroupSession")
|
Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession")
|
||||||
|
|
||||||
session?.releaseSession()
|
session?.releaseSession()
|
||||||
}
|
}
|
||||||
|
@ -521,7 +548,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
try {
|
try {
|
||||||
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
|
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## getSessionKey() : failed")
|
Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -550,8 +577,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
|
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## encryptGroupMessage() : failed")
|
Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -578,52 +605,64 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
forwardingCurve25519KeyChain: List<String>,
|
||||||
keysClaimed: Map<String, String>,
|
keysClaimed: Map<String, String>,
|
||||||
exportFormat: Boolean): Boolean {
|
exportFormat: Boolean): Boolean {
|
||||||
val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
||||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
.fold(
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
{
|
// If we have an existing one we should check if the new one is not better
|
||||||
// If we already have this session, consider updating it
|
if (existingSession != null) {
|
||||||
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||||
|
try {
|
||||||
|
val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
|
||||||
|
// This is quite unexpected, could throw if native was released?
|
||||||
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||||
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
|
// Probably should discard it?
|
||||||
|
}
|
||||||
|
val newKnownFirstIndex = candidateSession.firstKnownIndex
|
||||||
|
// If our existing session is better we keep it
|
||||||
|
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||||
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||||
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||||
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val existingFirstKnown = it.firstKnownIndex!!
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
|
||||||
val newKnownFirstIndex = session.firstKnownIndex
|
|
||||||
|
|
||||||
// If our existing session is better we keep it
|
// sanity check on the new session
|
||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
||||||
session.olmInboundGroupSession?.releaseSession()
|
if (null == candidateOlmInboundSession) {
|
||||||
return false
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Nothing to do in case of error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (null == session.olmInboundGroupSession) {
|
|
||||||
Timber.e("## addInboundGroupSession : invalid session")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
|
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
||||||
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
session.olmInboundGroupSession!!.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
session.olmInboundGroupSession?.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
session.senderKey = senderKey
|
candidateSession.senderKey = senderKey
|
||||||
session.roomId = roomId
|
candidateSession.roomId = roomId
|
||||||
session.keysClaimed = keysClaimed
|
candidateSession.keysClaimed = keysClaimed
|
||||||
session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
||||||
|
|
||||||
inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
|
if (existingSession != null) {
|
||||||
// store.storeInboundGroupSessions(listOf(session))
|
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||||
|
} else {
|
||||||
|
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -638,57 +677,70 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
|
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
|
||||||
|
|
||||||
for (megolmSessionData in megolmSessionsData) {
|
for (megolmSessionData in megolmSessionsData) {
|
||||||
val sessionId = megolmSessionData.sessionId
|
val sessionId = megolmSessionData.sessionId ?: continue
|
||||||
val senderKey = megolmSessionData.senderKey
|
val senderKey = megolmSessionData.senderKey ?: continue
|
||||||
val roomId = megolmSessionData.roomId
|
val roomId = megolmSessionData.roomId
|
||||||
|
|
||||||
var session: OlmInboundGroupSessionWrapper2? = null
|
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = OlmInboundGroupSessionWrapper2(megolmSessionData)
|
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check
|
// sanity check
|
||||||
if (session?.olmInboundGroupSession == null) {
|
if (candidateSessionToImport?.olmInboundGroupSession == null) {
|
||||||
Timber.e("## importInboundGroupSession : invalid session")
|
Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
|
||||||
try {
|
try {
|
||||||
if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
||||||
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
candidateOlmInboundGroupSession?.releaseSession()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed")
|
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
|
||||||
session.olmInboundGroupSession!!.releaseSession()
|
candidateOlmInboundGroupSession?.releaseSession()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
.fold(
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
{
|
|
||||||
// If we already have this session, consider updating it
|
|
||||||
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
|
||||||
|
|
||||||
// For now we just ignore updates. TODO: implement something here
|
if (existingSession == null) {
|
||||||
if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
|
// Session does not already exist, add it
|
||||||
// Ignore this, keep existing
|
Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId")
|
||||||
session.olmInboundGroupSession!!.releaseSession()
|
sessions.add(candidateSessionToImport)
|
||||||
} else {
|
} else {
|
||||||
sessions.add(session)
|
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||||
}
|
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
|
||||||
Unit
|
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
|
||||||
},
|
|
||||||
{
|
|
||||||
// Session does not already exist, add it
|
|
||||||
sessions.add(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
|
||||||
|
// should not happen?
|
||||||
|
candidateSessionToImport.olmInboundGroupSession?.releaseSession()
|
||||||
|
Timber.tag(loggerTag.value)
|
||||||
|
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
|
||||||
|
} else {
|
||||||
|
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
|
||||||
|
// Ignore this, keep existing
|
||||||
|
candidateOlmInboundGroupSession.releaseSession()
|
||||||
|
} else {
|
||||||
|
// update cache with better session
|
||||||
|
inboundGroupSessionStore.replaceGroupSession(
|
||||||
|
existingSessionHolder,
|
||||||
|
InboundGroupSessionHolder(candidateSessionToImport),
|
||||||
|
sessionId,
|
||||||
|
senderKey
|
||||||
|
)
|
||||||
|
sessions.add(candidateSessionToImport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
store.storeInboundGroupSessions(sessions)
|
store.storeInboundGroupSessions(sessions)
|
||||||
|
@ -696,18 +748,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
return sessions
|
return sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an inbound group session
|
|
||||||
*
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param sessionKey base64-encoded secret key.
|
|
||||||
*/
|
|
||||||
fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) {
|
|
||||||
if (null != sessionId && null != sessionKey) {
|
|
||||||
store.removeInboundGroupSession(sessionId, sessionKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a received message with an inbound group session.
|
* Decrypt a received message with an inbound group session.
|
||||||
*
|
*
|
||||||
|
@ -719,19 +759,24 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return the decrypting result. Nil if the sessionId is unknown.
|
* @return the decrypting result. Nil if the sessionId is unknown.
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
fun decryptGroupMessage(body: String,
|
suspend fun decryptGroupMessage(body: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
timeline: String?,
|
timeline: String?,
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
senderKey: String): OlmDecryptionResult {
|
senderKey: String): OlmDecryptionResult {
|
||||||
val session = getInboundGroupSession(sessionId, senderKey, roomId)
|
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||||
|
val wrapper = sessionHolder.wrapper
|
||||||
|
val inboundGroupSession = wrapper.olmInboundGroupSession
|
||||||
|
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
|
||||||
// Check that the room id matches the original one for the session. This stops
|
// Check that the room id matches the original one for the session. This stops
|
||||||
// the HS pretending a message was targeting a different room.
|
// the HS pretending a message was targeting a different room.
|
||||||
if (roomId == session.roomId) {
|
if (roomId == wrapper.roomId) {
|
||||||
val decryptResult = try {
|
val decryptResult = try {
|
||||||
session.olmInboundGroupSession!!.decryptMessage(body)
|
sessionHolder.mutex.withLock {
|
||||||
|
inboundGroupSession.decryptMessage(body)
|
||||||
|
}
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||||
throw MXCryptoError.OlmError(e)
|
throw MXCryptoError.OlmError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -742,32 +787,32 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
if (timelineSet.contains(messageIndexKey)) {
|
if (timelineSet.contains(messageIndexKey)) {
|
||||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||||
Timber.e("## decryptGroupMessage() : $reason")
|
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineSet.add(messageIndexKey)
|
timelineSet.add(messageIndexKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
|
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
|
||||||
val payload = try {
|
val payload = try {
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||||
adapter.fromJson(payloadString)
|
adapter.fromJson(payloadString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
return OlmDecryptionResult(
|
return OlmDecryptionResult(
|
||||||
payload,
|
payload,
|
||||||
session.keysClaimed,
|
wrapper.keysClaimed,
|
||||||
senderKey,
|
senderKey,
|
||||||
session.forwardingCurve25519KeyChain
|
wrapper.forwardingCurve25519KeyChain
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
|
||||||
Timber.e("## decryptGroupMessage() : $reason")
|
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -819,7 +864,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
|
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
|
||||||
// sanity check
|
// sanity check
|
||||||
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
|
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
|
||||||
store.getDeviceSession(sessionId, theirDeviceIdentityKey)
|
olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -832,25 +877,26 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||||
* @return the inbound group session.
|
* @return the inbound group session.
|
||||||
*/
|
*/
|
||||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 {
|
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {
|
||||||
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
|
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
||||||
|
val session = holder?.wrapper
|
||||||
|
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Check that the room id matches the original one for the session. This stops
|
// Check that the room id matches the original one for the session. This stops
|
||||||
// the HS pretending a message was targeting a different room.
|
// the HS pretending a message was targeting a different room.
|
||||||
if (roomId != session.roomId) {
|
if (roomId != session.roomId) {
|
||||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
||||||
} else {
|
} else {
|
||||||
return session
|
return holder
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -866,4 +912,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
||||||
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
|
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun clearOlmSessionCache() {
|
||||||
|
olmSessionStore.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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)) {
|
||||||
|
persistedKnownSessions.add(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,14 +16,18 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
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.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
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.model.MXOlmSessionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toDebugString
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -31,90 +35,90 @@ private const val ONE_TIME_KEYS_RETRY_COUNT = 3
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
|
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
|
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
|
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> {
|
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||||
val devicesWithoutSession = ArrayList<CryptoDeviceInfo>()
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
// is there a session that as been already used?
|
||||||
|
val sessionId = olmDevice.getSessionId(key)
|
||||||
for ((userId, deviceList) in devicesByUser) {
|
if (sessionId.isNullOrEmpty()) {
|
||||||
for (deviceInfo in deviceList) {
|
Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
|
||||||
val deviceId = deviceInfo.deviceId
|
devicesToCreateSessionWith.add(deviceInfo)
|
||||||
val key = deviceInfo.identityKey()
|
} else {
|
||||||
if (key == null) {
|
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
|
||||||
Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key")
|
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
|
||||||
continue
|
results.setObject(userId, deviceId, olmSessionResult)
|
||||||
}
|
|
||||||
|
|
||||||
val sessionId = olmDevice.getSessionId(key)
|
|
||||||
|
|
||||||
if (sessionId.isNullOrEmpty() || force) {
|
|
||||||
Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)")
|
|
||||||
devicesWithoutSession.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" +
|
|
||||||
" ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}")
|
|
||||||
if (devicesWithoutSession.size == 0) {
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the request for claiming one-time keys
|
|
||||||
val usersDevicesToClaim = MXUsersDevicesMap<String>()
|
|
||||||
|
|
||||||
val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
|
|
||||||
|
|
||||||
for (device in devicesWithoutSession) {
|
|
||||||
usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this has a race condition - if we try to send another message
|
|
||||||
// while we are claiming a key, we will end up claiming two and setting up
|
|
||||||
// two sessions.
|
|
||||||
//
|
|
||||||
// That should eventually resolve itself, but it's poor form.
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}")
|
|
||||||
|
|
||||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
|
||||||
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT)
|
|
||||||
Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
|
|
||||||
for ((userId, deviceInfos) in devicesByUser) {
|
|
||||||
for (deviceInfo in deviceInfos) {
|
|
||||||
var oneTimeKey: MXKey? = null
|
|
||||||
val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
|
|
||||||
if (null != deviceIds) {
|
|
||||||
for (deviceId in deviceIds) {
|
|
||||||
val olmSessionResult = results.getObject(userId, deviceId)
|
|
||||||
if (olmSessionResult?.sessionId != null && !force) {
|
|
||||||
// We already have a result for this device
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val key = oneTimeKeys.getObject(userId, deviceId)
|
|
||||||
if (key?.type == oneTimeKeyAlgorithm) {
|
|
||||||
oneTimeKey = key
|
|
||||||
}
|
|
||||||
if (oneTimeKey == null) {
|
|
||||||
Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Update the result for this device in results
|
|
||||||
olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
val oneTimeKeys = withContext(coroutineDispatchers.io) {
|
||||||
|
oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.actions
|
package org.matrix.android.sdk.internal.crypto.actions
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
|
@ -28,6 +29,8 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
internal class MessageEncrypter @Inject constructor(
|
internal class MessageEncrypter @Inject constructor(
|
||||||
@UserId
|
@UserId
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
|
@ -42,7 +45,7 @@ internal class MessageEncrypter @Inject constructor(
|
||||||
* @param deviceInfos list of device infos to encrypt for.
|
* @param deviceInfos list of device infos to encrypt for.
|
||||||
* @return the content for an m.room.encrypted event.
|
* @return the content for an m.room.encrypted event.
|
||||||
*/
|
*/
|
||||||
fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
|
suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
|
||||||
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
||||||
|
|
||||||
val payloadJson = payloadFields.toMutableMap()
|
val payloadJson = payloadFields.toMutableMap()
|
||||||
|
@ -66,7 +69,7 @@ internal class MessageEncrypter @Inject constructor(
|
||||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
val sessionId = olmDevice.getSessionId(deviceKey)
|
||||||
|
|
||||||
if (!sessionId.isNullOrEmpty()) {
|
if (!sessionId.isNullOrEmpty()) {
|
||||||
Timber.v("Using sessionid $sessionId for device $deviceKey")
|
Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
|
||||||
|
|
||||||
payloadJson["recipient"] = deviceInfo.userId
|
payloadJson["recipient"] = deviceInfo.userId
|
||||||
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
||||||
|
|
|
@ -36,7 +36,7 @@ internal interface IMXDecrypting {
|
||||||
* @return the decryption information, or an error
|
* @return the decryption information, or an error
|
||||||
*/
|
*/
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a key event.
|
* Handle a key event.
|
||||||
|
|
|
@ -45,7 +45,7 @@ internal interface IMXGroupEncryption {
|
||||||
*
|
*
|
||||||
* @return true in case of success
|
* @return true in case of success
|
||||||
*/
|
*/
|
||||||
suspend fun reshareKey(sessionId: String,
|
suspend fun reshareKey(groupSessionId: String,
|
||||||
userId: String,
|
userId: String,
|
||||||
deviceId: String,
|
deviceId: String,
|
||||||
senderKey: String): Boolean
|
senderKey: String): Boolean
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
@ -71,7 +72,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
// If cross signing is enabled, we don't send request until the keys are trusted
|
// If cross signing is enabled, we don't send request until the keys are trusted
|
||||||
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
|
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
|
||||||
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
|
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
|
||||||
|
@ -79,7 +80,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
||||||
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
|
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
|
||||||
if (event.roomId.isNullOrBlank()) {
|
if (event.roomId.isNullOrBlank()) {
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||||
|
@ -345,7 +346,22 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val userId = request.userId ?: return
|
val userId = request.userId ?: return
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
val body = request.requestBody
|
||||||
|
val sessionHolder = try {
|
||||||
|
olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val export = sessionHolder.mutex.withLock {
|
||||||
|
sessionHolder.wrapper.exportKeys()
|
||||||
|
} ?: return@launch Unit.also {
|
||||||
|
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
|
||||||
|
}
|
||||||
|
|
||||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||||
.mapCatching {
|
.mapCatching {
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
|
@ -355,7 +371,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
} else {
|
} else {
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
val body = request.requestBody
|
|
||||||
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
||||||
if (olmSessionResult?.sessionId == null) {
|
if (olmSessionResult?.sessionId == null) {
|
||||||
// no session with this device, probably because there
|
// no session with this device, probably because there
|
||||||
|
@ -365,19 +380,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
}
|
}
|
||||||
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
|
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
|
||||||
|
|
||||||
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
val payloadJson = mapOf(
|
||||||
runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) }
|
"type" to EventType.FORWARDED_ROOM_KEY,
|
||||||
.fold(
|
"content" to export
|
||||||
{
|
)
|
||||||
// TODO
|
|
||||||
payloadJson["content"] = it.exportKeys() ?: ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// TODO
|
|
||||||
Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body")
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
|
|
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
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.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
@ -88,7 +90,7 @@ internal class MXMegolmEncryption(
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
|
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
|
||||||
val devices = getDevicesInRoom(userIds)
|
val devices = getDevicesInRoom(userIds)
|
||||||
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
|
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
|
Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
|
||||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
||||||
|
|
||||||
return encryptContent(outboundSession, eventType, eventContent)
|
return encryptContent(outboundSession, eventType, eventContent)
|
||||||
|
@ -142,8 +144,9 @@ internal class MXMegolmEncryption(
|
||||||
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
|
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
|
||||||
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
|
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
|
||||||
|
|
||||||
val keysClaimedMap = HashMap<String, String>()
|
val keysClaimedMap = mapOf(
|
||||||
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
"ed25519" to olmDevice.deviceEd25519Key!!
|
||||||
|
)
|
||||||
|
|
||||||
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
||||||
emptyList(), keysClaimedMap, false)
|
emptyList(), keysClaimedMap, false)
|
||||||
|
@ -303,11 +306,13 @@ internal class MXMegolmEncryption(
|
||||||
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
|
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||||
try {
|
try {
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
withContext(coroutineDispatchers.io) {
|
||||||
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
|
}
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// What to do here...
|
// What to do here...
|
||||||
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
|
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
||||||
|
@ -346,9 +351,12 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
sendToDeviceTask.execute(params)
|
withContext(coroutineDispatchers.io) {
|
||||||
|
sendToDeviceTask.execute(params)
|
||||||
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
|
Timber.tag(loggerTag.value)
|
||||||
|
.e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,20 +440,20 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun reshareKey(sessionId: String,
|
override suspend fun reshareKey(groupSessionId: String,
|
||||||
userId: String,
|
userId: String,
|
||||||
deviceId: String,
|
deviceId: String,
|
||||||
senderKey: String): Boolean {
|
senderKey: String): Boolean {
|
||||||
Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId")
|
Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
|
||||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
|
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
|
||||||
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
|
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
|
||||||
|
|
||||||
// Get the chain index of the key we previously sent this device
|
// Get the chain index of the key we previously sent this device
|
||||||
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo)
|
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
|
||||||
if (!wasSessionSharedWithUser.found) {
|
if (!wasSessionSharedWithUser.found) {
|
||||||
// This session was never shared with this user
|
// This session was never shared with this user
|
||||||
// Send a room key with held
|
// Send a room key with held
|
||||||
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
|
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -456,42 +464,47 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
val usersDeviceMap = try {
|
||||||
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys.
|
} catch (failure: Throwable) {
|
||||||
// ensureOlmSessionsForDevicesAction has already done the logging, so just skip it.
|
null
|
||||||
?: return false.also {
|
}
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
|
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}")
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
|
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 payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
val export = sessionHolder.mutex.withLock {
|
||||||
|
sessionHolder.wrapper.exportKeys()
|
||||||
|
} ?: return false.also {
|
||||||
|
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
|
||||||
|
}
|
||||||
|
|
||||||
runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
|
val payloadJson = mapOf(
|
||||||
.fold(
|
"type" to EventType.FORWARDED_ROOM_KEY,
|
||||||
{
|
"content" to export
|
||||||
// TODO
|
)
|
||||||
payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// TODO
|
|
||||||
Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId")
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId")
|
Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
return try {
|
return try {
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId")
|
Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
|
||||||
true
|
true
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
|
Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
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.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
@ -30,6 +32,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
|
||||||
internal class MXOlmDecryption(
|
internal class MXOlmDecryption(
|
||||||
// The olm device interface
|
// The olm device interface
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
|
@ -38,27 +41,27 @@ internal class MXOlmDecryption(
|
||||||
IMXDecrypting {
|
IMXDecrypting {
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
||||||
Timber.e("## decryptEvent() : bad event format")
|
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
||||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
|
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cipherText = olmEventContent.ciphertext ?: run {
|
val cipherText = olmEventContent.ciphertext ?: run {
|
||||||
Timber.e("## decryptEvent() : missing cipher text")
|
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val senderKey = olmEventContent.senderKey ?: run {
|
val senderKey = olmEventContent.senderKey ?: run {
|
||||||
Timber.e("## decryptEvent() : missing sender key")
|
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
||||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
|
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
||||||
Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
|
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)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ internal class MXOlmDecryption(
|
||||||
val decryptedPayload = decryptMessage(message, senderKey)
|
val decryptedPayload = decryptMessage(message, senderKey)
|
||||||
|
|
||||||
if (decryptedPayload == null) {
|
if (decryptedPayload == null) {
|
||||||
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
|
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)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||||
}
|
}
|
||||||
val payloadString = convertFromUTF8(decryptedPayload)
|
val payloadString = convertFromUTF8(decryptedPayload)
|
||||||
|
@ -78,30 +81,30 @@ internal class MXOlmDecryption(
|
||||||
val payload = adapter.fromJson(payloadString)
|
val payload = adapter.fromJson(payloadString)
|
||||||
|
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
Timber.e("## decryptEvent failed : null payload")
|
Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
|
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
|
||||||
Timber.e("## decryptEvent() : bad olmPayloadContent format")
|
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (olmPayloadContent.recipient.isNullOrBlank()) {
|
if (olmPayloadContent.recipient.isNullOrBlank()) {
|
||||||
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
||||||
Timber.e("## decryptEvent() : $reason")
|
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (olmPayloadContent.recipient != userId) {
|
if (olmPayloadContent.recipient != userId) {
|
||||||
Timber.e("## decryptEvent() : Event ${event.eventId}:" +
|
Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" +
|
||||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
|
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
|
||||||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
|
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
|
||||||
}
|
}
|
||||||
|
|
||||||
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
|
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
|
||||||
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
||||||
" property; cannot prevent unknown-key attack")
|
" property; cannot prevent unknown-key attack")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
|
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
|
||||||
|
@ -110,31 +113,34 @@ internal class MXOlmDecryption(
|
||||||
val ed25519 = recipientKeys["ed25519"]
|
val ed25519 = recipientKeys["ed25519"]
|
||||||
|
|
||||||
if (ed25519 != olmDevice.deviceEd25519Key) {
|
if (ed25519 != olmDevice.deviceEd25519Key) {
|
||||||
Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
|
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,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
|
||||||
MXCryptoError.BAD_RECIPIENT_KEY_REASON)
|
MXCryptoError.BAD_RECIPIENT_KEY_REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (olmPayloadContent.sender.isNullOrBlank()) {
|
if (olmPayloadContent.sender.isNullOrBlank()) {
|
||||||
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
|
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,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
|
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (olmPayloadContent.sender != event.senderId) {
|
if (olmPayloadContent.sender != event.senderId) {
|
||||||
Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported 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,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
||||||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
|
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (olmPayloadContent.roomId != event.roomId) {
|
if (olmPayloadContent.roomId != event.roomId) {
|
||||||
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${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,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
|
||||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
|
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
val keys = olmPayloadContent.keys ?: run {
|
val keys = olmPayloadContent.keys ?: run {
|
||||||
Timber.e("## decryptEvent failed : null keys")
|
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
||||||
}
|
}
|
||||||
|
@ -153,8 +159,8 @@ internal class MXOlmDecryption(
|
||||||
* @param message message object, with 'type' and 'body' fields.
|
* @param message message object, with 'type' and 'body' fields.
|
||||||
* @return payload, if decrypted successfully.
|
* @return payload, if decrypted successfully.
|
||||||
*/
|
*/
|
||||||
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
||||||
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
|
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
||||||
|
|
||||||
val messageBody = message["body"] as? String ?: return null
|
val messageBody = message["body"] as? String ?: return null
|
||||||
val messageType = when (val typeAsVoid = message["type"]) {
|
val messageType = when (val typeAsVoid = message["type"]) {
|
||||||
|
@ -166,11 +172,32 @@ internal class MXOlmDecryption(
|
||||||
|
|
||||||
// Try each session in turn
|
// Try each session in turn
|
||||||
// decryptionErrors = {};
|
// 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) {
|
for (sessionId in sessionIds) {
|
||||||
val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
|
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) {
|
if (null != payload) {
|
||||||
Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
||||||
return payload
|
return payload
|
||||||
} else {
|
} else {
|
||||||
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
|
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
|
||||||
|
@ -178,7 +205,7 @@ internal class MXOlmDecryption(
|
||||||
if (foundSession) {
|
if (foundSession) {
|
||||||
// Decryption failed, but it was a prekey message matching this
|
// Decryption failed, but it was a prekey message matching this
|
||||||
// session, so it should have worked.
|
// session, so it should have worked.
|
||||||
Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
|
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,9 +216,9 @@ internal class MXOlmDecryption(
|
||||||
// didn't work.
|
// didn't work.
|
||||||
|
|
||||||
if (sessionIds.isEmpty()) {
|
if (sessionIds.isEmpty()) {
|
||||||
Timber.e("## decryptMessage() : No existing sessions")
|
Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@ -199,14 +226,17 @@ internal class MXOlmDecryption(
|
||||||
|
|
||||||
// prekey message which doesn't match any existing sessions: make a new
|
// prekey message which doesn't match any existing sessions: make a new
|
||||||
// session.
|
// 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)
|
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
|
||||||
|
|
||||||
if (null == res) {
|
if (null == res) {
|
||||||
Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
|
Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
|
||||||
|
|
||||||
return res["payload"]
|
return res["payload"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
if (userList.isNotEmpty()) {
|
if (userList.isNotEmpty()) {
|
||||||
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
// 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 :/
|
// or a new device?) So we check all again :/
|
||||||
Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
|
Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
|
||||||
updateTrust(userList)
|
updateTrust(userList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
myUserId -> myTrustResult
|
myUserId -> myTrustResult
|
||||||
else -> {
|
else -> {
|
||||||
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
|
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
|
||||||
Timber.d("## CrossSigning - user:${entry.key} result:$it")
|
Timber.v("## CrossSigning - user:${entry.key} result:$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
// Update trust if needed
|
// Update trust if needed
|
||||||
devicesEntities?.forEach { device ->
|
devicesEntities?.forEach { device ->
|
||||||
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
||||||
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
||||||
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
||||||
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
||||||
// need to save
|
// need to save
|
||||||
|
@ -216,7 +216,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
|
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
?.let { roomSummary ->
|
?.let { roomSummary ->
|
||||||
Timber.d("## CrossSigning - Check shield state for room $roomId")
|
Timber.v("## CrossSigning - Check shield state for room $roomId")
|
||||||
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
|
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
|
||||||
try {
|
try {
|
||||||
val updatedTrust = computeRoomShield(
|
val updatedTrust = computeRoomShield(
|
||||||
|
@ -277,7 +277,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
|
||||||
cryptoRealm: Realm,
|
cryptoRealm: Realm,
|
||||||
activeMemberUserIds: List<String>,
|
activeMemberUserIds: List<String>,
|
||||||
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
||||||
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
|
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
|
||||||
// The set of “all users” depends on the type of room:
|
// 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 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
|
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||||
|
|
|
@ -671,7 +671,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||||
throw InvalidParameterException("Invalid recovery key")
|
throw InvalidParameterException("Invalid recovery key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a PK decryption instance
|
// Get a PK decryption instance
|
||||||
pkDecryptionFromRecoveryKey(recoveryKey)
|
pkDecryptionFromRecoveryKey(recoveryKey)
|
||||||
}
|
}
|
||||||
|
@ -681,6 +680,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
throw InvalidParameterException("Invalid recovery key")
|
throw InvalidParameterException("Invalid recovery key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save for next time and for gossiping
|
||||||
|
// Save now as it's valid, don't wait for the import as it could take long.
|
||||||
|
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
||||||
|
|
||||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||||
|
|
||||||
// Get backed up keys from the homeserver
|
// Get backed up keys from the homeserver
|
||||||
|
@ -729,8 +732,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
if (backUp) {
|
if (backUp) {
|
||||||
maybeBackupKeys()
|
maybeBackupKeys()
|
||||||
}
|
}
|
||||||
// Save for next time and for gossiping
|
|
||||||
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(callback)
|
||||||
|
|
|
@ -70,6 +70,8 @@ data class CryptoDeviceInfo(
|
||||||
keys?.let { map["keys"] = it }
|
keys?.let { map["keys"] = it }
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shortDebugString() = "$userId|$deviceId"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
|
internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.model
|
package org.matrix.android.sdk.internal.crypto.model
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import org.matrix.olm.OlmSession
|
import org.matrix.olm.OlmSession
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +26,10 @@ data class OlmSessionWrapper(
|
||||||
// The associated olm session.
|
// The associated olm session.
|
||||||
val olmSession: OlmSession,
|
val olmSession: OlmSession,
|
||||||
// Timestamp at which the session last received a message.
|
// Timestamp at which the session last received a message.
|
||||||
var lastReceivedMessageTs: Long = 0) {
|
var lastReceivedMessageTs: Long = 0,
|
||||||
|
|
||||||
|
val mutex: Mutex = Mutex()
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs`
|
* Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs`
|
||||||
|
|
|
@ -54,7 +54,7 @@ internal interface IMXCryptoStore {
|
||||||
/**
|
/**
|
||||||
* @return the olm account
|
* @return the olm account
|
||||||
*/
|
*/
|
||||||
fun getOlmAccount(): OlmAccount
|
fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T
|
||||||
|
|
||||||
fun getOrCreateOlmAccount(): OlmAccount
|
fun getOrCreateOlmAccount(): OlmAccount
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ internal interface IMXCryptoStore {
|
||||||
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
|
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the end-to-end session ids between the logged-in user and another
|
* Retrieve all end-to-end session ids between our own device and another
|
||||||
* device.
|
* device.
|
||||||
*
|
*
|
||||||
* @param deviceKey the public key of the other device.
|
* @param deviceKey the public key of the other device.
|
||||||
|
@ -270,7 +270,7 @@ internal interface IMXCryptoStore {
|
||||||
fun getDeviceSessionIds(deviceKey: String): List<String>?
|
fun getDeviceSessionIds(deviceKey: String): List<String>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve an end-to-end session between the logged-in user and another
|
* Retrieve an end-to-end session between our own device and another
|
||||||
* device.
|
* device.
|
||||||
*
|
*
|
||||||
* @param sessionId the session Id.
|
* @param sessionId the session Id.
|
||||||
|
|
|
@ -104,7 +104,6 @@ import timber.log.Timber
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class RealmCryptoStore @Inject constructor(
|
internal class RealmCryptoStore @Inject constructor(
|
||||||
|
@ -124,12 +123,6 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
// The olm account
|
// The olm account
|
||||||
private var olmAccount: OlmAccount? = null
|
private var olmAccount: OlmAccount? = null
|
||||||
|
|
||||||
// Cache for OlmSession, to release them properly
|
|
||||||
private val olmSessionsToRelease = HashMap<String, OlmSessionWrapper>()
|
|
||||||
|
|
||||||
// Cache for InboundGroupSession, to release them properly
|
|
||||||
private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper2>()
|
|
||||||
|
|
||||||
private val newSessionListeners = ArrayList<NewSessionListener>()
|
private val newSessionListeners = ArrayList<NewSessionListener>()
|
||||||
|
|
||||||
override fun addNewSessionListener(listener: NewSessionListener) {
|
override fun addNewSessionListener(listener: NewSessionListener) {
|
||||||
|
@ -213,16 +206,6 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
|
monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
olmSessionsToRelease.forEach {
|
|
||||||
it.value.olmSession.releaseSession()
|
|
||||||
}
|
|
||||||
olmSessionsToRelease.clear()
|
|
||||||
|
|
||||||
inboundGroupSessionToRelease.forEach {
|
|
||||||
it.value.olmInboundGroupSession?.releaseSession()
|
|
||||||
}
|
|
||||||
inboundGroupSessionToRelease.clear()
|
|
||||||
|
|
||||||
olmAccount?.releaseAccount()
|
olmAccount?.releaseAccount()
|
||||||
|
|
||||||
realmLocker?.close()
|
realmLocker?.close()
|
||||||
|
@ -247,10 +230,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOlmAccount(): OlmAccount {
|
/**
|
||||||
return olmAccount!!
|
* Olm account access should be synchronized
|
||||||
|
*/
|
||||||
|
override fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T {
|
||||||
|
return olmAccount!!.let { olmAccount ->
|
||||||
|
synchronized(olmAccount) {
|
||||||
|
block.invoke(olmAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
override fun getOrCreateOlmAccount(): OlmAccount {
|
override fun getOrCreateOlmAccount(): OlmAccount {
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
val metaData = it.where<CryptoMetadataEntity>().findFirst()
|
val metaData = it.where<CryptoMetadataEntity>().findFirst()
|
||||||
|
@ -680,13 +671,6 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
if (sessionIdentifier != null) {
|
if (sessionIdentifier != null) {
|
||||||
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
|
val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
|
||||||
|
|
||||||
// Release memory of previously known session, if it is not the same one
|
|
||||||
if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) {
|
|
||||||
olmSessionsToRelease[key]?.olmSession?.releaseSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
olmSessionsToRelease[key] = olmSessionWrapper
|
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
val realmOlmSession = OlmSessionEntity().apply {
|
val realmOlmSession = OlmSessionEntity().apply {
|
||||||
primaryKey = key
|
primaryKey = key
|
||||||
|
@ -703,23 +687,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
|
|
||||||
override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
||||||
val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
|
val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
|
||||||
|
return doRealmQueryAndCopy(realmConfiguration) {
|
||||||
// If not in cache (or not found), try to read it from realm
|
it.where<OlmSessionEntity>()
|
||||||
if (olmSessionsToRelease[key] == null) {
|
.equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
|
||||||
doRealmQueryAndCopy(realmConfiguration) {
|
.findFirst()
|
||||||
it.where<OlmSessionEntity>()
|
|
||||||
.equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
|
|
||||||
.findFirst()
|
|
||||||
}
|
|
||||||
?.let {
|
|
||||||
val olmSession = it.getOlmSession()
|
|
||||||
if (olmSession != null && it.sessionId != null) {
|
|
||||||
olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
?.let {
|
||||||
return olmSessionsToRelease[key]
|
val olmSession = it.getOlmSession()
|
||||||
|
if (olmSession != null && it.sessionId != null) {
|
||||||
|
return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastUsedSessionId(deviceKey: String): String? {
|
override fun getLastUsedSessionId(deviceKey: String): String? {
|
||||||
|
@ -761,13 +740,6 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
if (sessionIdentifier != null) {
|
if (sessionIdentifier != null) {
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
||||||
|
|
||||||
// Release memory of previously known session, if it is not the same one
|
|
||||||
if (inboundGroupSessionToRelease[key] != session) {
|
|
||||||
inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
inboundGroupSessionToRelease[key] = session
|
|
||||||
|
|
||||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||||
primaryKey = key
|
primaryKey = key
|
||||||
sessionId = sessionIdentifier
|
sessionId = sessionIdentifier
|
||||||
|
@ -784,20 +756,12 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
||||||
|
|
||||||
// If not in cache (or not found), try to read it from realm
|
return doWithRealm(realmConfiguration) {
|
||||||
if (inboundGroupSessionToRelease[key] == null) {
|
it.where<OlmInboundGroupSessionEntity>()
|
||||||
doWithRealm(realmConfiguration) {
|
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
.findFirst()
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
?.getInboundGroupSession()
|
||||||
.findFirst()
|
|
||||||
?.getInboundGroupSession()
|
|
||||||
}
|
|
||||||
?.let {
|
|
||||||
inboundGroupSessionToRelease[key] = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return inboundGroupSessionToRelease[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
|
override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
|
||||||
|
@ -853,10 +817,6 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
|
override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
||||||
|
|
||||||
// Release memory of previously known session
|
|
||||||
inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
|
|
||||||
inboundGroupSessionToRelease.remove(key)
|
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
it.where<OlmInboundGroupSessionEntity>()
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.legacy.riot;
|
package org.matrix.android.sdk.internal.legacy.riot;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
@ -196,6 +197,7 @@ public class LoginStorage {
|
||||||
/**
|
/**
|
||||||
* Clear the stored values
|
* Clear the stored values
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ApplySharedPref")
|
||||||
public void clear() {
|
public void clear() {
|
||||||
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
|
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
|
@ -21,6 +21,7 @@ internal object NetworkConstants {
|
||||||
private const val URI_API_PREFIX_PATH = "_matrix/client"
|
private const val URI_API_PREFIX_PATH = "_matrix/client"
|
||||||
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
|
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
|
||||||
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
|
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
|
||||||
|
const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
|
||||||
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
||||||
|
|
||||||
// Media
|
// Media
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
import org.matrix.android.sdk.api.session.room.RoomService
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
|
@ -109,6 +110,10 @@ internal class DefaultRoomService @Inject constructor(
|
||||||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> {
|
||||||
|
return roomSummaryDataSource.getCountFlow(queryParams)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||||
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
|
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
||||||
* Invoke the event decryption mechanism for a specific event
|
* Invoke the event decryption mechanism for a specific event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private fun decryptIfNeeded(event: Event, roomId: String) {
|
private suspend fun decryptIfNeeded(event: Event, roomId: String) {
|
||||||
try {
|
try {
|
||||||
// Event from sync does not have roomId, so add it to the event first
|
// Event from sync does not have roomId, so add it to the event first
|
||||||
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
||||||
|
|
|
@ -25,7 +25,13 @@ import androidx.paging.PagedList
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.toFlow
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||||
import org.matrix.android.sdk.api.query.isNormalized
|
import org.matrix.android.sdk.api.query.isNormalized
|
||||||
|
@ -42,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
|
||||||
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
|
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||||
|
@ -55,8 +62,10 @@ import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomSummaryDataSource @Inject constructor(
|
internal class RoomSummaryDataSource @Inject constructor(
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
private val realmSessionProvider: RealmSessionProvider,
|
||||||
private val roomSummaryMapper: RoomSummaryMapper,
|
private val roomSummaryMapper: RoomSummaryMapper,
|
||||||
private val queryStringValueProcessor: QueryStringValueProcessor
|
private val queryStringValueProcessor: QueryStringValueProcessor,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
||||||
|
@ -219,17 +228,29 @@ internal class RoomSummaryDataSource @Inject constructor(
|
||||||
return object : UpdatableLivePageResult {
|
return object : UpdatableLivePageResult {
|
||||||
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
|
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
|
||||||
|
|
||||||
override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
|
|
||||||
realmDataSourceFactory.updateQuery {
|
|
||||||
roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val liveBoundaries: LiveData<ResultBoundaries>
|
override val liveBoundaries: LiveData<ResultBoundaries>
|
||||||
get() = boundaries
|
get() = boundaries
|
||||||
|
|
||||||
|
override var queryParams: RoomSummaryQueryParams = queryParams
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
realmDataSourceFactory.updateQuery {
|
||||||
|
roomSummariesQuery(it, value).process(sortOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> =
|
||||||
|
realmSessionProvider
|
||||||
|
.withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() }
|
||||||
|
.toFlow()
|
||||||
|
// need to create the flow on a context dispatcher with a thread with attached Looper
|
||||||
|
.flowOn(coroutineDispatchers.main)
|
||||||
|
.map { it.size }
|
||||||
|
.flowOn(coroutineDispatchers.io)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||||
var notificationCount: RoomAggregateNotificationCount? = null
|
var notificationCount: RoomAggregateNotificationCount? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary
|
||||||
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
@ -165,7 +166,9 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||||
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
||||||
// mmm i want to decrypt now or is it ok to do it async?
|
// mmm i want to decrypt now or is it ok to do it async?
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
eventDecryptor.decryptEvent(root.asDomain(), "")
|
runBlocking {
|
||||||
|
eventDecryptor.decryptEvent(root.asDomain(), "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?.let { root.setDecryptionResult(it) }
|
?.let { root.setDecryptionResult(it) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,9 @@ internal class RealmSendingEventsDataSource(
|
||||||
|
|
||||||
private fun updateFrozenResults(sendingEvents: RealmList<TimelineEventEntity>?) {
|
private fun updateFrozenResults(sendingEvents: RealmList<TimelineEventEntity>?) {
|
||||||
// Makes sure to close the previous frozen realm
|
// Makes sure to close the previous frozen realm
|
||||||
frozenSendingTimelineEvents?.realm?.close()
|
if (frozenSendingTimelineEvents?.isValid == true) {
|
||||||
|
frozenSendingTimelineEvents?.realm?.close()
|
||||||
|
}
|
||||||
frozenSendingTimelineEvents = sendingEvents?.freeze()
|
frozenSendingTimelineEvents = sendingEvents?.freeze()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,13 +76,15 @@ internal class RealmSendingEventsDataSource(
|
||||||
val builtSendingEvents = mutableListOf<TimelineEvent>()
|
val builtSendingEvents = mutableListOf<TimelineEvent>()
|
||||||
uiEchoManager.getInMemorySendingEvents()
|
uiEchoManager.getInMemorySendingEvents()
|
||||||
.addWithUiEcho(builtSendingEvents)
|
.addWithUiEcho(builtSendingEvents)
|
||||||
frozenSendingTimelineEvents
|
if (frozenSendingTimelineEvents?.isValid == true) {
|
||||||
?.filter { timelineEvent ->
|
frozenSendingTimelineEvents
|
||||||
builtSendingEvents.none { it.eventId == timelineEvent.eventId }
|
?.filter { timelineEvent ->
|
||||||
}
|
builtSendingEvents.none { it.eventId == timelineEvent.eventId }
|
||||||
?.map {
|
}
|
||||||
timelineEventMapper.map(it)
|
?.map {
|
||||||
}?.addWithUiEcho(builtSendingEvents)
|
timelineEventMapper.map(it)
|
||||||
|
}?.addWithUiEcho(builtSendingEvents)
|
||||||
|
}
|
||||||
|
|
||||||
return builtSendingEvents
|
return builtSendingEvents
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,14 +144,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
||||||
|
|
||||||
val offsetCount = count - loadFromStorage.numberOfEvents
|
val offsetCount = count - loadFromStorage.numberOfEvents
|
||||||
|
|
||||||
return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
|
return if (offsetCount == 0) {
|
||||||
|
LoadMoreResult.SUCCESS
|
||||||
|
} else if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
|
||||||
LoadMoreResult.REACHED_END
|
LoadMoreResult.REACHED_END
|
||||||
} else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
|
} else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
|
||||||
LoadMoreResult.REACHED_END
|
LoadMoreResult.REACHED_END
|
||||||
} else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
|
} else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
|
||||||
LoadMoreResult.REACHED_END
|
LoadMoreResult.REACHED_END
|
||||||
} else if (offsetCount == 0) {
|
|
||||||
LoadMoreResult.SUCCESS
|
|
||||||
} else {
|
} else {
|
||||||
delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
|
delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
|
||||||
}
|
}
|
||||||
|
@ -508,13 +508,18 @@ private fun RealmQuery<TimelineEventEntity>.offsets(
|
||||||
count: Int,
|
count: Int,
|
||||||
startDisplayIndex: Int
|
startDisplayIndex: Int
|
||||||
): RealmQuery<TimelineEventEntity> {
|
): RealmQuery<TimelineEventEntity> {
|
||||||
sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
return if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
|
||||||
lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
limit(count.toLong())
|
||||||
} else {
|
} else {
|
||||||
greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
// We need to sort ascending first so limit works in the right direction
|
||||||
|
sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
|
limit(count.toLong())
|
||||||
|
// Result is expected to be sorted descending
|
||||||
|
sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
}
|
}
|
||||||
return limit(count.toLong())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
|
|
|
@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
||||||
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
@ -99,7 +100,9 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
}
|
}
|
||||||
executor?.execute {
|
executor?.execute {
|
||||||
Realm.getInstance(realmConfiguration).use { realm ->
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
processDecryptRequest(request, realm)
|
runBlocking {
|
||||||
|
processDecryptRequest(request, realm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,7 +118,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
|
private suspend fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
|
||||||
val event = request.event
|
val event = request.event
|
||||||
val timelineId = request.timelineId
|
val timelineId = request.timelineId
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue