Merge branch 'develop' into feature/aris/thread_live_thread_list

# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
This commit is contained in:
ariskotsomitopoulos 2022-03-10 12:55:13 +02:00
commit 21111922e6
215 changed files with 4089 additions and 1839 deletions

View File

@ -25,7 +25,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }} group: ${{ github.ref == 'refs/heads/develop' && format('integration-tests-develop-{0}-{1}', matrix.target, github.sha) || format('build-debug-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: | path: |
@ -49,7 +49,7 @@ jobs:
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
# Only runs on main, no concurrency. # Only runs on main, no concurrency.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: | path: |

View File

@ -7,5 +7,5 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# No concurrency required, this is a prerequisite to other actions and should run every time. # No concurrency required, this is a prerequisite to other actions and should run every time.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1

View File

@ -13,8 +13,6 @@ env:
CI_GRADLE_ARG_PROPERTIES: > CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false -Porg.gradle.parallel=false
-PallWarningsAsErrors=false
jobs: jobs:
# Build Android Tests [Matrix SDK] # Build Android Tests [Matrix SDK]
build-android-test-matrix-sdk: build-android-test-matrix-sdk:
@ -22,7 +20,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'adopt' distribution: 'adopt'
@ -44,7 +42,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
distribution: 'adopt' distribution: 'adopt'
@ -70,7 +68,7 @@ jobs:
api-level: [ 28 ] api-level: [ 28 ]
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2 - uses: actions/setup-java@v2
with: with:
@ -262,9 +260,7 @@ jobs:
api-level: [ 28 ] api-level: [ 28 ]
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with:
ref: develop
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
@ -301,7 +297,7 @@ jobs:
touch emulator.log touch emulator.log
chmod 777 emulator.log chmod 777 emulator.log
adb logcat >> emulator.log & adb logcat >> emulator.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 ) ./gradlew $CI_GRADLE_ARG_PROPERTIES connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
- name: Upload Test Report Log - name: Upload Test Report Log
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
if: always() if: always()
@ -311,12 +307,69 @@ jobs:
emulator.log emulator.log
failure_screenshots/ failure_screenshots/
codecov-units:
runs-on: macos-latest
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-
- run: ./gradlew allCodeCoverageReport $CI_GRADLE_ARG_PROPERTIES
- name: Upload Codecov data
uses: actions/upload-artifact@v2
if: always()
with:
name: codecov-xml
path: |
build/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml
sonarqube:
runs-on: macos-latest
if: always()
needs:
- codecov-units
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-
- uses: actions/download-artifact@v3
with:
name: codecov-xml # will restore to allCodeCoverageReport.xml by default; we restore to the same location in following tasks
- run: mkdir -p build/reports/jacoco/allCodeCoverageReport/
- run: mv allCodeCoverageReport.xml build/reports/jacoco/allCodeCoverageReport/
- run: ./gradlew sonarqube $CI_GRADLE_ARG_PROPERTIES
env:
ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
# Notify the channel about scheduled runs, do not notify for manually triggered runs # Notify the channel about scheduled runs, do not notify for manually triggered runs
notify: notify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- integration-tests - integration-tests
- ui-tests - ui-tests
# - unit-tests
- build-android-test-matrix-sdk
- build-android-test-app
- 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.
steps: steps:

View File

@ -10,7 +10,7 @@ jobs:
name: Project Check Suite name: Project Check Suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Run code quality check suite - name: Run code quality check suite
run: ./tools/check/check_code_quality.sh run: ./tools/check/check_code_quality.sh
@ -23,7 +23,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('ktlint-develop-{0}', github.sha) || format('ktlint-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Run ktlint - name: Run ktlint
run: | run: |
./gradlew ktlintCheck --continue ./gradlew ktlintCheck --continue
@ -96,7 +96,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('android-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('android-lint-develop-{0}', github.sha) || format('android-lint-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: | path: |
@ -129,7 +129,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }} group: ${{ github.ref == 'refs/heads/develop' && format('apk-lint-develop-{0}-{1}', matrix.target, github.sha) || format('apk-lint-{0}-{1}', matrix.target, github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: | path: |

View File

@ -11,7 +11,7 @@ jobs:
if: github.repository == 'vector-im/element-android' if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
@ -38,7 +38,7 @@ jobs:
if: github.repository == 'vector-im/element-android' if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.8 - name: Set up Python 3.8
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
@ -64,7 +64,7 @@ jobs:
if: github.repository == 'vector-im/element-android' if: github.repository == 'vector-im/element-android'
# No concurrency required, runs every time on a schedule. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Run analytics import script - name: Run analytics import script
run: ./tools/import_analytic_plan.sh run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan - name: Create Pull Request for analytics plan

View File

@ -20,7 +20,7 @@ jobs:
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('unit-tests-{0}', github.ref) }} 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('unit-tests-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
path: | path: |
@ -30,7 +30,10 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Run unit tests - name: Run unit tests
run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false --stacktrace run: ./gradlew clean test $CI_GRADLE_ARG_PROPERTIES --stacktrace
- name: Format unit test results
if: always()
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml
- name: Publish Unit Test Results - name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1 uses: EnricoMi/publish-unit-test-result-action@v1
if: always() && if: always() &&

View File

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Update Gradle Wrapper - name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1 uses: gradle-update/update-gradle-wrapper-action@v1

View File

@ -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!)
====================================================== ======================================================

View File

@ -105,11 +105,21 @@ task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
def launchTask = getGradle()
.getStartParameter()
.getTaskRequests()
.toString()
.toLowerCase()
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
apply from: 'coverage.gradle'
}
apply plugin: 'org.sonarqube' apply plugin: 'org.sonarqube'
// To run a sonar analysis: // To run a sonar analysis:
// Run './gradlew sonarqube -Dsonar.login=<REPLACE_WITH_SONAR_KEY>' // Run './gradlew sonarqube -Dsonar.login=<REPLACE_WITH_SONAR_KEY>'
// The SONAR_KEY is stored in passbolt // The SONAR_KEY is stored in passbolt as Token Sonar Cloud Bma
sonarqube { sonarqube {
properties { properties {
@ -119,10 +129,12 @@ sonarqube {
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8" property "sonar.sourceEncoding", "UTF-8"
property "sonar.links.homepage", "https://github.com/vector-im/element-android/" property "sonar.links.homepage", "https://github.com/vector-im/element-android/"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android" property "sonar.links.ci", "https://github.com/vector-im/element-android/actions"
property "sonar.links.scm", "https://github.com/vector-im/element-android/" property "sonar.links.scm", "https://github.com/vector-im/element-android/"
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues" property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization" property "sonar.organization", "new_vector_ltd_organization"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.coverage.jacoco.xmlReportPaths", "${project.buildDir}/reports/jacoco/allCodeCoverageReport/allCodeCoverageReport.xml"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid" property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
} }
} }

View File

@ -1 +0,0 @@
Typing notifications moved from the header to the bottom of the timeline.

View File

@ -1 +0,0 @@
Open direct message screen when clicking on DM button in the space members list

View File

@ -1 +0,0 @@
Add possibility to save media from Gallery + reorder choices in message context menu

View File

@ -1 +0,0 @@
Starts the FTUE account personalisation flow by adding an account created screen behind a feature flag

1
changelog.d/5260.misc Normal file
View File

@ -0,0 +1 @@
Number of unread messages on space badge now include number of unread DMs

View File

@ -1 +0,0 @@
Tentatively fixing the UI sanity test being unable to click on the space menu items

1
changelog.d/5270.misc Normal file
View File

@ -0,0 +1 @@
Amend spaces menu to be consistent with iOS version

View File

@ -1 +0,0 @@
Improve Bubble layouts rendering.

View File

@ -1 +0,0 @@
Moves attachment-viewer, diff-match-patch, and multipicker modules to subfolders under library

View File

@ -1 +0,0 @@
Log the `since` token used and `next_batch` token returned when doing an incremental sync.

View File

@ -1 +0,0 @@
Update reaction button layout.

View File

@ -1 +0,0 @@
Notify element-android channel each time a nightly build completes.

View File

@ -1 +0,0 @@
Log the `since` token used and `next_batch` token returned when doing an incremental sync.

View File

@ -1 +0,0 @@
Adds forceLoginFallback feature flag and usages to FTUE login and registration

View File

@ -1 +0,0 @@
[Export e2ee keys] use appName instead of element

View File

@ -1 +0,0 @@
Continue improving realm usage.

View File

@ -1 +0,0 @@
Change name of getTimeLineEvent and getTimeLineEventLive methods to getTimelineEvent and getTimelineEventLive.

View File

@ -1 +0,0 @@
Upgrade the plugin which generate strings with template from 1.2.2 to 2.0.0

View File

@ -1 +0,0 @@
Remove about 700 unused strings and their translations

View File

@ -1 +0,0 @@
Creates dedicated VectorOverrides for forcing behaviour for local testing/development

View File

@ -1 +0,0 @@
Cleanup unused threads build configurations

View File

@ -1 +0,0 @@
Upgrades material dependency version from 1.4.0 to 1.5.0

View File

@ -1 +0,0 @@
Fix incorrect media cache size in settings

1
changelog.d/5395.feature Normal file
View File

@ -0,0 +1 @@
Add a custom view to display a picker for share location options

55
coverage.gradle Normal file
View File

@ -0,0 +1,55 @@
def excludes = [ ]
def initializeReport(report, projects, classExcludes) {
projects.each { project -> project.apply plugin: 'jacoco' }
report.executionData { fileTree(rootProject.rootDir.absolutePath).include("**/build/jacoco/*.exec") }
report.reports {
xml.enabled true
html.enabled true
csv.enabled false
}
gradle.projectsEvaluated {
def androidSourceDirs = []
def androidClassDirs = []
projects.each { project ->
switch (project) {
case { project.plugins.hasPlugin("com.android.application") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug")
androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java")
break
case { project.plugins.hasPlugin("com.android.library") }:
androidClassDirs.add("${project.buildDir}/tmp/kotlin-classes/debug")
androidSourceDirs.add("${project.projectDir}/src/main/kotlin")
androidSourceDirs.add("${project.projectDir}/src/main/java")
break
default:
report.sourceSets project.sourceSets.main
}
}
report.sourceDirectories.setFrom(report.sourceDirectories + files(androidSourceDirs))
def classFiles = androidClassDirs.collect { files(it).files }.flatten()
report.classDirectories.setFrom(files((report.classDirectories.files + classFiles).collect {
fileTree(dir: it, excludes: classExcludes)
}))
}
}
def collectProjects(predicate) {
return subprojects.findAll { it.buildFile.isFile() && predicate(it) }
}
task allCodeCoverageReport(type: JacocoReport) {
outputs.upToDateWhen { false }
rootProject.apply plugin: 'jacoco'
// to limit projects in a specific report, add
// def excludedProjects = [ ... ]
// def projects = collectProjects { !excludedProjects.contains(it.name) }
def projects = collectProjects { true }
dependsOn { projects*.test }
initializeReport(it, projects, excludes)
}

View File

@ -29,9 +29,10 @@ ext.groups = [
'com\\.google\\.android\\..*', 'com\\.google\\.android\\..*',
], ],
group: [ group: [
'com.google.firebase',
'com.android', 'com.android',
'com.android.tools', 'com.android.tools',
'com.google.firebase',
'com.google.testing.platform',
] ]
], ],
mavenCentral: [ mavenCentral: [
@ -63,6 +64,8 @@ ext.groups = [
'com.github.piasy', 'com.github.piasy',
'com.github.shyiko.klob', 'com.github.shyiko.klob',
'com.google', 'com.google',
'com.google.android',
'com.google.api.grpc',
'com.google.auto.service', 'com.google.auto.service',
'com.google.auto.value', 'com.google.auto.value',
'com.google.code.findbugs', 'com.google.code.findbugs',
@ -111,10 +114,13 @@ ext.groups = [
'io.arrow-kt', 'io.arrow-kt',
'io.github.detekt.sarif4k', 'io.github.detekt.sarif4k',
'io.github.reactivecircus.flowbinding', 'io.github.reactivecircus.flowbinding',
'io.grpc',
'io.jsonwebtoken', 'io.jsonwebtoken',
'io.kindedj', 'io.kindedj',
'io.mockk', 'io.mockk',
'io.netty',
'io.noties.markwon', 'io.noties.markwon',
'io.opencensus',
'io.reactivex.rxjava2', 'io.reactivex.rxjava2',
'io.realm', 'io.realm',
'it.unimi.dsi', 'it.unimi.dsi',
@ -150,6 +156,7 @@ ext.groups = [
'org.ec4j.core', 'org.ec4j.core',
'org.glassfish.jaxb', 'org.glassfish.jaxb',
'org.hamcrest', 'org.hamcrest',
'org.jacoco',
'org.jetbrains', 'org.jetbrains',
'org.jetbrains.intellij.deps', 'org.jetbrains.intellij.deps',
'org.jetbrains.kotlin', 'org.jetbrains.kotlin',

View File

@ -0,0 +1,2 @@
Neues: Erstelle Threads, damit dein Chatverlauf nicht zugespammt wird. Nachrichtenblasen.
Ganze Änderungsliste: https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@ -0,0 +1,2 @@
Neues: Unterstützung für @room, Verbesserungen der Abstimmungen und weitere kleine Änderungen
Ganzer Changelog: https://github.com/vector-im/element-android/releases/tag/v1.4.2

View File

@ -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

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.17

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : envoyer votre position dans n'importe quel salon. Éditer un sondage.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.3.18

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : Implémentation initial des fils de discussion. Bulles de messages.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@ -0,0 +1,2 @@
Principaux changements pour cette version : Ajout du support pour @room et des sondages non terminé parmi plein d'autres changements mineurs.
Intégralité des changements : https://github.com/vector-im/element-android/releases/tag/v1.4.2

View File

@ -1,43 +1,42 @@
Element egy biztonságos üzenetküldő és csapatmunka támogató alkalmazás ami ideális távoli munkavégzés közben csoportos csevegéshez. Az alkalmazás végpontok közötti titkosítást használ videó konferenciához, fájl megosztáshoz és videó hivásokhoz. Az Element egy biztonságos üzenetküldő, és egy csapatmunka app, amely távoli munkavégzéshez is alkalmas lehet. Az alkalmazás végponti titkosítás használatával biztosít videó konferencia, fájlmegosztás, és audio hívás lehetőségeket.
<b>Element tulajdonságai:</b> <b>Az Element funkciói többek között:</b>
- Fejlett online kommunikációs eszköz - Fejlett online kommunikációs eszközök
- Teljesen titkosított üzenetküldés biztonságos céges kommunikációt kínál még a távdolgozóknak is - Titkosított üzenetek a biztonságos céges kommunikációhoz, otthonról dolgozóknak is
- Elosztott csevegés a Matrix nyílt forráskódú keretrendszer felhasználásával - Decentralizált chat a nyílt forráskódú Matrix protokoll használatával
- Bizontságos fájl megosztás titkosítottan projektek kezeléséhez - Biztonságos fájlmegosztáss a projektek kezeléséhez
- Videó hívás VoIP-pal és képernyőmegosztással - Videochat, VoIP, és képernyőmegosztási lehetőséggel
- Könnyen integrálható a kedvenc online kollaborációs eszközöddel, projekt menedzsment eszközzel, VoIP szolgáltatással vagy más csoport üzenetküldő alkalmazással - Egyszerű integráció a kedvenc online kollaborációs eszközeiddel, projektkezelési eszközökkel, VoIP szolgáltatásokkal, és más csoportos üzenetküldő alkalmazásokkal
Element teljesen más mint a többi üzenetküldő alkalmazás. Matrixot használ, egy nyílt hálózatot a decentralizált biztonságos kommunikációhoz. Lehetőséget ad saját szerver üzemeltetésére ami maximális tulajdont és kontrollt biztosít az adatok fölött. Element is completely different from other messaging and collaboration apps. It operates on Matrix, an open network for secure messaging and decentralized communication. It allows self-hosting to give users maximum ownership and control of their data and messages.
<b>Magánélet védelme és titkosított üzenetküldés</b> <b>Privacy and encrypted messaging</b>
Element megóv a kéretlen hirdetésektől, adatbányászattól és a különböző szigetszerű megoldásoktól. Minden adatot biztonságba helyez, egy az egybe videó és hang kommunikáció végpontok között titkosítva ahol az eszközök hitelesítve vannak. Element protects you from unwanted ads, data mining and walled gardens. It also secures all your data, one-to-one video and voice communication through end-to-end encryption and cross-signed device verification.
Element a kezedbe adja az adatvédelmi irányítást miközben bárkivel kommunikálhatsz a Matrix hálózatban vagy más üzleti kollaborációs eszközzel ami integrálva van, mint amilyen a Slack. Element gives you control over your privacy while allowing you to communicate securely with anyone on the Matrix network, or other business collaboration tools by integrating with apps such as Slack.
<b>Element futtatható saját szerveren</b> <b>Element can be self-hosted</b>
To allow more control of your sensitive data and conversations, Element can be self-hosted or you can choose any Matrix-based host - the standard for open source, decentralized communication. Element gives you privacy, security compliance and integration flexibility.
Azért, hogy az érzékeny adatok és beszélgetések minnél inkább az irányításod alatt lehessen az Elementet saját magadnak üzemeltetheted vagy választhatsz bármely Matrixon alapuló - szabványos nyílt forráskódú és decentralizált kommunikáció - szoláltató közül. Element adatvédelmet, biztonságot és rugalmas integrációkat biztosít. <b>Own your data</b>
You decide where to keep your data and messages. Without the risk of data mining or access from third parties.
<b>A te adatod a tiéd</b> Element puts you in control in different ways:
Te döntöd el, hogy hol tárolod az adataidat és üzeneteidet. Adatbányászat vagy harmadik fél hozzáférésének kockázata nélkül. 1. Get a free account on the matrix.org public server hosted by the Matrix developers, or choose from thousands of public servers hosted by volunteers
2. Self-host your account by running a server on your own IT infrastructure
3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform
Element többféle képpen adja vissza az irányítást: <b>Open messaging and collaboration</b>
1. Szerezz egy ingyenes hozzáférést a matrix.org nyilvános szerverre amit a Matrix fejlesztők üzemeltetnek vagy válassz a több ezer önkéntesek által üzemeltetett nyilvános szerverből You can chat with anyone on the Matrix network, whether theyre using Element, another Matrix app or even if they are using a different messaging app.
2. Üzemeltess szerver magadnak a saját infrastruktúrádon
3. Iratkozz fel egy egyedi szerverre az Element Matrix Services platformon
<b>Nyílt üzenetküldés és kollaboráció</b> <b>Super secure</b>
Bárkivel beszélgethetsz a Matrix hálózaton, akár az Elementet használja akár egy másik Matrix alkalmazást használ vagy akár egy eltérő üzenetküldőt. Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signed device verification.
<b>Fantasztikusan biztonságos</b> <b>Complete communication and integration</b>
Igazi végpontok között titkosítás (csak a beszélgetésben résztvevők tudják visszafejteni) és hitelesítés eszközök közötti aláírásokkal. Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.
<b>Teljes kommunikáció és integráció</b> <b>Pick up where you left off</b>
Üzenetküldés, hang és videóhívás, fájl megosztás, képernyő megosztás és egy csomó integráció, botok és kisalkalmazások. Építs szobákat, közösségeket, maradj kapcsolatban és végezz el dolgokat. Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://app.element.io
<b>Vedd fel a fonalat</b> <b>Open source</b>
Maradj kapcsolatban bárhol minden eszközödön a szinkronizált üzenetekkel és a weben a https://app.element.io oldallal Element Android is an open source project, hosted by GitHub. Please report bugs and/or contribute to its development at https://github.com/vector-im/element-android
<b>Nyílt forráskód</b>
Element Android egy nyílt forráskódú projekt a GitHubon. Küldj hibajegyet és/vagy vegyél részt a fejlesztésében itt: https://github.com/vector-im/element-android

View File

@ -1 +1 @@
Csoportos üzenetküldő - titkosított üzenetek, videó hívások Csoportos üzenetküldő - titkosított üzenetek és videó hívások

View File

@ -1 +1 @@
Element Element - Biztonságos üzenetküldő

View File

@ -1,2 +1,2 @@
今回の新バージョンでは、主にバグの修正と改善が行われています。メッセージの送信がより速くなりました。 今回の新バージョンでは、主にバグの修正と改善が行われています。メッセージの送信がより速くなりました。
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.10 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -1,2 +1,2 @@
今回の新バージョンでは、主にUIユーザーインターフェースとUXユーザーエクスペリエンスの向上が図られています。友達を招待したり、QRコードを読み取って素早くDMを作成できるようになりました。 今回の新バージョンでは、主にUIユーザーインターフェースとUXユーザーエクスペリエンスの向上が図られています。友達を招待したり、QRコードを読み取って素早くDMを作成できるようになりました。
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.11 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が このバージョンの主な変更点URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.12 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.12

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が このバージョンの主な変更点URLプレビュー、新しい絵文字、新しいルーム設定機能、それにクリスマスには雪が
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.13 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.13

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: 部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。 このバージョンの主な変更点部屋の許可、自動のテーマ切替、そして多くのバグを修正しました。
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.14 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.14

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: ソーシャルログインに対応しました。 このバージョンの主な変更点ソーシャルログインに対応しました。
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: パフォーマンスの向上とバグの修正 このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: バグの修正 このバージョンの主な変更点:バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.0.17 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.0.17

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: パフォーマンスの向上とバグの修正 このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.0 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.0

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: パフォーマンスの向上とバグの修正 このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.1 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.1

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: パフォーマンスの向上とバグの修正 このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.2 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.2

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点: パフォーマンスの向上とバグの修正 このバージョンの主な変更点:パフォーマンスの向上と、バグを修正しました
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.3 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.3

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:パフォーマンスの向上と不具合の修正
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.1.4

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:テーマ、スタイルの更新と、スペースに関する新機能。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.1.10

View File

@ -1,2 +1,2 @@
このバージョンの主な変更点:ルームにて誰かがログアウトした際に発生するエラーを修正しました。 このバージョンの主な変更点ルームにて誰かがログアウトした際に発生するエラーを修正しました。
全ての変更履歴はこちら: https://github.com/vector-im/element-android/releases/tag/v1.1.16 更新履歴:https://github.com/vector-im/element-android/releases/tag/v1.1.16

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:主に通知に関する不具合の修正。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.7-RC2

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:不具合の修正
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.8

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:音声メッセージの下書き機能の追加。不具合の修正。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.9

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点アンケート機能のサポート実験的。URL プレビューの新規デザイン。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.10

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:不具合の修正
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.11

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:不具合の修正
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.12

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点登録時の表示に関する変更Analyticsへのオプトインなど。数学に関するイベントをラボに追加。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.13

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点登録時の表示に関する変更Analyticsへのオプトインなど。数学に関するイベントをラボに追加。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.14

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点登録時の表示に関する変更Analyticsへのオプトインなど。数学に関するイベントをラボに追加。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.3.15

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:スレッド機能の実装、吹き出しメッセージ。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@ -0,0 +1,2 @@
このバージョンの主な変更点:@roomの対応、非公開の投票など。
更新履歴https://github.com/vector-im/element-android/releases/tag/v1.4.2

View File

@ -1,42 +1,42 @@
Elementは、安全なメッセンジャー、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンドツーエンドの暗号化を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。 Elementは、安全なメッセージングアプリ、リモートワーク中のグループチャットに適したチームコラボレーションアプリです。エンドツーエンドの暗号化技術を使用して、強力なビデオ会議、ファイル共有、音声通話を提供します。
<b>Elementの特徴</b> <b>Elementの特徴</b>
- 高度なオンラインコミュニケーションツール - 高度なオンラインコミュニケーションツール
- 完全に暗号化されたメッセージにより、リモートワーカーでも、より安全な企業コミュニケーションが可能 - メッセージの完全な暗号化。リモートワーカーでも、より安全な企業コミュニケーションが可能
- Matrixオープンソースフレームワークをベースにした分散型のチャット - Matrixオープンソースフレームワークに基づく、分散型のチャット
- プロジェクトを管理しながら、暗号化されたデータで安全にファイル共有 - プロジェクトの管理と並行して、データの暗号化によりファイルを安全に共有することが可能
- Voice over IPによるビデオチャットと画面共有 - Voice over IPによるビデオチャットと画面共有
- お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能 - お気に入りのオンラインコラボレーションツール、プロジェクト管理ツール、VoIPサービス、その他のチームメッセージングアプリと簡単に統合可能
Elementは他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型非中央集権コミュニケーションのためのオープンネットワークであるMatrixで動作します。ユーザーが自分のデータやメッセージを最大限にコントロールできるように、セルフホスティングも可能です。 Elementは他のメッセージングアプリやコラボレーションアプリとは全く異なります。安全なメッセージングと分散型(非中央集権コミュニケーションのためのオープンネットワークであるMatrixで動作します。自分のデータやメッセージを最大限にコントロールするために、あなた自身がサーバーを運営することもできます。
<b>プライバシーと暗号化されたコミュニケーション</b> <b>プライバシーと暗号化されたコミュニケーション</b>
Elementは、望ましくない広告、データマイニング、ウォールドガーデンからユーザーを保護します。また、エンド・ツー・エンドの暗号化と相互署名された端末の検証により、全てのデータ、1対1のビデオおよび音声通信を保護します。 Elementは、望ましくない広告、データマイニング、囲い込みからユーザーを守ります。また、エンド・ツー・エンドの暗号化と、相互署名による端末の認証に基づき、全てのデータ、ビデオ会議、音声通信を保護します。
Elementは、Slackなどのアプリと統合することで、Matrixネットワーク上の誰とでも安全にコミュニケーションを取ることができると同時に、プライバシーをコントロールすることができます。 Elementでは、Matrixネットワークにいる誰とでもコミュニケーションが行えるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うとともに、プライバシーをコントロールすることができます。
<b>Elementはセルフホスティングが可能</b> <b>セルフホスティングが可能</b>
機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixベースのホストを選択することもできます。Elementは、プライバシー、セキュリティーコンプライアンス、および統合の柔軟性を提供します。 機密データや会話の管理を強化するために、Elementはセルフホスティングが可能です。または、オープンソースの分散型コミュニケーションの標準であるMatrixに基づくサーバーを選ぶこともできます。Elementは、プライバシー、セキュリティーコンプライアンス、および柔軟な機能統合を提供します。
<b>自分のデータを所有する</b> <b>自分のデータを所有する</b>
データやメッセージをどこに保管するかは、ユーザー自身が決めることができます。データマイニングやサードパーティからのアクセスのリスクはありません。 データやメッセージを保管する場所を自分で決めることができます。データマイニングや第三者へのデータ流出のリスクはありません。
Elementでは、どのサーバーを使うかをご自身で決めることができます。 Elementでは、どのサーバーを使うかをご自身で決めることができます。
1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得するか、ボランティアがホストしているパブリックサーバーから選択する 1. 開発者が運営する matrix.org の公開サーバーで無料アカウントを取得するか、ボランティアが管理している運営サーバーから選ぶ
2. あなた自身がサーバーを運営し、アカウントを管理する。 2. あなた自身がサーバーを運営し、アカウントを管理する。
3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。 3. Element Matrix Servicesの運営プラットフォームに加入し、カスタムサーバー上でアカウントを作る。
<b>オープンなメッセージングとコラボレーション</b> <b>オープンなメッセージングとコラボレーション</b>
Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらには他のメッセージングアプリを使っているかに関わらず、チャットをすることができます。 相手がElement、他のMatrixアプリ、さらには他のメッセージングアプリを使っているかに関わらず、Matrixネットワーク上の誰とでもチャットをすることができます。
<b>非常に安全</b> <b>非常に安全</b>
本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できる)と、相互署名された端末の検証を行います。 本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、クロス署名による端末の認証が可能です。
<b>包括的なコミュニケーションと統合</b> <b>包括的なコミュニケーションと統合</b>
メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くのインテグレーション、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう
<b>中断からの再開</b> <b>いつでも、どこにいても</b>
メッセージの履歴は全ての端末とウェブhttps://app.element.ioで完全に同期されるので、どこからでも連絡を取り合うことができます。 メッセージの履歴は全ての端末とウェブhttps://app.element.ioで完全に同期されるので、どこからでも連絡を取り合うことができます。
<b>オープンソース</b> <b>オープンソース</b>
Element AndroidはGitHubで開発されているオープンソースのプロジェクトです。 バグの報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。 Element AndroidはGitHubで開発されているオープンソースのプロジェクトです。 不具合の報告や開発への貢献は https://github.com/vector-im/element-android にて受け付けています。

View File

@ -0,0 +1,2 @@
Ndryshime kryesore në këtë version: Sendërtimi fillestar i mesazheve në rrjedha. Flluska mesazhesh.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: shtim mbulimi për @room dhe për pyetësorë jopublikë, mes mjaft ndryshimesh të tjera të vockla.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases/tag/v1.4.2

View File

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: Initial implementation av trådmeddelanden. Meddelandebubblor.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.0

View File

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: lägg till stöd för @room och slutna omröstningar, och många andra små ändringar.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.4.2

View File

@ -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

View File

@ -17,7 +17,12 @@
package im.vector.lib.multipicker.utils package im.vector.lib.multipicker.utils
import android.database.Cursor import android.database.Cursor
import androidx.core.database.getStringOrNull
fun Cursor.getColumnIndexOrNull(column: String): Int? { fun Cursor.getColumnIndexOrNull(column: String): Int? {
return getColumnIndex(column).takeIf { it != -1 } return getColumnIndex(column).takeIf { it != -1 }
} }
fun Cursor.readStringColumnOrNull(column: String): String? {
return getColumnIndexOrNull(column)?.let { getStringOrNull(it) }
}

View File

@ -56,6 +56,12 @@
<!-- Used for item separators in list, on surface --> <!-- Used for item separators in list, on surface -->
<attr name="vctr_list_separator_on_surface" format="color" /> <attr name="vctr_list_separator_on_surface" format="color" />
<!-- Background color used for:
- unread badge background for a room item in the room list
- start unread indicator for a room item in the room list
- Background for unread badge background in the bottom navigation -->
<attr name="vctr_unread_background" format="color" />
<!-- Other colors, which are not in the palette --> <!-- Other colors, which are not in the palette -->
<attr name="vctr_fab_label_bg" format="color" /> <attr name="vctr_fab_label_bg" format="color" />
<color name="vctr_fab_label_bg_light">@android:color/white</color> <color name="vctr_fab_label_bg_light">@android:color/white</color>
@ -133,4 +139,8 @@
<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>
</resources> </resources>

View File

@ -67,4 +67,10 @@
<item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.01</item> <item name="ftue_auth_carousel_item_spacing" format="float" type="dimen">0.01</item>
<item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.35</item> <item name="ftue_auth_carousel_item_image_height" format="float" type="dimen">0.35</item>
</resources>
<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>
<!-- Location sharing -->
<dimen name="location_sharing_option_default_padding">10dp</dimen>
</resources>

View File

@ -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>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.Vector.ActionButton" parent="Widget.AppCompat.ActionButton">
<item name="android:paddingStart">5dp</item>
<item name="android:paddingEnd">5dp</item>
<item name="android:minWidth">0dp</item>
</style>
</resources>

View File

@ -46,11 +46,12 @@
<!-- Presence Indicator colors --> <!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item> <item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_dark</item>
<!-- Some alias --> <!-- Some aliases -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item> <item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item> <item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item>
<!-- Material color --> <!-- Material color -->
<item name="colorPrimary">@color/element_accent_dark</item> <item name="colorPrimary">@color/element_accent_dark</item>
@ -141,6 +142,11 @@
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item> <item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<item name="vctr_toast_background">@color/vctr_toast_background_dark</item> <item name="vctr_toast_background">@color/vctr_toast_background_dark</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" />

View File

@ -46,11 +46,12 @@
<!-- Presence Indicator colors --> <!-- Presence Indicator colors -->
<item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item> <item name="vctr_presence_indicator_offline">@color/vctr_presence_indicator_offline_light</item>
<!-- Some alias --> <!-- Some aliases -->
<item name="vctr_header_background">?vctr_system</item> <item name="vctr_header_background">?vctr_system</item>
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item> <item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item> <item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item>
<!-- Material color --> <!-- Material color -->
<item name="colorPrimary">@color/element_accent_light</item> <item name="colorPrimary">@color/element_accent_light</item>
@ -142,6 +143,11 @@
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item> <item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<item name="vctr_toast_background">@color/vctr_toast_background_light</item> <item name="vctr_toast_background">@color/vctr_toast_background_light</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" />

View File

@ -31,7 +31,7 @@ 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()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\""

View File

@ -71,7 +71,7 @@ class CommonTestHelper(context: Context) {
) )
) )
} }
matrix = TestMatrix.getInstance(context) matrix = TestMatrix.getInstance()
} }
fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {

View File

@ -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

View File

@ -105,16 +105,9 @@ internal class TestMatrix constructor(context: Context, matrixConfiguration: Mat
} }
} }
fun getInstance(context: Context): TestMatrix { fun getInstance(): TestMatrix {
if (isInit.compareAndSet(false, true)) { if (isInit.compareAndSet(false, false)) {
val appContext = context.applicationContext throw IllegalStateException("Matrix is not initialized properly. You should call TestMatrix.initialize first")
if (appContext is MatrixConfiguration.Provider) {
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
instance = TestMatrix(appContext, matrixConfiguration)
} else {
throw IllegalStateException("Matrix is not initialized properly." +
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
}
} }
return instance return instance
} }

View File

@ -0,0 +1,648 @@
/*
* 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.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.equals(timeLineEvent.root.getClearContent().toModel<MessageContent>()?.body)
}
}
}
otherAccounts.forEach {
testHelper.signOutAndClose(it)
}
newAccount.forEach { testHelper.signOutAndClose(it) }
cryptoTestData.cleanUp(testHelper)
}
/**
* Quick test for basic keybackup
* 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 uplaod 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 keybackup for bob ...")
val keysBackupService = bobSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = testHelper.doSync<KeysVersion> {
keysBackupService.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 ->
keysBackupService.backupAllGroupSessions(
null,
TestMatrixCallback(latch, true)
)
}
Log.v("#E2E TEST", "... Keybackup 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 forwared (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 * 60_000) {
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodicallyWithLatch(it) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also {
Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}")
}
.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 {
testHelper.retryPeriodicallyWithLatch(it) {
otherAccounts.map {
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
it == Membership.JOIN
}
}
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
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")
}
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
} }

View File

@ -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

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
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
@ -39,6 +40,7 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@Ignore("This test is flaky ; see issue #5449")
class VerificationTest : InstrumentedTest { class VerificationTest : InstrumentedTest {
data class ExpectedResult( data class ExpectedResult(

View File

@ -22,6 +22,7 @@ import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
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.JUnit4 import org.junit.runners.JUnit4
@ -38,6 +39,7 @@ import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@Ignore("Remaining Integration tests are unstable if run with this test. Issue #5439")
class ThreadMessagingTest : InstrumentedTest { class ThreadMessagingTest : InstrumentedTest {
@Test @Test

View File

@ -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>)

View File

@ -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
} }

View File

@ -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}") }
} }
} }
} }
} }

View File

@ -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)
} }

View File

@ -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()
}
} }

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