mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
152 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
552b143f8c | ||
|
f46a9d6cc8 | ||
|
b27dc02cfd | ||
|
6ba03f82e4 | ||
|
a6fafb07da | ||
|
0c1f190035 | ||
|
8d85d047b7 | ||
|
84158ece37 | ||
|
63ef40f58b | ||
|
fd5530a2f9 | ||
|
ee2fd9f123 | ||
|
7c58af735b | ||
|
1a941149ab | ||
|
98e09eedc3 | ||
|
3d7489c7c5 | ||
|
edf23bbb89 | ||
|
3179dc1400 | ||
|
d3391076b5 | ||
|
36ce42e36e | ||
|
f37d918ce6 | ||
|
b61b2b6f16 | ||
|
979324da84 | ||
|
d045cedb46 | ||
|
58a44ac668 | ||
|
e4c1913e01 | ||
|
4e53d8462f | ||
|
7073b1647c | ||
|
dd6410794c | ||
|
2c75f41072 | ||
|
454ba7bf7c | ||
|
b14338d2c4 | ||
|
83084f6481 | ||
|
4341cf8c9c | ||
|
8d8a5d3de2 | ||
|
26e2f4e967 | ||
|
531d9f2802 | ||
|
6112082d07 | ||
|
95070d3664 | ||
|
8bfd5f7c54 | ||
|
17e9bd200b | ||
|
fd07835e45 | ||
|
57d224e8ba | ||
|
d26d28f770 | ||
|
3aa5f34ee7 | ||
|
8d95eb7b16 | ||
|
64a7de5326 | ||
|
7884b9dd5e | ||
|
d4c6a46e90 | ||
|
d4c141664b | ||
|
77a576784f | ||
|
491f52a3a3 | ||
|
5f68f98d3a | ||
|
cd101f871c | ||
|
e8922a5fa7 | ||
|
e27916f85e | ||
|
973246819a | ||
|
25ecd599f3 | ||
|
747c81c687 | ||
|
f13a15495b | ||
|
2a5e233e2c | ||
|
842aeb70e0 | ||
|
60940c01df | ||
|
ebc81e24af | ||
|
52082a9def | ||
|
9e74afc9b1 | ||
|
ecd1057ce9 | ||
|
4fa634a283 | ||
|
7001f21330 | ||
|
d379cef0ba | ||
|
59ddf1a107 | ||
|
a015eda72c | ||
|
87df8ab6f6 | ||
|
1bd2da5c99 | ||
|
a6b127cb20 | ||
|
df82eee736 | ||
|
dfbb3122e7 | ||
|
1e00da6e2f | ||
|
2709cb2973 | ||
|
0d70f6eb54 | ||
|
42eec4b557 | ||
|
6ee438d7d5 | ||
|
3b9daec869 | ||
|
1b3be240b3 | ||
|
8c1cc44255 | ||
|
3f2f3860e1 | ||
|
470557c59e | ||
|
ff548d2f98 | ||
|
d31c741f9d | ||
|
ec9a066900 | ||
|
52a06931f4 | ||
|
a889d8d678 | ||
|
1f41c54a82 | ||
|
fe51ee3956 | ||
|
d65459cc59 | ||
|
dc8230e435 | ||
|
0838a10b65 | ||
|
a3be0286ee | ||
|
751bd27c9d | ||
|
0a6dbeb3fe | ||
|
bc23f82ade | ||
|
ca109f70a4 | ||
|
b5c224f3e0 | ||
|
e2c7833f93 | ||
|
6d5f59c67e | ||
|
e6bd57d88c | ||
|
81f7517560 | ||
|
1ceacdd194 | ||
|
52aa4bb0d8 | ||
|
a5d231c259 | ||
|
e6a18a2241 | ||
|
003a134f68 | ||
|
494e824a85 | ||
|
df97229b9c | ||
|
6e6478a949 | ||
|
de688aa93b | ||
|
1eee5c1de7 | ||
|
ce5d42d484 | ||
|
6379420401 | ||
|
9821487a8e | ||
|
2b29a57b9b | ||
|
87e5900dcd | ||
|
dc19380fbf | ||
|
880ed69f97 | ||
|
8941e6396c | ||
|
425441546e | ||
|
12395e9b04 | ||
|
8f6edba403 | ||
|
39a783196e | ||
|
92399aba07 | ||
|
83e2419c30 | ||
|
3216fa6146 | ||
|
eeb67e1934 | ||
|
23e7bdbae3 | ||
|
ad7934847c | ||
|
45be2749f6 | ||
|
754ea6a98d | ||
|
30906885ec | ||
|
5580f307be | ||
|
8885d14ee5 | ||
|
380a0b8de3 | ||
|
1bbd4b7e44 | ||
|
27bae30eac | ||
|
40fd9f2f7b | ||
|
10cde1f0a6 | ||
|
fd46487270 | ||
|
cd7bf12e16 | ||
|
95b63ccefb | ||
|
975ef3c06f | ||
|
e567b9c9cf | ||
|
9aeb3b7074 | ||
|
313d4f82f7 | ||
|
28da02c583 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
|
||||
jobs:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble ${{ matrix.target }} debug apk
|
||||
run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
run: ./gradlew assemble${{ matrix.target }}Debug $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload ${{ matrix.target }} debug APKs
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Assemble GPlay unsigned apk
|
||||
run: ./gradlew clean assembleGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
run: ./gradlew clean assembleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload Gplay unsigned APKs
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: Execute exodus-standalone
|
||||
uses: docker://exodusprivacy/exodus-standalone:latest
|
||||
with:
|
||||
args: /github/workspace/gplayRustCrypto/release/vector-gplay-rustCrypto-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
|
||||
args: /github/workspace/gplay/release/vector-gplay-universal-release-unsigned.apk -j -o /github/workspace/exodus.json
|
||||
- name: Upload exodus json report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
37
.github/workflows/elementr.yml
vendored
37
.github/workflows/elementr.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: ER APK Build
|
||||
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
|
||||
jobs:
|
||||
debug:
|
||||
name: Build debug APKs ER
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/main'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [ Gplay, Fdroid ]
|
||||
# Allow all jobs on develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/develop' && format('elementr-{0}-{1}', matrix.target, github.sha) || format('build-er-debug-{0}-{1}', matrix.target, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Assemble ${{ matrix.target }} debug apk
|
||||
run: ./gradlew assemble${{ matrix.target }}RustCryptoDebug $CI_GRADLE_ARG_PROPERTIES
|
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
|
||||
jobs:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
yes n | towncrier build --version nightly
|
||||
- name: Build and upload Gplay Nightly APK
|
||||
run: |
|
||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew assembleGplayNightly appDistributionUploadGplayNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
env:
|
||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||
|
46
.github/workflows/nightly_er.yml
vendored
46
.github/workflows/nightly_er.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Build and release Element R nightly APK
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every nights at 4
|
||||
- cron: "0 4 * * *"
|
||||
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
name: Build and publish ER nightly Gplay APK to Firebase
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Install towncrier
|
||||
run: |
|
||||
python3 -m pip install towncrier
|
||||
- name: Prepare changelog file
|
||||
run: |
|
||||
mv towncrier.toml towncrier.toml.bak
|
||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
||||
rm towncrier.toml.bak
|
||||
yes n | towncrier build --version nightly
|
||||
- name: Build and upload Gplay Nightly ER APK
|
||||
run: |
|
||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadGplayRustCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
env:
|
||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_R_NIGHTLY_FIREBASE_TOKEN }}
|
6
.github/workflows/quality.yml
vendored
6
.github/workflows/quality.yml
vendored
@@ -49,10 +49,8 @@ jobs:
|
||||
- name: Run lint
|
||||
# Not always, if ktlint or detekt fail, avoid running the long lint check.
|
||||
run: |
|
||||
./gradlew vector-app:lintGplayKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidKotlinCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintGplayRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidRustCryptoRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew vector-app:lintFdroidRelease $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
|
@@ -1,5 +1,6 @@
|
||||
name: Sync Data From External Sources
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on every Monday UTC
|
||||
- cron: '0 0 * * 1'
|
||||
@@ -80,4 +81,4 @@ jobs:
|
||||
|
||||
*Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events)
|
||||
branch: sync-analytics-plan
|
||||
base: develop
|
||||
base: develop
|
||||
|
102
.github/workflows/tests-rust.yml
vendored
102
.github/workflows/tests-rust.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 4 --no-daemon
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Runs all tests with rust crypto
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 90 # We might need to increase it if the time for tests grows
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [28]
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-rust-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-rust-{0}', github.sha) || format('unit-tests-rust-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
gradle-home-cache-cleanup: ${{ github.ref == 'refs/heads/develop' }}
|
||||
|
||||
# - name: Run screenshot tests
|
||||
# run: ./gradlew verifyScreenshots $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
# - name: Archive Screenshot Results on Error
|
||||
# if: failure()
|
||||
# uses: actions/upload-artifact@v3
|
||||
# with:
|
||||
# name: screenshot-results
|
||||
# path: |
|
||||
# **/out/failures/
|
||||
# **/build/reports/tests/*UnitTest/
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: michaelkaye/setup-matrix-synapse@v1.0.4
|
||||
with:
|
||||
uploadLogs: true
|
||||
httpPort: 8080
|
||||
disableRateLimiting: true
|
||||
public_baseurl: "http://10.0.2.2:8080/"
|
||||
|
||||
- name: Run all the codecoverage tests at once
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
# continue-on-error: true
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
arch: x86
|
||||
profile: Nexus 5X
|
||||
target: playstore
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
# emulator-build: 7425822
|
||||
script: |
|
||||
./gradlew gatherGplayRustCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew instrumentationTestsRustWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: Upload Rust Integration Test Report Log
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: integration-test-rust-error-results
|
||||
path: |
|
||||
*/build/outputs/androidTest-results/connected/
|
||||
*/build/reports/androidTests/connected/
|
||||
|
||||
# For now ignore sonar
|
||||
# - name: Publish results to Sonar
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.SONARQUBE_GITHUB_API_TOKEN }} # Needed to get PR information, if any
|
||||
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
# ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }}
|
||||
# if: ${{ always() && env.GITHUB_TOKEN != '' && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }}
|
||||
# run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
- name: Format unit test results
|
||||
if: always()
|
||||
run: python3 ./tools/ci/render_test_output.py unit ./**/build/test-results/**/*.xml
|
||||
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
disable-animations: true
|
||||
# emulator-build: 7425822
|
||||
script: |
|
||||
./gradlew gatherGplayKotlinCryptoDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew gatherGplayDebugStringTemplates $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew generateCoverageReport $CI_GRADLE_ARG_PROPERTIES
|
||||
|
36
CHANGES.md
36
CHANGES.md
@@ -1,3 +1,39 @@
|
||||
Changes in Element v1.6.8 (2023-11-28)
|
||||
======================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Stop incoming call ringing if the call is cancelled or answered on another session. ([#4066](https://github.com/vector-im/element-android/issues/4066))
|
||||
- Ensure the incoming call will not ring forever, in case the call is not ended by another way. ([#8178](https://github.com/vector-im/element-android/issues/8178))
|
||||
- Unified Push: Ignore the potential SSL error when the custom gateway is testing locally ([#8683](https://github.com/vector-im/element-android/issues/8683))
|
||||
- Fix issue with timeline message view reuse while rich text editor is enabled ([#8688](https://github.com/vector-im/element-android/issues/8688))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Remove unused WebRTC dependency ([#8658](https://github.com/vector-im/element-android/issues/8658))
|
||||
- Take into account boolean "io.element.disable_network_constraint" from the .well-known file. ([#8662](https://github.com/vector-im/element-android/issues/8662))
|
||||
- Update regex for email address to be aligned on RFC 5322 ([#8671](https://github.com/vector-im/element-android/issues/8671))
|
||||
- Bump crypto sdk bindings to v0.3.16 ([#8679](https://github.com/vector-im/element-android/issues/8679))
|
||||
|
||||
|
||||
Changes in Element v1.6.6 (2023-10-05)
|
||||
======================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Fixed JWT token for Jitsi openidtoken-jwt authentication ([#7758](https://github.com/vector-im/element-android/issues/7758))
|
||||
- Fix crash when max shortcuts count is exceeded ([#8644](https://github.com/vector-im/element-android/issues/8644))
|
||||
- Fix Login with QR code not working with rust crypto. ([#8653](https://github.com/vector-im/element-android/issues/8653))
|
||||
|
||||
Other changes
|
||||
-------------
|
||||
- Use 3PID capability to show / hide email UI in settings ([#8615](https://github.com/vector-im/element-android/issues/8615))
|
||||
- If an external account manager is configured on the server, use it to delete other sessions and hide the multi session deletion. ([#8616](https://github.com/vector-im/element-android/issues/8616))
|
||||
- Hide account deactivation UI for account managed externally. ([#8619](https://github.com/vector-im/element-android/issues/8619))
|
||||
- Fix import of SAS Emoji string translations. ([#8623](https://github.com/vector-im/element-android/issues/8623))
|
||||
- Open external account manager for delete other sessions using Chrome custom tabs. ([#8645](https://github.com/vector-im/element-android/issues/8645))
|
||||
|
||||
|
||||
Changes in Element v1.6.5 (2023-07-25)
|
||||
======================================
|
||||
|
||||
|
@@ -312,7 +312,7 @@ tasks.register("recordScreenshots", GradleBuild) {
|
||||
|
||||
tasks.register("verifyScreenshots", GradleBuild) {
|
||||
startParameter.projectProperties.screenshot = ""
|
||||
tasks = [':vector:verifyPaparazziRustCryptoDebug']
|
||||
tasks = [':vector:verifyPaparazziDebug']
|
||||
}
|
||||
|
||||
ext.initScreenshotTests = { project ->
|
||||
@@ -331,6 +331,10 @@ ext.initScreenshotTests = { project ->
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
maxHeapSize = "2g"
|
||||
}
|
||||
|
||||
// Workaround to have KSP generated Kotlin code available in the IDE (for code completion)
|
||||
// Ref: https://github.com/airbnb/epoxy/releases/tag/5.0.0beta02
|
||||
subprojects { project ->
|
||||
|
@@ -87,11 +87,5 @@ task unitTestsWithCoverage(type: GradleBuild) {
|
||||
task instrumentationTestsWithCoverage(type: GradleBuild) {
|
||||
startParameter.projectProperties.coverage = "true"
|
||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
||||
tasks = [':vector-app:connectedGplayKotlinCryptoDebugAndroidTest', ':vector:connectedKotlinCryptoDebugAndroidTest', 'matrix-sdk-android:connectedKotlinCryptoDebugAndroidTest']
|
||||
}
|
||||
|
||||
task instrumentationTestsRustWithCoverage(type: GradleBuild) {
|
||||
startParameter.projectProperties.coverage = "true"
|
||||
startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui'
|
||||
tasks = [':vector-app:connectedGplayRustCryptoDebugAndroidTest', ':vector:connectedRustCryptoDebugAndroidTest', 'matrix-sdk-android:connectedRustCryptoDebugAndroidTest']
|
||||
tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest']
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ def lifecycle = "2.5.1"
|
||||
def flowBinding = "1.2.0"
|
||||
def flipper = "0.190.0"
|
||||
def epoxy = "5.0.0"
|
||||
def mavericks = "3.0.2"
|
||||
def mavericks = "3.0.7"
|
||||
def glide = "4.15.1"
|
||||
def bigImageViewer = "1.8.1"
|
||||
def jjwt = "0.11.5"
|
||||
@@ -101,7 +101,7 @@ ext.libs = [
|
||||
],
|
||||
element : [
|
||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||
'wysiwyg' : "io.element.android:wysiwyg:2.2.2"
|
||||
'wysiwyg' : "io.element.android:wysiwyg:2.14.1"
|
||||
],
|
||||
squareup : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
|
@@ -48,7 +48,7 @@ mv towncrier.toml towncrier.toml.bak
|
||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
||||
rm towncrier.toml.bak
|
||||
yes n | towncrier build --version nightly
|
||||
./gradlew assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
||||
```
|
||||
|
||||
Then you can reset the change on the codebase.
|
||||
|
2
fastlane/metadata/android/cs-CZ/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Hlavní změny v této verzi: Element Android nyní používá Crypto Rust SDK.
|
||||
Úplný seznam změn: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/cs-CZ/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/cs-CZ/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Hlavní změny v této verzi: opravné vydání.
|
||||
Úplný seznam změn: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/de-DE/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Die wichtigsten Änderungen in dieser Version: Element Android nutzt nun das Crypto-Rust-SDK.
|
||||
Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/de-DE/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Die wichtigsten Änderungen in dieser Version: Fehlerbehebungen.
|
||||
Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/en-US/changelogs/40106060.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40106060.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: mainly bug fixes.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/en-US/changelogs/40106080.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40106080.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Main changes in this version: Bugfixes.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/fa/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
تغییرات عمده در این نگارش: المنت اندروید اکنون از SDK راست Crypto استفاده میکند.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/fa/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/fa/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
تغغیرات عمده در این نگارش: ارائه تصحیحی.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/id/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Perubahan utama dalam versi ini: Element Android sekarang menggunakan SDK Kripto Rust.
|
||||
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/id/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/id/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Perubahan utama dalam versi ini: rilis perbaikan.
|
||||
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105160.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105160.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Threads são agora habilitadas por padrão.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105180.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105180.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Threads são agora habilitadas por padrão.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105200.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105200.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs!
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105220.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105220.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente melhorias no recurso de transmissão de voz.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105240.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105240.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs, em especial a correção da mensagem não aparecer na linha do tempo.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105250.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105250.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs, em especial a correção da mensagem não aparecer na linha do tempo.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105260.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105260.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105280.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105280.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105300.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105300.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: permalinks para salas, espaços, usuários e mensagens são agora exibidos como pílulas na linha do tempo. Também corrigimos alguns problemas com figurinhas personalizadas e o marcador de lido ficando travado no passado.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40105320.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40105320.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Basicamente correção de bugs.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40106000.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40106000.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Element Android está agora usando o Crypto Rust SDK.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40106010.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40106010.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Element Android está agora usando o Crypto Rust SDK.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/pt-BR/changelogs/40106020.txt
Normal file
2
fastlane/metadata/android/pt-BR/changelogs/40106020.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Principais mudanças nesta versão: Element Android está agora usando o Crypto Rust SDK.
|
||||
Changelog completo: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/sk/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/sk/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Hlavné zmeny v tejto verzii: Element Android teraz používa Crypto Rust SDK.
|
||||
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/sk/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/sk/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Hlavné zmeny v tejto verzii: opravné vydanie.
|
||||
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/sq/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Ndryshime në këtë version: Element Android tanimë përdor Crypto Rust SDK.
|
||||
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/sq/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/sq/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Ndryshimet kryesore në këtë version: hedhje në qarkullim me ndreqje të ndryshme.
|
||||
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/uk/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Основні зміни в цій версії: Element для Android відтепер використовує Crypto Rust SDK.
|
||||
Список усіх змін: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/uk/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/uk/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Основні зміни в цій версії: коригувальний випуск.
|
||||
Список усіх змін: https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/zh-TW/changelogs/40106030.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40106030.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
此版本的主要變更:現在起,Element Android 使用 Crypto Rust SDK。
|
||||
完整的變更紀錄:https://github.com/vector-im/element-android/releases
|
2
fastlane/metadata/android/zh-TW/changelogs/40106050.txt
Normal file
2
fastlane/metadata/android/zh-TW/changelogs/40106050.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
此版本中的主要變動:版本修正。
|
||||
完整的變更紀錄:https://github.com/vector-im/element-android/releases
|
@@ -1,20 +0,0 @@
|
||||
android {
|
||||
|
||||
flavorDimensions "crypto"
|
||||
|
||||
productFlavors {
|
||||
kotlinCrypto {
|
||||
dimension "crypto"
|
||||
// versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
|
||||
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"JC\""
|
||||
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"KotlinCrypto\""
|
||||
}
|
||||
rustCrypto {
|
||||
dimension "crypto"
|
||||
isDefault = true
|
||||
// // versionName "${versionMajor}.${versionMinor}.${versionPatch}${getFdroidVersionSuffix()}"
|
||||
// buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"RC\""
|
||||
// buildConfigField "String", "FLAVOR_DESCRIPTION", "\"RustCrypto\""
|
||||
}
|
||||
}
|
||||
}
|
@@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret
|
||||
|
||||
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
||||
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
||||
android.experimental.lint.version=8.0.0-alpha10
|
||||
android.experimental.lint.version=8.3.0-alpha12
|
||||
|
@@ -256,4 +256,39 @@
|
||||
</plurals>
|
||||
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• IP literallarına uyğunlaşan serverlər qadağan edildi.</string>
|
||||
<string name="notice_room_server_acl_set_ip_literals_allowed">• IP literallarına uyğunlaşan serverlərə icazə verilir.</string>
|
||||
<string name="pill_message_in_unknown_room">Otaqdakı mesaj</string>
|
||||
<string name="set_link_link">Bağlantı</string>
|
||||
<string name="set_link_create">Bağlantı yarat</string>
|
||||
<string name="message_reply_to_poll_preview">Anket</string>
|
||||
<string name="pill_message_unknown_room_or_space">Otaq / Məkan</string>
|
||||
<string name="set_link_edit">Bağlantını redaktə et</string>
|
||||
<string name="set_link_text">Mətn</string>
|
||||
<string name="message_reply_to_sender_ended_poll">Anket başa çatıb.</string>
|
||||
<string name="notice_display_name_changed_to">%1$s ekran adını %2$s olaraq dəyişdi</string>
|
||||
<string name="rich_text_editor_inline_code">Daxili kod formatın işlət</string>
|
||||
<string name="rich_text_editor_code_block">Kod blokun dəyiş</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Bu otaq üçün əsas ünvanı %1$s olaraq təyin etdiniz.</string>
|
||||
<string name="pill_message_in_room">Mesaj %s</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Bu otaq üçün ünvan kimi %2$s-nı sildiniz və %1$s əlavə etdiniz.</string>
|
||||
<string name="settings_access_token">Giriş Nişanəniz</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s bu otaq üçün əsas ünvanı sildi.</string>
|
||||
<string name="message_reply_to_ended_poll_preview">Anket bitdi</string>
|
||||
<string name="notice_room_canonical_alias_set">%1$s bu otaq üçün əsas ünvanı %2$s olaraq təyin etdi.</string>
|
||||
<string name="pill_message_from_unknown_user">Mesaj</string>
|
||||
<string name="pill_message_from_user">%s-dan² mesaj</string>
|
||||
<string name="settings_access_token_summary">Giriş nişanəniz hesabınıza tam giriş imkanı verir.Bunu heç kimlə paylaşmayın.</string>
|
||||
<plurals name="notice_room_canonical_alias_alternative_added_by_you">
|
||||
<item quantity="one">Bu otaq üçün alternativ %1$s ünvanın əlavə etdiniz.</item>
|
||||
<item quantity="other">Bu otaq üçün alternativ %1$s ünvanın əlavə etdiniz.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_canonical_alias_alternative_changed">%1$s bu otaq üçün alternativ ünvanları dəyişdi.</string>
|
||||
<plurals name="notice_room_canonical_alias_alternative_added">
|
||||
<item quantity="one">%1$s bu otaq üçün %2$s alternativ ünvanın əlavə etdi.</item>
|
||||
<item quantity="other">%1$s bu otaq üçün %2$s alternativ ünvanın əlavə etdi.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_canonical_alias_alternative_changed_by_you">Bu otaq üçün alternativ ünvanları dəyişdiniz.</string>
|
||||
<plurals name="notice_room_canonical_alias_alternative_removed">
|
||||
<item quantity="one">%1$s bu otaq üçün %2$s alternativ ünvanın sildi.</item>
|
||||
<item quantity="other">%1$s bu otaq üçün %2$s alternativ ünvanın sildi.</item>
|
||||
</plurals>
|
||||
</resources>
|
@@ -224,7 +224,7 @@
|
||||
<string name="notice_room_server_acl_set_title_by_you">Du hast die Server-ACL für diesen Raum gesetzt.</string>
|
||||
<string name="notice_room_server_acl_set_title">%s hat die Server-Zugriffssteuerungsliste (ACL) für diesen Raum gesetzt.</string>
|
||||
<string name="title_activity_settings">Einstellungen</string>
|
||||
<string name="call_notification_answer">Akzeptiere</string>
|
||||
<string name="call_notification_answer">Akzeptieren</string>
|
||||
<string name="call_notification_reject">Ablehnen</string>
|
||||
<string name="call_notification_hangup">Anruf beenden</string>
|
||||
<string name="ok">Ok</string>
|
||||
@@ -263,7 +263,7 @@
|
||||
<string name="matrix_only_filter">Nur Matrix-Kontakte</string>
|
||||
<string name="no_result_placeholder">Keine Ergebnisse</string>
|
||||
<string name="rooms_header">Räume</string>
|
||||
<string name="send_bug_report_include_logs">Sende Protokolle</string>
|
||||
<string name="send_bug_report_include_logs">Protokolle senden</string>
|
||||
<string name="send_bug_report_include_crash_logs">Absturzberichte übermitteln</string>
|
||||
<string name="send_bug_report_include_screenshot">Bildschirmfoto übermitteln</string>
|
||||
<string name="send_bug_report">Problem melden</string>
|
||||
@@ -692,7 +692,7 @@
|
||||
<string name="settings_noisy_notifications_preferences">Laute Benachrichtigungen einstellen</string>
|
||||
<string name="settings_call_notifications_preferences">Anrufbenachrichtigung einstellen</string>
|
||||
<string name="settings_silent_notifications_preferences">Stumme Benachrichtigungen einstellen</string>
|
||||
<string name="settings_system_preferences_summary">Wähle LED-Farbe, Vibration, Ton …</string>
|
||||
<string name="settings_system_preferences_summary">LED-Farbe, Vibration, Ton auswählen …</string>
|
||||
<string name="notification_silent">Stumm</string>
|
||||
<string name="passphrase_empty_error_message">Bitte eine Passphrase eingeben</string>
|
||||
<string name="passphrase_passphrase_too_weak">Passphrase ist zu schwach</string>
|
||||
@@ -712,7 +712,7 @@
|
||||
<string name="keys_backup_unlock_button">Historie entschlüsseln</string>
|
||||
<string name="keys_backup_settings_restore_backup_button">Von Sicherung wiederherstellen</string>
|
||||
<string name="keys_backup_settings_delete_backup_button">Lösche Sicherung</string>
|
||||
<string name="keys_backup_settings_deleting_backup">Lösche Sicherung …</string>
|
||||
<string name="keys_backup_settings_deleting_backup">Sicherung löschen …</string>
|
||||
<string name="keys_backup_settings_delete_confirm_title">Lösche Sicherung</string>
|
||||
<string name="settings_notification_by_event">Präferenz der Benachrichtigungen nach Ereignis</string>
|
||||
<string name="settings_troubleshoot_test_fcm_failed_too_many_registration">[%1$s]
|
||||
@@ -728,7 +728,7 @@
|
||||
\nSichere deine Schlüssel, um sie nicht zu verlieren.</string>
|
||||
<string name="keys_backup_setup_step3_generating_key_status">Wiederherstellungsschlüssel aus Passphrase generieren. Dies kann mehrere Sekunden brauchen.</string>
|
||||
<string name="keys_backup_setup_skip_msg">Du verlierst möglicherweise den Zugang zu deinen Nachrichten, wenn du dich abmeldest oder das Gerät verlierst.</string>
|
||||
<string name="keys_backup_restore_is_getting_backup_version">Rufe Sicherungsversion ab …</string>
|
||||
<string name="keys_backup_restore_is_getting_backup_version">Sicherungsversion abrufen …</string>
|
||||
<string name="keys_backup_restore_with_passphrase">Nutze deine Wiederherstellungs-Passphrase, um deinen verschlüsselten Nachrichtenverlauf lesen zu können</string>
|
||||
<string name="keys_backup_restore_use_recovery_key">nutze deinen Wiederherstellungsschlüssel</string>
|
||||
<string name="keys_backup_restore_with_passphrase_helper_with_link">Wenn du deine Wiederherstellungspassphrase nicht weist, kannst du %s.</string>
|
||||
@@ -793,15 +793,15 @@
|
||||
<string name="keys_backup_banner_in_progress">Sichere deine Schlüssel. Dies könnte einige Minuten dauern …</string>
|
||||
<string name="keys_backup_info_keys_all_backup_up">Alle Schlüssel sind gesichert</string>
|
||||
<plurals name="keys_backup_info_keys_backing_up">
|
||||
<item quantity="one">Sichere einen Schlüssel …</item>
|
||||
<item quantity="other">Sichere %d Schlüssel …</item>
|
||||
<item quantity="one">Einen Schlüssel sichern …</item>
|
||||
<item quantity="other">%d Schlüssel sichern …</item>
|
||||
</plurals>
|
||||
<string name="keys_backup_info_title_version">Version</string>
|
||||
<string name="keys_backup_info_title_algorithm">Algorithmus</string>
|
||||
<string name="keys_backup_info_title_signature">Signatur</string>
|
||||
<string name="keys_backup_restoring_computing_key_waiting_message">Berechne Wiederherstellungsschlüssel …</string>
|
||||
<string name="keys_backup_restoring_downloading_backup_waiting_message">Lade Schlüssel herunter …</string>
|
||||
<string name="keys_backup_restoring_importing_keys_waiting_message">Importiere Schlüssel …</string>
|
||||
<string name="keys_backup_restoring_computing_key_waiting_message">Wiederherstellungsschlüssel berechnen …</string>
|
||||
<string name="keys_backup_restoring_downloading_backup_waiting_message">Schlüssel herunterladen …</string>
|
||||
<string name="keys_backup_restoring_importing_keys_waiting_message">Schlüssel importieren …</string>
|
||||
<string name="action_ignore">Ignorieren</string>
|
||||
<string name="auth_login_sso">Mit Single-Sign-On anmelden</string>
|
||||
<string name="settings_send_message_with_enter">Nachricht mit Eingabetaste senden</string>
|
||||
@@ -903,8 +903,8 @@
|
||||
<string name="settings_labs_show_hidden_events_in_timeline">Versteckte Ereignisse in der Zeitleiste anzeigen</string>
|
||||
<string name="bottom_action_people_x">Direktnachrichten</string>
|
||||
<string name="send_file_step_idle">Warten …</string>
|
||||
<string name="send_file_step_encrypting_thumbnail">Vorschaubild wird verschlüsselt …</string>
|
||||
<string name="send_file_step_encrypting_file">Verschlüssle Datei …</string>
|
||||
<string name="send_file_step_encrypting_thumbnail">Vorschaubild verschlüsseln …</string>
|
||||
<string name="send_file_step_encrypting_file">Datei verschlüsseln …</string>
|
||||
<string name="edited_suffix">(bearbeitet)</string>
|
||||
<string name="message_edits">Nachrichtenbearbeitung</string>
|
||||
<string name="no_message_edits_found">Keine Änderungen gefunden</string>
|
||||
@@ -912,7 +912,7 @@
|
||||
<string name="room_filtering_footer_create_new_direct_message">Sende eine neue Direktnachricht</string>
|
||||
<string name="room_filtering_footer_open_room_directory">Das Raumverzeichnis anzeigen</string>
|
||||
<string name="link_copied_to_clipboard">Link in die Zwischenablage kopiert</string>
|
||||
<string name="creating_direct_room">Erstelle Raum …</string>
|
||||
<string name="creating_direct_room">Raum erstellen …</string>
|
||||
<string name="message_view_edit_history">Bearbeitungsverlauf anzeigen</string>
|
||||
<string name="import_e2e_keys_from_file">E2E-Schlüssel aus der Datei \"%1$s\" importieren.</string>
|
||||
<string name="send_suggestion_sent">Vielen Dank, der Vorschlag wurde erfolgreich gesendet</string>
|
||||
@@ -1003,7 +1003,7 @@
|
||||
<string name="send_attachment">Anhang senden</string>
|
||||
<string name="a11y_open_drawer">Navigationsmenü öffnen</string>
|
||||
<string name="a11y_create_menu_open">Raumerstellungsmenü öffnen</string>
|
||||
<string name="a11y_create_menu_close">Schließe das Raumerstellungsmenü …</string>
|
||||
<string name="a11y_create_menu_close">Das Raumerstellungsmenü schließen …</string>
|
||||
<string name="a11y_create_direct_message">Erstelle eine neue Direktnachricht</string>
|
||||
<string name="a11y_create_room">Erstelle einen neuen Raum</string>
|
||||
<string name="a11y_close_keys_backup_banner">Schließe Key-Backup-Einblendung</string>
|
||||
@@ -1030,15 +1030,15 @@
|
||||
<string name="content_reported_title">Inhalt gemeldet</string>
|
||||
<string name="content_reported_content">Dieser Inhalt wurde gemeldet.
|
||||
\n
|
||||
\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
\nWenn du keine Inhalte mehr von dieser Person sehen möchtest, kannst du sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
<string name="content_reported_as_spam_title">Als Spam gemeldet</string>
|
||||
<string name="content_reported_as_spam_content">Dieser Inhalt wurde als Spam gemeldet.
|
||||
\n
|
||||
\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
\nWenn du keine Inhalte mehr von dieser Person sehen möchtest, kannst du sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
<string name="content_reported_as_inappropriate_title">Als unangebracht gemeldet</string>
|
||||
<string name="content_reported_as_inappropriate_content">Dieser Inhalt wurde als unangebracht gemeldet.
|
||||
\n
|
||||
\nWenn du keine weiteren Inhalte dieser Person sehen möchtest, kannst sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
\nWenn du keine Inhalte mehr von dieser Person sehen möchtest, kannst du sie ignorieren, um ihre Nachrichten auszublenden.</string>
|
||||
<string name="message_ignore_user">Nutzer ignorieren</string>
|
||||
<string name="room_list_quick_actions_notifications_all_noisy">Alle Nachrichten (laut)</string>
|
||||
<string name="room_list_quick_actions_notifications_all">Alle Nachrichten</string>
|
||||
@@ -1524,8 +1524,8 @@
|
||||
<string name="uploads_files_title">DATEIEN</string>
|
||||
<string name="uploads_files_subtitle">%1$s um %2$s</string>
|
||||
<string name="uploads_files_no_result">Es gibt in diesem Raum keine Dateien</string>
|
||||
<string name="room_list_quick_actions_favorite_add">Füge zu Favoriten hinzu</string>
|
||||
<string name="room_list_quick_actions_favorite_remove">Entferne von Favoriten</string>
|
||||
<string name="room_list_quick_actions_favorite_add">Zu Favoriten hinzufügen</string>
|
||||
<string name="room_list_quick_actions_favorite_remove">Aus Favoriten entfernen</string>
|
||||
<string name="notice_member_no_changes_by_you">Du hast keine Änderungen gemacht</string>
|
||||
<string name="room_join_rules_public_by_you">Du hast den Raum für alle, die den Link kennen, zugänglich gemacht.</string>
|
||||
<string name="room_join_rules_invite_by_you">Du hast den Raumbeitritt auf Einladungen beschränkt.</string>
|
||||
@@ -1622,9 +1622,9 @@
|
||||
<string name="settings_call_show_confirmation_dialog_title">Versehentliche Anrufe verhindern</string>
|
||||
<string name="settings_call_show_confirmation_dialog_summary">Bitte um Bestätigung, bevor du einen Anruf tätigst</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_submit">Einrichten</string>
|
||||
<string name="no_permissions_to_start_conf_call">Dir fehlt die Berechtigung in diesem Raum eine Konferenz zu starten</string>
|
||||
<string name="video_meeting">Beginne eine Videokonferenz</string>
|
||||
<string name="audio_meeting">Beginne eine Audiokonferenz</string>
|
||||
<string name="no_permissions_to_start_conf_call">Du bist nicht berechtigt, in diesem Raum ein Konferenzgespräch zu starten</string>
|
||||
<string name="video_meeting">Videokonferenz starten</string>
|
||||
<string name="audio_meeting">Audiokonferenz starten</string>
|
||||
<string name="audio_video_meeting_description">Konferenzen nutzen die Jitsi-Sicherheits- und Berechtigungsrichtlinien. Alle im Raum Anwesenden können während der Konferenz beitreten.</string>
|
||||
<string name="cannot_call_yourself">Du kannst dich nicht selbst anrufen</string>
|
||||
<string name="cannot_call_yourself_with_invite">Du kannst dich nicht selbst anrufen, warte bis Teilnehmer die Einladung annehmen</string>
|
||||
@@ -1670,7 +1670,7 @@
|
||||
<string name="sent_a_poll">Umfrage</string>
|
||||
<string name="sent_a_reaction">Reagierte mit: %s</string>
|
||||
<string name="universal_link_malformed">Der Link war fehlerhaft</string>
|
||||
<string name="no_permissions_to_start_webrtc_call">Du bist nicht berechtigt, einen Anruf in diesem Raum zu beginnen</string>
|
||||
<string name="no_permissions_to_start_webrtc_call">Du bist nicht berechtigt, in diesem Raum einen Anruf zu beginnen</string>
|
||||
<string name="sent_verification_conclusion">Ergebnis der Überprüfung</string>
|
||||
<string name="delete_account_data_warning">Kontodaten vom Typ %1$s löschen\?
|
||||
\n
|
||||
@@ -1681,8 +1681,8 @@
|
||||
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">Die Applikation wartet auf den PUSH</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_title">Push testen</string>
|
||||
<string name="search_banned_user_hint">Gebannte Nutzer filtern</string>
|
||||
<string name="no_permissions_to_start_webrtc_call_in_direct_room">Du bist nicht berechtigt einen Anruf zu beginnen</string>
|
||||
<string name="no_permissions_to_start_conf_call_in_direct_room">Du hast keine Berechtigung ein Konferenzgespräch zu starten</string>
|
||||
<string name="no_permissions_to_start_webrtc_call_in_direct_room">Du bist nicht berechtigt, einen Anruf zu beginnen</string>
|
||||
<string name="no_permissions_to_start_conf_call_in_direct_room">Du bist nicht berechtigt, ein Konferenzgespräch zu starten</string>
|
||||
<string name="settings_security_pin_code_notifications_summary_on">Details wie Raumnamen und Nachrichteninhalt zeigen.</string>
|
||||
<string name="settings_security_pin_code_notifications_title">Inhalt in Benachrichtigungen anzeigen</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_summary_off">PIN-Code ist die einzige Möglichkeit ${app_name} zu entsperren.</string>
|
||||
@@ -1729,11 +1729,11 @@
|
||||
<string name="settings_security_pin_code_notifications_summary_off">Nur die Anzahl ungelesener Nachrichten in der Benachrichtigung zeigen.</string>
|
||||
<string name="attachment_type_dialog_title">Füge Bild hinzu per</string>
|
||||
<string name="warning_room_not_created_yet">Der Raum ist noch nicht erstellt. Raumerstellung abbrechen\?</string>
|
||||
<string name="room_list_quick_actions_low_priority_add">Zu niedrige Priorität hinzufügen</string>
|
||||
<string name="room_list_quick_actions_low_priority_add">Zu niedriger Priorität hinzufügen</string>
|
||||
<string name="create_room_topic_hint">Thema</string>
|
||||
<string name="warning_unsaved_change_discard">Änderungen verwerfen</string>
|
||||
<string name="warning_unsaved_change">Es gibt ungespeicherte Änderungen. Änderungen verwerfen\?</string>
|
||||
<string name="room_list_quick_actions_low_priority_remove">Von niedrige Priorität entfernen</string>
|
||||
<string name="room_list_quick_actions_low_priority_remove">Aus niedriger Priorität entfernen</string>
|
||||
<string name="rotate_and_crop_screen_title">Rotieren und Zuschneiden</string>
|
||||
<string name="create_room_settings_section">Raumeinstellungen</string>
|
||||
<string name="create_room_topic_section">Raumthema (optional)</string>
|
||||
@@ -2026,7 +2026,7 @@
|
||||
</plurals>
|
||||
<string name="error_file_too_big_simple">Die Datei ist zu groß.</string>
|
||||
<string name="send_file_step_compressing_video">Video wird komprimiert (%d%%)</string>
|
||||
<string name="send_file_step_compressing_image">Komprimiere Bild …</string>
|
||||
<string name="send_file_step_compressing_image">Bild komprimieren …</string>
|
||||
<string name="use_as_default_and_do_not_ask_again">Als Standard festsetzen und nicht mehr fragen</string>
|
||||
<string name="option_always_ask">Jedes Mal fragen</string>
|
||||
<string name="directory_add_a_new_server_prompt">Gib den Namen des neuen Servers ein, den du erkunden möchtest.</string>
|
||||
@@ -2128,9 +2128,9 @@
|
||||
<item quantity="other">%d verpasste Sprachanrufe</item>
|
||||
</plurals>
|
||||
<string name="hs_client_url">Heim-Server-API-Adresse</string>
|
||||
<string name="denied_permission_voice_message">Um Sprachnachrichten zu senden, erlaube bitte Zugriff aufs Mikrofon.</string>
|
||||
<string name="denied_permission_camera">Um fortzufahren, erlaube bitte in den Systemeinstellungen Zugriff auf die Kamera.</string>
|
||||
<string name="denied_permission_generic">Für diese Aktion fehlen einige Berechtigungen, bitte erlaube diese in den Systemeinstellungen.</string>
|
||||
<string name="denied_permission_voice_message">Um Sprachnachrichten zu senden, gewähre bitte den Zugriff aufs Mikrofon.</string>
|
||||
<string name="denied_permission_camera">Um fortzufahren, gewähre bitte in den Systemeinstellungen den Zugriff auf die Kamera.</string>
|
||||
<string name="denied_permission_generic">Für diese Aktion fehlen einige Berechtigungen, bitte gewähre diese in den Systemeinstellungen.</string>
|
||||
<string name="voice_message_slide_to_cancel">Wische zum Abbrechen</string>
|
||||
<string name="spaces_which_can_access">Spaces mit Zugriff auf</string>
|
||||
<string name="allow_space_member_to_find_and_access">Space-Mitgliedern Auffinden und Zugriff erlauben.</string>
|
||||
@@ -2877,7 +2877,7 @@
|
||||
<string name="notice_voice_broadcast_ended">%1$s beendete eine Sprachübertragung.</string>
|
||||
<string name="stop_voice_broadcast_content">Möchtest du die Übertragung wirklich beenden\? Dies wird die Übertragung beenden und die vollständige Aufnahme im Raum bereitstellen.</string>
|
||||
<string name="stop_voice_broadcast_dialog_title">Live-Übertragung beenden\?</string>
|
||||
<string name="action_stop">Ja, beende</string>
|
||||
<string name="action_stop">Ja, beenden</string>
|
||||
<string name="rich_text_editor_link">Link setzen</string>
|
||||
<string name="set_link_edit">Link bearbeiten</string>
|
||||
<string name="set_link_create">Link erstellen</string>
|
||||
|
@@ -2958,4 +2958,12 @@
|
||||
<string name="crosssigning_verify_after_update">کاره بهروز شد</string>
|
||||
<string name="sign_out_anyway">خروج به هر صورت</string>
|
||||
<string name="sign_out_failed_dialog_message">نمیتوان به کارساز خانگی رسید. اگر همچنان خارج شوید این افزاره از سیاههٔ افزارههایتان پاک نخواهد شد و باید از کارخواهس دیگر برش دارید.</string>
|
||||
<string name="invite_unknown_users_dialog_content">ناتوان در یافتن نمایهها برای شناسههای ماتریکس سیاهه شده. میخواهید به هر حال دعوتشان کنید؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="create_room_unknown_users_dialog_content">ناتوان در یافتن نمایهها برای شناسههای ماتریکس سیاهه شده. میخواهید به هر حال گپی بیاغازید؟
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="create_room_unknown_users_dialog_submit">آغاز گپ به هر حال</string>
|
||||
<string name="invite_unknown_users_dialog_submit">دعوت به هر حال</string>
|
||||
</resources>
|
@@ -659,7 +659,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="send_suggestion_content">Lūdzu, rakstiet savu ieteikumu zemāk.</string>
|
||||
<string name="send_suggestion">Ieteikumi</string>
|
||||
<string name="preference_root_help_about">Palīdzība un par lietotni</string>
|
||||
<string name="settings_security_and_privacy">Drošība un konfidencialitāte</string>
|
||||
<string name="settings_security_and_privacy">Drošība un privātums</string>
|
||||
<string name="settings_general_title">Vispārīgi</string>
|
||||
<string name="create_room_public_title">Publiska</string>
|
||||
<string name="create_room_settings_section">Istabas iestatījumi</string>
|
||||
@@ -1236,7 +1236,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="initial_sync_start_server_computing">Sākotnējā sinhronizācija:
|
||||
\nGaida servera atbildi…</string>
|
||||
<string name="labs_show_unread_notifications_as_tab">Pievienojiet speciālu cilni priekš nelasītiem paziņojumiem galvenajā ekrānā.</string>
|
||||
<string name="labs_swipe_to_reply_in_timeline">Ieslēgt pavilkšanas žestu, lai atbildētu uz ziņu</string>
|
||||
<string name="labs_swipe_to_reply_in_timeline">Laika joslā iespējot atbildēšanu pavelkot</string>
|
||||
<string name="no_message_edits_found">Labojumi netika atrasti</string>
|
||||
<string name="message_edits">Ziņu labojumi</string>
|
||||
<string name="settings_labs_show_complete_history_in_encrypted_room">Rādīt pilnu vēsturi šifrētajās istabās</string>
|
||||
@@ -1877,7 +1877,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="settings_server_default_room_version">Noklusējuma versija</string>
|
||||
<string name="settings_server_room_versions">Istabu versijas 👓</string>
|
||||
<string name="settings_category_composer">Ziņu redaktors</string>
|
||||
<string name="settings_category_timeline">Hronoloģija</string>
|
||||
<string name="settings_category_timeline">Laika josla</string>
|
||||
<string name="verify_cannot_cross_sign">Šī sesija nevar kopīgot šo verifikāciju ar citām sesijām.
|
||||
\nVerifikācija tiks saglabāta lokāli un kopīgota kādā no nākamajām lietotnes versijām.</string>
|
||||
<string name="rendering_event_error_exception">${app_name} saskārās ar problēmu, atveidojot notikuma ar id \'%1$s\' saturu</string>
|
||||
@@ -1945,11 +1945,11 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="push_gateway_item_url">Url:</string>
|
||||
<string name="push_gateway_item_device_name">Sesijas attēlojamais nosaukums:</string>
|
||||
<string name="push_gateway_item_app_display_name">Lietotnes attēlojamais nosaukums:</string>
|
||||
<string name="push_gateway_item_push_key">Iesūtīšanas atslēga:</string>
|
||||
<string name="push_gateway_item_push_key">Pašpiegādes atslēga:</string>
|
||||
<string name="push_gateway_item_app_id">Lietotnes identifikators:</string>
|
||||
<string name="settings_push_gateway_no_pushers">Nav reģistrētu Palaišanas vārtu</string>
|
||||
<string name="settings_push_rules_no_rules">Nav iestatīti Palaišanas Noteikumi</string>
|
||||
<string name="settings_push_rules">Palaišanas Noteikumi</string>
|
||||
<string name="settings_push_gateway_no_pushers">Nav norādītas pašpiegādes vārtejas</string>
|
||||
<string name="settings_push_rules_no_rules">Nav norādīti pašpiegādes nosacījumi</string>
|
||||
<string name="settings_push_rules">Pašpiegādes nosacījumi</string>
|
||||
<string name="create_new_space">Radīt jaunu Telpu</string>
|
||||
<string name="error_user_already_logged_in">Izskatās, ka mēģini izveidot savienojumu ar citu mājasserveri. Vai atteikties\?</string>
|
||||
<string name="room_settings_room_notifications_notify_me">Paziņot mani priekš</string>
|
||||
@@ -1988,9 +1988,9 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
\n%1$s</string>
|
||||
<string name="settings_troubleshoot_test_bg_restricted_success">Fona ierobežojumi ir izslēgti priekš ${app_name}. Šis tests jāveic, izmantojot mobilos datus (bez WIFI).
|
||||
\n%1$s</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_failed">Neizdevās saņemt palaišanu. Iespējams, vajag pārielādēt lietotni.</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_success">Lietotne saņem Palaišanu</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">Lietotne gaida Palaišanu</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_failed">Neizdevās saņemt pašpiegādi. Risinājums varētu būt atkārtota lietotnes uzstādīšana.</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_success">Lietotne saņem pašpiegādi</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">Lietotne gaida pašpiegādi</string>
|
||||
<string name="settings_troubleshoot_test_push_loop_title">Pārbaudīt pašpiegādi</string>
|
||||
<string name="settings_troubleshoot_test_fcm_failed_account_missing">[%1$s]
|
||||
\nŠī kļūda ir ārpus ${app_name} kontroles. Tālrunī nav Google konta. Lūdzu, atveriet kontu pārvaldnieku un pievienojiet Google kontu.</string>
|
||||
@@ -2054,8 +2054,8 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="open_poll_option_title">Atvērt aptauju</string>
|
||||
<string name="onboarding_new_app_layout_feedback_title">Sniegt atgriezenisko saiti</string>
|
||||
<string name="onboarding_new_app_layout_feedback_message">Piesist augšējā labajā stūrī, lai redzētu iespēju sniegt atgriezenisko saiti.</string>
|
||||
<string name="notice_voice_broadcast_ended_by_you">Tu beidzi balss pārraidi.</string>
|
||||
<string name="notice_voice_broadcast_ended">%1$s izbeidza balss pārraidi.</string>
|
||||
<string name="notice_voice_broadcast_ended_by_you">Tu beidzi balss apraidi.</string>
|
||||
<string name="notice_voice_broadcast_ended">%1$s izbeidza balss apraidi.</string>
|
||||
<string name="qr_code_login_header_show_qr_code_new_device_description">Izmantot ierīci, kurā veikta pieteikšanās, lai nolasītu zemāk esošo kvadrātkodu:</string>
|
||||
<string name="qr_code_login_header_show_qr_code_link_a_device_description">Nolasīt zemāk esošo kvadrātkodu ar ierīci, kurā ir notikusi atteikšanās.</string>
|
||||
<string name="qr_code_login_header_show_qr_code_title">Pieteikties ar kvadrātkodu</string>
|
||||
@@ -2132,7 +2132,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="thread_list_modal_my_threads_subtitle">Rāda visus pavedienus, kuros ir notikusi iesaistīšanās</string>
|
||||
<string name="call_start_screen_sharing">Kopīgot ekrānu</string>
|
||||
<string name="labs_enable_deferred_dm_summary">Izveidot tiešo ziņu tikai pēc pirmās ziņas</string>
|
||||
<string name="labs_enable_rich_text_editor_summary">Izmēģināt bagātinātu teksta rakstīšanu (vienkārša teksta ievade būs drīzumā)</string>
|
||||
<string name="labs_enable_rich_text_editor_summary">Izmēģināt bagātinātu teksta rakstīšanu (vienkārša teksta ievade gaidāma drīzumā)</string>
|
||||
<string name="invites_empty_title">Nekā jauna.</string>
|
||||
<string name="room_notification_more_than_two_users_are_typing">%1$s, %2$s un citi</string>
|
||||
<string name="thread_list_modal_all_threads_title">Visi pavedieni</string>
|
||||
@@ -2200,7 +2200,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="threads_labs_enable_notice_title">Pavedieni Beta</string>
|
||||
<string name="search_space_two_parents">%1$s un %2$s</string>
|
||||
<string name="analytics_opt_in_title">Palīdzēt uzlabot ${app_name}</string>
|
||||
<string name="settings_autoplay_animated_images_summary">Atskaņot kustīgos laikjoslas attēlus, tiklīdz tie ir redzami</string>
|
||||
<string name="settings_autoplay_animated_images_summary">Atskaņot kustīgos laika joslas attēlus, tiklīdz tie ir redzami</string>
|
||||
<string name="settings_troubleshoot_test_system_settings_permission_failed">${app_name} ir nepieciešama atļauja, lai parādītu paziņojumus.
|
||||
\nLūgums piešķirt atļauju.</string>
|
||||
<plurals name="search_space_multiple_parents">
|
||||
@@ -2341,7 +2341,7 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="contact_admin_to_restore_encryption">Lūgums sazināties ar pārvaldītāju, lai atjaunotu šifrēšanu derīgā stāvoklī.</string>
|
||||
<string name="ftue_auth_phone_confirmation_title">Apstiprināt tālruņa numuru</string>
|
||||
<string name="permalink_unsupported_groups">Šo saiti nevar atvērt: kopienas tika aizstātas ar vietām</string>
|
||||
<string name="started_a_voice_broadcast">Uzsāka balss pārraidi</string>
|
||||
<string name="started_a_voice_broadcast">Uzsāka balss apraidi</string>
|
||||
<string name="ftue_auth_use_case_skip_partial">Izlaist šo jautājumu</string>
|
||||
<string name="verification_request_waiting_for_recovery">Apliecina ar drošo atslēgu vai vārdkopu…</string>
|
||||
<string name="encryption_has_been_misconfigured">Šifrēšana ir kļūdaini uzstādīta.</string>
|
||||
@@ -2503,4 +2503,520 @@ Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string>
|
||||
<string name="alert_push_are_disabled_description">Jāpārskata iestatījumi, lai iespējotu pašpiegādes paziņojumus</string>
|
||||
<string name="save_recovery_key_chooser_hint">Saglabāt atkopšanas atslēgu</string>
|
||||
<string name="bottom_sheet_setup_secure_backup_submit">Uzstādīt</string>
|
||||
<plurals name="entries">
|
||||
<item quantity="zero">%d ierakstu</item>
|
||||
<item quantity="one">%d ieraksts</item>
|
||||
<item quantity="other">%d ieraksti</item>
|
||||
</plurals>
|
||||
<string name="settings_security_application_protection_title">Aizsargāt piekļuvi</string>
|
||||
<string name="settings_security_application_protection_screen_title">Iestatīt aizsardzību</string>
|
||||
<string name="auth_pin_reset_content">Lai atiestatītu PIN, būs nepieciešams atkārtoti pieteikties un izveidot jaunu.</string>
|
||||
<string name="create_pin_confirm_failure">Neizdevās apstiprināt PIN, lūgums ievadīt jaunu.</string>
|
||||
<string name="auth_biometric_key_invalidated_message">Biometriskā autentificēšanās tika atspējota, jo nesen tika pievienots jauns biometriskās autentifikācijas veids. To var atkal iespējot iestatījumos.</string>
|
||||
<string name="settings_security_application_protection_summary">Aizsargāt piekļuvi ar PIN un biometriju.</string>
|
||||
<plurals name="wrong_pin_message_remaining_attempts">
|
||||
<item quantity="zero">Nepareizs kods, atlikuši %d mēģinājumu</item>
|
||||
<item quantity="one">Nepareizs kods, atlicis %d mēģinājums</item>
|
||||
<item quantity="other">Nepareizs kods, atlikuši %d mēģinājumi</item>
|
||||
</plurals>
|
||||
<string name="too_many_pin_failures">Pārāk daudz kļūdu, tādēļ tiki izrakstīts</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_error">Nevarēja iespējot biometrisko autentifikāciju.</string>
|
||||
<string name="a11y_open_settings">Atvērt iestatījumus</string>
|
||||
<string name="a11y_trust_level_default">Noklusējuma uzticamības līmenis</string>
|
||||
<string name="settings_security_pin_code_summary">Ja ir vēlēšanas atiestatīt savu PIN, jāpiesit Aizmirsu PIN, lai atteiktos un atiesatītu.</string>
|
||||
<string name="settings_security_pin_code_grace_period_title">Pieprasīt PIN pēc 2 minūtēm</string>
|
||||
<string name="call_transfer_consult_first">Iepriekškonsultēšana</string>
|
||||
<string name="dev_tools_error_no_content">Nav satura</string>
|
||||
<string name="error_voice_message_unable_to_record">Nevar ierakstīt balss ziņu</string>
|
||||
<string name="voice_broadcast_buffering">Buferizācija…</string>
|
||||
<string name="a11y_trust_level_warning">Brīdinājuma uzticamības līmenis</string>
|
||||
<string name="re_authentication_default_confirm_text">${app_name} pieprasa ievadīt piekļuves datus, lai veiktu šo darbību.</string>
|
||||
<string name="a11y_unsent_draft">ir nenosūtīts melnraksts</string>
|
||||
<string name="dev_tools_explore_room_state">Izpētīt istabas stāvokli</string>
|
||||
<string name="a11y_trust_level_trusted">Uzticams uzticamības līmenis</string>
|
||||
<string name="a11y_checked">Atzīmēts</string>
|
||||
<string name="dev_tools_send_custom_event">Nosūtīt pielāgotu notikumu</string>
|
||||
<string name="dev_tools_edit_content">Labot saturu</string>
|
||||
<string name="dev_tools_send_custom_state_event">Nosūtīt pielāgotu stāvokļa notikumu</string>
|
||||
<string name="dev_tools_error_no_message_type">Trūkst ziņas veida</string>
|
||||
<string name="dev_tools_success_state_event">Stāvokļa notikums ir nosūtīts.</string>
|
||||
<string name="dev_tools_event_content_hint">Notikuma saturs</string>
|
||||
<string name="space_explore_filter_no_result_description">Dažas vietas var būt paslēptas, jo tās ir privātas un ir nepieciešams uzaicinājums uz tām.</string>
|
||||
<string name="spaces_feeling_experimental_subspace">Jūtams izmēģinājuma gars\?
|
||||
\nVietai ir iespējams pievienot esošas vietas.</string>
|
||||
<string name="error_voice_message_cannot_reply_or_edit">Nevar atbildēt vai labot, kamēr tiek izmantota balss ziņa</string>
|
||||
<string name="a11y_pause_audio_message">Apturēt %1$s</string>
|
||||
<string name="audio_message_reply_content">%1$s (%2$s)</string>
|
||||
<string name="audio_message_file_size">(%1$s)</string>
|
||||
<string name="upgrade">Jaunināt</string>
|
||||
<string name="voice_message_n_seconds_warning_toast">Atlikušas %1$d s</string>
|
||||
<plurals name="call_active_status">
|
||||
<item quantity="zero">%1$d notiekošu zvanu ·</item>
|
||||
<item quantity="one">%1$d notiekošs zvans ·</item>
|
||||
<item quantity="other">%1$d notiekoši zvani ·</item>
|
||||
</plurals>
|
||||
<string name="a11y_unchecked">Nav atzīmēts</string>
|
||||
<string name="dev_tools_menu_name">Izstrādātāja rīki</string>
|
||||
<string name="discovery_section">Atklājamība (%s)</string>
|
||||
<string name="space_add_existing_spaces">Pievienot esošas vietas</string>
|
||||
<string name="space_add_space_to_any_space_you_manage">Pievienot vietu jebkurai sevis pārvaldītai vietai.</string>
|
||||
<string name="labs_enable_thread_messages_desc">Piezīme: lietotne tiks pārsāknēta</string>
|
||||
<string name="settings_show_latest_profile">Parādīt jaunāko lietotāja informāciju</string>
|
||||
<string name="a11y_play_voice_message">Atskaņot balss ziņu</string>
|
||||
<string name="a11y_pause_voice_message">Apturēt balss ziņu</string>
|
||||
<string name="voice_message_reply_content">Balss ziņa (%1$s)</string>
|
||||
<string name="voice_broadcast_live">Tiešraide</string>
|
||||
<string name="a11y_resume_voice_broadcast_record">Atsākt balss apraides ierakstu</string>
|
||||
<string name="call_transfer_consulting_with">Konsultējas ar %1$s</string>
|
||||
<string name="voice_message_slide_to_cancel">Slidināt, lai atceltu</string>
|
||||
<string name="a11y_stop_voice_broadcast_record">Pārtraukt balss apraides ierakstu</string>
|
||||
<string name="call_transfer_title">Pārvirzīt</string>
|
||||
<string name="a11y_rule_notify_noisy">Paziņot ar skaņu</string>
|
||||
<string name="a11y_error_some_message_not_sent">Dažas ziņas netika nosūtītas</string>
|
||||
<string name="user_invites_you">%s uzaicina Tevi</string>
|
||||
<string name="upgrade_room_update_parent_space">Automātiski uzaicināt vietas vecākvienumu</string>
|
||||
<string name="error_failed_to_join_room">Atvainojamies, atgadījās kļūda pievienošanās mēģinājuma laikā: %s</string>
|
||||
<string name="a11y_import_key_from_file">Ievietot atslēgu no datnes</string>
|
||||
<string name="dev_tools_send_state_event">Nosūtīt stāvokļa notikumu</string>
|
||||
<string name="dev_tools_error_malformed_event">Nepareizi veidots notikums</string>
|
||||
<string name="space_mark_as_not_suggested">Atzīmēt kā neieteikto</string>
|
||||
<string name="a11y_start_voice_message">Ierakstīt balss ziņu</string>
|
||||
<string name="voice_broadcast_live_broadcast">Tiešraides apraide</string>
|
||||
<string name="upgrade_room_auto_invite">Automātiski uzaicināt lietotājus</string>
|
||||
<string name="a11y_presence_busy">Aizņemts</string>
|
||||
<string name="command_description_upgrade_room">Atjaunina istabu uz jaunu versiju</string>
|
||||
<plurals name="space_people_you_know">
|
||||
<item quantity="zero">Jau pievienojušies %d zināmu cilvēku</item>
|
||||
<item quantity="one">Jau pievienojies %d zināms cilvēks</item>
|
||||
<item quantity="other">Jau pievienojušies %d zināmi cilvēki</item>
|
||||
</plurals>
|
||||
<string name="dev_tools_form_hint_event_content">Notikuma saturs</string>
|
||||
<string name="upgrade_public_room_from_to">Šī istaba tiks jaunināta no %1$s uz %2$s.</string>
|
||||
<string name="space_mark_as_suggested">Atzīmēt kā ieteikto</string>
|
||||
<string name="settings_security_pin_code_notifications_title">Rādīt saturu paziņojumos</string>
|
||||
<string name="settings_security_pin_code_notifications_summary_on">Rādīt, piemēram, istabu nosaukumus un ziņas saturu.</string>
|
||||
<string name="settings_security_pin_code_notifications_summary_off">Attēlot tikai neizlasīto ziņu skaitu vienkāršā paziņojumā.</string>
|
||||
<string name="error_opening_banned_room">Nevar atvērt istabu, no kuras esi izslēgts.</string>
|
||||
<string name="call_transfer_transfer_to_title">Pārvirzīt uz %1$s</string>
|
||||
<string name="re_authentication_activity_title">Nepieciešama atkārtota autentificēšanās</string>
|
||||
<string name="dev_tools_state_event">Stāvokļa notikumi</string>
|
||||
<string name="room_alias_preview_not_found">Pašlaik šis aizstājvārds nav pieejams.
|
||||
\nVēlāk jāmēģina vēlreiz vai jāvaicā istabas pārvaldītājam, lai pārbauda, vai Tev ir piekļuve.</string>
|
||||
<string name="create_space_identity_server_info_none">Pašlaik netiek izmantots identitātes serveris. Lai varētu uzaicināt komandas biedrus un būtu tiem atklājams, jāiestata tāds zemāk.</string>
|
||||
<string name="space_leave_prompt_msg_private">Nebūs iespējams atkārtoti pievienoties bez uzaicinājuma.</string>
|
||||
<string name="space_explore_filter_no_result_title">Nekas netika atrasts</string>
|
||||
<string name="labs_auto_report_uisi">Automātiski ziņot par atšifrēšanas kļūdām.</string>
|
||||
<string name="labs_auto_report_uisi_desc">Sistēma automātiski nosūtīts žurnāla ierakstus, kad notiks kļūda \"nav iespējams atšifrēt\"</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_summary_on">Iespējot no ierīces atkarīgu biometriju, piemēram, pirkstu nospiedumus un sejas atpazīšanu.</string>
|
||||
<string name="settings_security_pin_code_grace_period_summary_on">PIN kods tiks pieprasīts pēc 2 minūšu ${app_name} neizmantošanas.</string>
|
||||
<string name="settings_security_pin_code_grace_period_summary_off">PIN kods tiek pieprasīts katrā ${app_name} atvēršanas reizē.</string>
|
||||
<string name="call_transfer_failure">Notika kļūda zvana pārvirzīšanas laikā</string>
|
||||
<string name="a11y_rule_notify_silent">Paziņot bez skaņas</string>
|
||||
<string name="dev_tools_success_event">Notikums ir nosūtīts.</string>
|
||||
<string name="event_status_a11y_failed">Neizdevās</string>
|
||||
<string name="space_leave_prompt_msg_as_admin">Tu esi vienīgais šīs vietas pārvaldītājs. Tās atstāšana nozīmē, ka neviens to nepārvaldīs.</string>
|
||||
<string name="labs_enable_thread_messages">Iespējot pavedienu ziņas</string>
|
||||
<string name="looking_for_someone_not_in_space">Meklē kādu, kurš nav %s\?</string>
|
||||
<string name="space_suggested">Ieteiktā</string>
|
||||
<string name="unnamed_room">Istaba bez nosaukuma</string>
|
||||
<string name="upgrade_required">Nepieciešams jauninājums</string>
|
||||
<string name="upgrade_private_room">Jaunināt privāto istabu</string>
|
||||
<string name="upgrade_room_no_power_to_manage">Ir nepieciešama atļauja, lai jauninātu istabu</string>
|
||||
<string name="a11y_stop_voice_message">Apturēt ierakstīšanu</string>
|
||||
<string name="a11y_recording_voice_message">Ieraksta balss ziņu</string>
|
||||
<string name="error_voice_message_broadcast_in_progress">Nevar uzsākt balss ziņu</string>
|
||||
<string name="a11y_audio_message_item">%1$s, %2$s, %3$s</string>
|
||||
<string name="a11y_play_audio_message">Atskaņot %1$s</string>
|
||||
<string name="settings_show_latest_profile_description">Parādīt jaunāko profila informāciju (attēlu un attēlojamo vārdu) visām ziņām.</string>
|
||||
<string name="it_may_take_some_time">Lūgums paciesties, tas var aizņemt kādu laiku.</string>
|
||||
<string name="upgrade_room_warning">Istabas jaunināšana ir sarežģīta darbība un parasti ir ieteicama, kad istaba ir nepastāvīga kļūdu, trūkstošu iespēju vai drošības ievainojamību dēļ.
|
||||
\nTas parasti tikai ietekmē to, kā istaba tiek apstrādāta serverī.</string>
|
||||
<string name="room_upgrade_to_recommended_version">Jaunināt uz ieteicamo istabas versiju</string>
|
||||
<string name="voice_message_tap_to_stop_toast">Jāpiesit ierakstam, lai klausītos vai apturētu</string>
|
||||
<string name="error_voice_message_broadcast_in_progress_message">Nevar uzsākt balss ziņu, jo pašreiz tiek ierakstīta tiešraides apraide. Lūgums to pārtraukt, lai varētu uzsākt balss ziņas ierakstīšanu</string>
|
||||
<string name="a11y_pause_voice_broadcast_record">Apturēt balss apraides ierakstu</string>
|
||||
<string name="error_voice_message_unable_to_play">Nevar atskaņot šo balss ziņu</string>
|
||||
<string name="a11y_error_message_not_sent">Ziņa nav nosūtīta kļūdas dēļ</string>
|
||||
<string name="share_by_text">Kopīgot ar tekstu</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_title">Iespējot biometriju</string>
|
||||
<string name="finish_setup">Pabeigt iestatīšanu</string>
|
||||
<string name="joining_replacement_room">Pievienoties aizvietotājistabai</string>
|
||||
<string name="a11y_rule_notify_off">Nepaziņot</string>
|
||||
<string name="finish_setting_up_discovery">Pabeigt atklājamības izveidošanu.</string>
|
||||
<string name="you_are_invited">Tu esi uzaicināts</string>
|
||||
<string name="upgrade_public_room">Jaunināt publisko istabu</string>
|
||||
<string name="a11y_audio_playback_duration" tools:ignore="PluralsCandidate">%1$d minūtes %2$d sekundes</string>
|
||||
<string name="error_audio_message_unable_to_play">Nespēj atskaņot %1$s</string>
|
||||
<string name="settings_security_pin_code_use_biometrics_summary_off">PIN kods ir vienīgais veids, kā atslēgt ${app_name}.</string>
|
||||
<string name="discovery_invite">Uzaicināt e-pastā, atrast kontaktus un vēl…</string>
|
||||
<string name="dev_tools_form_hint_state_key">Stāvokļa atslēga</string>
|
||||
<string name="space_leave_radio_buttons_title">Lietas šajā vietā</string>
|
||||
<string name="location_share_external">Atvērt ar</string>
|
||||
<string name="room_polls_wait_for_display">Attēlo aptaujas</string>
|
||||
<string name="location_share_option_user_live">Kopīgot atrašanās vietu tiešraidē</string>
|
||||
<string name="create_poll_question_title">Aptaujas jautājums vai temats</string>
|
||||
<string name="a11y_location_share_option_pinned_icon">Kopīgot šo atrašanās vietu</string>
|
||||
<string name="link_this_email_settings_link">Sasaistīt šo e-pasta adresi ar kontu</string>
|
||||
<plurals name="poll_total_vote_count_before_ended_and_voted">
|
||||
<item quantity="zero">Pamatojoties uz %1$d balsojumiem</item>
|
||||
<item quantity="one">Pamatojoties uz %1$d balsojumu</item>
|
||||
<item quantity="other">Pamatojoties uz %1$d balsojumiem</item>
|
||||
</plurals>
|
||||
<string name="create_poll_button">IZVEIDOT APTAUJU</string>
|
||||
<string name="poll_no_votes_cast">Nav saņemti balsojumi</string>
|
||||
<string name="end_poll_confirmation_title">Noslēgt šo aptauju\?</string>
|
||||
<string name="error_voice_broadcast_unauthorized_title">Nevar uzsākt jaunu balss apraidi</string>
|
||||
<string name="a11y_play_voice_broadcast">Atskaņot vai atsākt balss apraidi</string>
|
||||
<string name="a11y_voice_broadcast_fast_backward">Ātri patīt 30 sekundes atpakaļ</string>
|
||||
<string name="a11y_voice_broadcast_fast_forward">Ātri patīt 30 sekundes uz priekšu</string>
|
||||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Kāds cits jau ieraksta balss apraidi. Jāgaida, līdz tā beigsies, lai varētu uzsākt jaunu.</string>
|
||||
<string name="stop_voice_broadcast_content">Vai tiešām pārtraukt tiešraides apraidi\? Tas izbeigs apraidi, un istabā būs pieejams viss ieraksts.</string>
|
||||
<string name="this_invite_to_this_space_was_sent">Šis uzaicinājums uz šo vietu tika nosūtīts uz %s, kas nav saistīts ar Tavu kontu</string>
|
||||
<string name="labs_enable_latex_maths">Iespējot LaTeX matemātiku</string>
|
||||
<string name="create_poll_question_hint">Jautājums vai temats</string>
|
||||
<string name="create_poll_empty_question_error">Jautājums nevar būt tukšs</string>
|
||||
<plurals name="poll_total_vote_count_before_ended_and_not_voted">
|
||||
<item quantity="zero">Saņemti %1$d balsojumu. Jābalso, lai redzētu iznākumu</item>
|
||||
<item quantity="one">Saņemts %1$d balsojums. Jābalso, lai redzētu iznākumu</item>
|
||||
<item quantity="other">Saņemti %1$d balsojumi. Jābalso, lai redzētu iznākumu</item>
|
||||
</plurals>
|
||||
<plurals name="poll_total_vote_count_after_ended">
|
||||
<item quantity="zero">Galīgais iznākums, pamatojoties uz %1$d balsojumiem</item>
|
||||
<item quantity="one">Galīgais iznākums, pamatojoties uz %1$d balsojumu</item>
|
||||
<item quantity="other">Galīgais iznākums, pamatojoties uz %1$d balsojumiem</item>
|
||||
</plurals>
|
||||
<string name="poll_end_room_list_preview">Aptauja noslēgta</string>
|
||||
<string name="edit_poll_title">Labot aptauju</string>
|
||||
<string name="ended_poll_indicator">Noslēdza aptauju.</string>
|
||||
<string name="room_polls_load_more">Ielādēt vairāk aptauju</string>
|
||||
<string name="room_polls_loading_error">Kļūda aptauju izgūšanā.</string>
|
||||
<string name="a11y_location_share_locate_button">Pietuvināt līdz pašreizējai atrašanās vietai</string>
|
||||
<string name="location_share_live_select_duration_option_1">15 minūtes</string>
|
||||
<string name="live_location_description">Atrašanās vietas tiešraide</string>
|
||||
<string name="error_voice_broadcast_already_in_progress_message">Tu jau ieraksti balss apraidi. Lūgums pabeigt pašreizējo, lai uzsāktu jaunu.</string>
|
||||
<string name="end_poll_confirmation_description">Tas vairs neļaus cilvēkiem balsot, un tiks attēlots aptaujas galīgais iznākums.</string>
|
||||
<string name="poll_type_title">Aptaujas veids</string>
|
||||
<string name="open_poll_option_description">Balsotāji redz iznākumu, tiklīdz viņi ir nobalsojuši</string>
|
||||
<string name="location_share_live_enabled">Atrašanās vietas tiešraide iespējota</string>
|
||||
<string name="location_share_live_ended">Atrašanās vietas tiešraide beidzās</string>
|
||||
<string name="live_location_bottom_sheet_last_updated_at">Atjaunota pirms %1$s</string>
|
||||
<string name="unable_to_decrypt_some_events_in_poll">Atšifrēšanas kļūdu dēļ daži balsojumi var netikt ieskaitīti</string>
|
||||
<string name="location_share_option_pinned">Kopīgot šo atrašanās vietu</string>
|
||||
<string name="a11y_location_share_pin_on_map">Piespraust atlasīto atrašanās vietu kartē</string>
|
||||
<string name="location_share_live_view">Apskatīt atrašanās vietas tiešraidi</string>
|
||||
<string name="tooltip_attachment_photo">Atvērt kameru</string>
|
||||
<string name="tooltip_attachment_gallery">Nosūtīt attēlus un video</string>
|
||||
<string name="location_share_live_until">Tiešraidē līdz %1$s</string>
|
||||
<string name="a11y_static_map_image">Karte</string>
|
||||
<string name="location_not_available_dialog_title">${app_name} nevarēja piekļūt atrašanās vietai</string>
|
||||
<string name="a11y_pause_voice_broadcast">Apturēt balss apraidi</string>
|
||||
<string name="poll_undisclosed_not_ended">Iznākums būs redzams, kad aptauja būs beigusies</string>
|
||||
<string name="closed_poll_option_title">Aizvērta aptauja</string>
|
||||
<string name="location_share_live_remaining_time">Atlicis %1$s</string>
|
||||
<string name="labs_enable_msc3061_share_history">MSC3061: istabu atslēgu kopīgošana pagātnes ziņām</string>
|
||||
<string name="poll_response_room_list_preview">Saņemtais balsojums</string>
|
||||
<string name="delete_poll_dialog_title">Noņemt aptauju</string>
|
||||
<string name="location_share_live_select_duration_option_3">8 stundas</string>
|
||||
<string name="poll_end_action">Noslēgt aptauju</string>
|
||||
<string name="location_share_live_stop">Pārtraukt</string>
|
||||
<string name="stop_voice_broadcast_dialog_title">Pārtraukt tiešraides apraidi\?</string>
|
||||
<string name="create_poll_title">Izveidot aptauju</string>
|
||||
<string name="room_polls_ended_no_item">Šajā istabā nav noslēgto aptauju</string>
|
||||
<string name="location_share_live_select_duration_option_2">1 stundu</string>
|
||||
<string name="location_timeline_failed_to_load_map">Neizdevās ielādēt karti</string>
|
||||
<string name="labs_enable_live_location">Iespējot atrašanās vietas tiešraides kopīgošanu</string>
|
||||
<string name="end_poll_confirmation_approve_button">Noslēgt aptauju</string>
|
||||
<string name="error_voice_broadcast_unable_to_decrypt">Nav iespējams atšifrēt šo balss apraidi.</string>
|
||||
<string name="restart_the_application_to_apply_changes">Lietotne ir jāpārsāknē, lai izmaiņas stātos spēkā.</string>
|
||||
<string name="delete_poll_dialog_content">Vai tiešām noņemt šo aptauju\? To pēc noņemšanas vairs nevarēs atgūt.</string>
|
||||
<string name="room_polls_active_no_item">Šajā istabā nav notiekošu aptauju</string>
|
||||
<string name="room_polls_ended">Noslēgušās aptaujas</string>
|
||||
<string name="location_share_option_user_current">Kopīgot manu pašreizējo atrašanās vietu</string>
|
||||
<string name="a11y_location_share_option_user_live_icon">Kopīgot atrašanās vietu tiešraidē</string>
|
||||
<string name="live_location_not_enough_permission_dialog_description">Nepieciešamas atbilstošas atļaujas, lai šajā istabā varētu kopīgot atrašanās vietas tiešraidi.</string>
|
||||
<string name="location_activity_title_preview">Atrašanās vieta</string>
|
||||
<string name="a11y_location_share_option_user_current_icon">Kopīgot manu pašreizējo atrašanās vietu</string>
|
||||
<string name="location_share_live_select_duration_title">Kopīgot atrašanās vietu tiešraidē</string>
|
||||
<string name="location_share_loading_map_error">Nebija iespējams ielādēt karti
|
||||
\nŠis mājasserveris var nebūt iestatīts, lai attēlotu kartes.</string>
|
||||
<string name="live_location_share_location_item_share">Kopīgot atrašanās vietu</string>
|
||||
<string name="room_polls_active">Notiekošās aptaujas</string>
|
||||
<plurals name="room_polls_ended_no_item_for_loaded_period">
|
||||
<item quantity="zero">Iepriekšējās %1$d dienās nav noslēgto aptauju.
|
||||
\nJāielādē vairāk aptauju, lai apskatītu iepriekšējo dienu aptaujas.</item>
|
||||
<item quantity="one">Iepriekšējā dienā nav noslēgto aptauju.
|
||||
\nJāielādē vairāk aptauju, lai apskatītu iepriekšējo dienu aptaujas.</item>
|
||||
<item quantity="other">Iepriekšējās %1$d dienās nav noslēgto aptauju.
|
||||
\nJāielādē vairāk aptauju, lai apskatītu iepriekšējo dienu aptaujas.</item>
|
||||
</plurals>
|
||||
<string name="location_not_available_dialog_content">${app_name} nevarēja piekļūt atrašanās vietai. Lūgums vēlāk mēģināt vēlreiz.</string>
|
||||
<string name="error_voice_broadcast_permission_denied_message">Nav nepieciešamo atļauju, lai uzsāktu balss apraidi šajā istabā. Jāsazinās ar istabas pārvaldītāju, lai paaugstinātu atļaujas.</string>
|
||||
<plurals name="room_polls_active_no_item_for_loaded_period">
|
||||
<item quantity="zero">Iepriekšējās %1$d dienās nav notiekošu aptauju.
|
||||
\nJāielādē vairāk aptauju, lai redzētu iepriekšējo dienu aptaujas.</item>
|
||||
<item quantity="one">Iepriekšējā dienā nav notiekošu aptauju.
|
||||
\nJāielādē vairāk aptauju, lai redzētu iepriekšējo dienu aptaujas.</item>
|
||||
<item quantity="other">Iepriekšējās %1$d dienās nav notiekošu aptauju.
|
||||
\nJāielādē vairāk aptauju, lai redzētu iepriekšējo dienu aptaujas.</item>
|
||||
</plurals>
|
||||
<string name="room_poll_details_go_to_timeline">Apskatīt aptauju laika joslā</string>
|
||||
<string name="location_share_live_started">Ielādē atrašanās vietas tiešraidi…</string>
|
||||
<string name="error_voice_broadcast_unable_to_play">Nav iespējams atskaņot šo balss apraidi.</string>
|
||||
<string name="location_activity_title_static_sharing">Kopīgot atrašanās vietu</string>
|
||||
<string name="live_location_sharing_notification_title">${app_name} atrašanās vietas tiešraide</string>
|
||||
<string name="error_voice_broadcast_no_connection_recording">Savienojuma kļūda - ierakstīšana apturēta</string>
|
||||
<string name="live_location_sharing_notification_description">Notiek atrašanās vietas kopīgošana</string>
|
||||
<string name="voice_broadcast_recording_time_left">Atlicis %1$s</string>
|
||||
<string name="labs_enable_live_location_summary">Pagaidu īstenojums: atrašanās vietas ir paliekošas istabas vēsturē</string>
|
||||
<string name="upgrade_room_for_restricted">Ikviens no %s varēs atrast šo istabu un pievienoties tai - nav nepieciešams pašrocīgi visus uzaicināt. To jebkurā laikā būs iespējams mainīt istabas iestatījumos.</string>
|
||||
<string name="live_location_not_enough_permission_dialog_title">Nav atļaujas kopīgot atrašanās vietas tiešraidi</string>
|
||||
<string name="upgrade_room_for_restricted_no_param">Ikviens vecākvietā varēs atrast šo istabu un pievienoties tai - nav nepieciešams pašrocīgi visus uzaicināt. To jebkurā laikā ir iespējams mainīt istabas iestatījumos.</string>
|
||||
<string name="upgrade_room_for_restricted_note">Lūgums ņemt vērā, ka jaunināšana izveidos jaunu istabas versiju. Visas pašreizējās ziņas paliks šajā arhivētajā istabā.</string>
|
||||
<string name="message_bubbles">Rādīt ziņu burbuļus</string>
|
||||
<string name="this_invite_to_this_room_was_sent">Šis uzaicinājums uz šo istabu tika nosūtīts uz %s, kas nav saistīta ar Tavu kontu</string>
|
||||
<string name="link_this_email_with_your_account">%s iestatījumos, lai saņemtu uzaicinājumus tieši ${app_name}.</string>
|
||||
<string name="labs_enable_msc3061_share_history_desc">Kad uzaicina šifrētā istabā, kas kopīgo vēsturi, būs redzama šifrētā vēsture.</string>
|
||||
<plurals name="poll_option_vote_count">
|
||||
<item quantity="zero">%1$d balsojumu</item>
|
||||
<item quantity="one">%1$d balsojums</item>
|
||||
<item quantity="other">%1$d balsojumi</item>
|
||||
</plurals>
|
||||
<string name="closed_poll_option_description">Iznākums tiek atklāts tikai pēc aptaujas noslēgšanas</string>
|
||||
<string name="attachment_type_selector_poll">Aptaujas</string>
|
||||
<string name="attachment_type_selector_camera">Kamera</string>
|
||||
<string name="settings_troubleshoot_test_current_endpoint_title">Galamērķis</string>
|
||||
<string name="device_manager_other_sessions_multi_signout_selection">Atteikties</string>
|
||||
<plurals name="device_manager_other_sessions_multi_signout_all">
|
||||
<item quantity="zero">Atteikties no %1$d sesijām</item>
|
||||
<item quantity="one">Atteikties no %1$d sesijas</item>
|
||||
<item quantity="other">Atteikties no %1$d sesijām</item>
|
||||
</plurals>
|
||||
<string name="device_manager_other_sessions_show_ip_address">Parādīt IP adresi</string>
|
||||
<string name="device_manager_other_sessions_hide_ip_address">Paslēpt IP adresi</string>
|
||||
<string name="device_manager_session_details_application_name">Nosaukums</string>
|
||||
<string name="device_manager_session_rename_edit_hint">Sesijas nosaukums</string>
|
||||
<string name="device_manager_learn_more_sessions_unverified_title">Neapliecinātās sesijas</string>
|
||||
<plurals name="message_reaction_show_more">
|
||||
<item quantity="zero">Vēl %1$d</item>
|
||||
<item quantity="one">Vēl %1$d</item>
|
||||
<item quantity="other">Vēl %1$d</item>
|
||||
</plurals>
|
||||
<string name="device_manager_learn_more_session_rename_title">Sesiju pārdēvēšana</string>
|
||||
<string name="device_manager_session_overview_signout">Atteikties no šīs sesijas</string>
|
||||
<string name="tooltip_attachment_file">Augšupielādēt datni</string>
|
||||
<string name="tooltip_attachment_poll">Izveidot aptauju</string>
|
||||
<string name="attachment_type_selector_voice_broadcast">Balss apraide</string>
|
||||
<string name="message_reaction_show_less">Rādīt mazāk</string>
|
||||
<string name="room_message_autocomplete_notification">Istabas paziņojums</string>
|
||||
<string name="screen_sharing_notification_title">${app_name} ekrāna kopīgošana</string>
|
||||
<string name="settings_troubleshoot_test_distributors_fdroid">Netika atrasts neviens cits veids bez sinhronizēšanas fonā.</string>
|
||||
<string name="settings_troubleshoot_test_current_endpoint_success">Pašreizējais galamērķis: %s</string>
|
||||
<string name="settings_troubleshoot_test_current_gateway_title">Vārteja</string>
|
||||
<string name="device_manager_verification_status_unknown">Nezināms apliecinājuma stāvoklis</string>
|
||||
<string name="device_manager_other_sessions_view_all">Apskatīt visas (%1$d)</string>
|
||||
<string name="device_manager_unverified_sessions_description">Jāapliecina vai jāatsakās no neapliecinātām sesijām.</string>
|
||||
<string name="device_manager_filter_option_verified_description">Gatavas drošai ziņapmaiņai</string>
|
||||
<string name="device_manager_filter_option_unverified_description">Nav gatavas drošai ziņapmaiņai</string>
|
||||
<string name="device_manager_other_sessions_recommendation_description_verified">Vislabākajai drošībai jāatsakās no jebkuras neatpazīstamas vai vairs neizmantotas sesijas.</string>
|
||||
<string name="device_manager_other_sessions_recommendation_description_unverified">Jāapliecina savas sesijas paplašinātai drošajai ziņapmaiņai vai jāatsakās no tām, kas nav atpazīstamas vai vairs netiek izmantotas.</string>
|
||||
<string name="device_manager_session_details_description">Informācija par lietotne, ierīci un darbībām.</string>
|
||||
<string name="device_manager_session_details_device_operating_system">Operētājsistēma</string>
|
||||
<string name="device_manager_learn_more_sessions_verified_title">Apliecinātas sesijas</string>
|
||||
<string name="screen_sharing_notification_description">Notiek ekrāna kopīgošana</string>
|
||||
<string name="labs_enable_element_call_permission_shortcuts_summary">Automātiski apstiprināt Element zvanu logrīkus un piešķirt piekļuvi kamerai/mikrofonam</string>
|
||||
<string name="device_manager_filter_bottom_sheet_title">Atlase</string>
|
||||
<string name="device_manager_other_sessions_recommendation_title_unverified">Neapliecinātas</string>
|
||||
<string name="device_manager_session_details_application">Lietotne</string>
|
||||
<string name="device_manager_verification_status_unverified">Neapliecināta sesija</string>
|
||||
<string name="settings_troubleshoot_test_distributors_title">Pieejamie veidi</string>
|
||||
<string name="device_manager_session_title">Sesija</string>
|
||||
<string name="device_manager_other_sessions_no_verified_sessions_found">Apliecinātas sesijas netika atrastas.</string>
|
||||
<string name="device_manager_session_rename">Pārdēvēt sesiju</string>
|
||||
<string name="unifiedpush_getdistributors_dialog_title">Izvēlēties, kā saņemt paziņojumus</string>
|
||||
<string name="unifiedpush_distributor_fcm_fallback">Google pakalpojumi</string>
|
||||
<plurals name="room_removed_messages">
|
||||
<item quantity="zero">Noņemtas %d ziņu</item>
|
||||
<item quantity="one">Noņemta %d ziņa</item>
|
||||
<item quantity="other">Noņemtas %d ziņas</item>
|
||||
</plurals>
|
||||
<string name="device_manager_verification_status_detail_current_session_unverified">Jāapliecina pašreizējā sesija paplašinātai drošajai ziņapmaiņai.</string>
|
||||
<string name="tooltip_attachment_contact">Atvērt kontaktus</string>
|
||||
<string name="a11y_device_manager_device_type_mobile">Tālrunis</string>
|
||||
<string name="device_manager_session_details_title">Sesijas izvērsums</string>
|
||||
<string name="attachment_type_selector_text_formatting">Teksta formatēšana</string>
|
||||
<string name="device_manager_device_title">Iekārta</string>
|
||||
<string name="device_manager_session_details_session_name">Sesijas nosaukums</string>
|
||||
<string name="attachment_type_selector_location">Atrašanās vieta</string>
|
||||
<string name="settings_troubleshoot_test_current_distributor_title">Veids</string>
|
||||
<string name="device_manager_header_section_security_recommendations_description">Uzlabo sava konta drošību ievērojot šos ieteikumus.</string>
|
||||
<string name="device_manager_session_details_device_browser">Pārlūks</string>
|
||||
<string name="device_manager_sessions_sign_in_with_qr_code_title">Pieteikties ar kvadrātkodu</string>
|
||||
<string name="device_manager_verify_session">Apliecināt sesiju</string>
|
||||
<string name="a11y_device_manager_filter">Atlasīt</string>
|
||||
<string name="tooltip_attachment_location">Kopīgot atrašanās vietu</string>
|
||||
<string name="tooltip_attachment_voice_broadcast">Uzsākt balss apraidi</string>
|
||||
<string name="attachment_type_selector_contact">Kontakts</string>
|
||||
<string name="room_message_notify_everyone">Paziņot visai istabai</string>
|
||||
<string name="room_message_autocomplete_users">Lietotāji</string>
|
||||
<string name="unifiedpush_distributor_background_sync">Sinhronizēšana fonā</string>
|
||||
<string name="settings_notification_method">Paziņojuma veids</string>
|
||||
<string name="settings_troubleshoot_test_current_endpoint_failed">Nevar atrast galamērķi.</string>
|
||||
<string name="live_location_labs_promotion_title">Atrašanās vietas tiešraides kopīgošana</string>
|
||||
<string name="device_manager_sessions_other_title">Citas sesijas</string>
|
||||
<string name="a11y_device_manager_device_type_web">Tīmeklis</string>
|
||||
<string name="a11y_device_manager_device_type_desktop">Darbvirsma</string>
|
||||
<string name="device_manager_verification_status_detail_other_session_verified">Šī sesija ir gatava drošai ziņapmaiņai.</string>
|
||||
<string name="device_manager_other_sessions_description_unverified">Neapliecināta · Pēdējā darbība %1$s</string>
|
||||
<plurals name="device_manager_other_sessions_description_inactive">
|
||||
<item quantity="zero">Neizmantota %1$d+ dienas (%2$s)</item>
|
||||
<item quantity="one">Neizmantota %1$d+ dienu (%2$s)</item>
|
||||
<item quantity="other">Neizmantota %1$d+ dienas (%2$s)</item>
|
||||
</plurals>
|
||||
<string name="device_manager_unverified_sessions_title">Neapliecinātas sesijas</string>
|
||||
<string name="device_manager_session_details_session_last_activity">Pēdējā darbība</string>
|
||||
<string name="device_manager_session_details_application_version">Versija</string>
|
||||
<string name="device_manager_session_rename_warning">Lūgums ņemt vērā, ka sesiju nosaukumi ir redzami arī cilvēkiem, ar kuriem sazinies.</string>
|
||||
<string name="settings_troubleshoot_test_current_distributor">Pašreiz tiek izmantots %s.</string>
|
||||
<string name="device_manager_verification_status_detail_other_session_unknown">Jāapliecina pašreizējā sesija, lai atklātu šīs sesijas apliecinājuma stāvokli.</string>
|
||||
<string name="device_manager_filter_option_all_sessions">Visas sesijas</string>
|
||||
<string name="device_manager_header_section_security_recommendations_title">Drošības ieteikumi</string>
|
||||
<string name="device_manager_current_session_title">Pašreizējā sesija</string>
|
||||
<string name="device_manager_other_sessions_clear_filter">Notīrīt atlasi</string>
|
||||
<string name="tooltip_attachment_sticker">Nosūtīt uzlīmi</string>
|
||||
<string name="attachment_type_selector_gallery">Attēlu bibliotēka</string>
|
||||
<string name="attachment_type_selector_sticker">Uzlīmes</string>
|
||||
<string name="attachment_type_selector_file">Pielikumi</string>
|
||||
<string name="settings_troubleshoot_test_distributors_gplay">Netika atrasts neviens cits veids bez Google Play pakalpojuma.</string>
|
||||
<plurals name="settings_troubleshoot_test_distributors_many">
|
||||
<item quantity="zero">Atrasti %d veidu.</item>
|
||||
<item quantity="one">Atrasts %d veids.</item>
|
||||
<item quantity="other">Atrasti %d veidi.</item>
|
||||
</plurals>
|
||||
<string name="settings_troubleshoot_test_current_gateway">Pašreizējā vārteja: %s</string>
|
||||
<string name="a11y_device_manager_device_type_unknown">Nezināms ierīces veids</string>
|
||||
<string name="device_manager_view_details">Apskatīt izvērsumu</string>
|
||||
<string name="device_manager_inactive_sessions_title">Neizmantotas sesijas</string>
|
||||
<string name="device_manager_filter_option_verified">Apliecinātas</string>
|
||||
<string name="device_manager_filter_option_inactive">Neizmantotas</string>
|
||||
<string name="device_manager_other_sessions_recommendation_title_verified">Apliecinātas</string>
|
||||
<string name="device_manager_other_sessions_no_inactive_sessions_found">Neizmantotas sesijas netika atrastas.</string>
|
||||
<string name="device_manager_other_sessions_select">Atlasīt sesijas</string>
|
||||
<string name="device_manager_signout_all_other_sessions">Atteikties no visām pārējām sesijām</string>
|
||||
<string name="device_manager_session_details_device_ip_address">IP adrese</string>
|
||||
<string name="device_manager_session_details_application_url">URL</string>
|
||||
<string name="device_manager_session_details_device_model">Modelis</string>
|
||||
<string name="device_manager_learn_more_sessions_inactive_title">Neizmantotās sesijas</string>
|
||||
<string name="device_manager_learn_more_sessions_unverified">Neapliecinātās sesijas ir sesijas, kurās ir notikusi pieteikšanās ar Taviem datiem, bet nav starpapliecinātas.
|
||||
\n
|
||||
\nVajadzētu īpaši pārliecināties, ka šīs sesijas ir atpazīstamas, jo tās var norādīt uz nepilnvarotu konta izmantošanu.</string>
|
||||
<string name="live_location_labs_promotion_switch_title">Iespējot atrašanās vietas kopīgošanu</string>
|
||||
<plurals name="device_manager_inactive_sessions_description">
|
||||
<item quantity="zero">Jāapsver atteikšanās no vecajām sesijām (%1$d dienu vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
<item quantity="one">Jāapsver atteikšanās no vecajām sesijām (%1$d diena vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
<item quantity="other">Jāapsver atteikšanās no vecajām sesijām (%1$d dienas vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
</plurals>
|
||||
<string name="labs_enable_element_call_permission_shortcuts">Iespējot Element zvanu atļauju īsceļus</string>
|
||||
<string name="device_manager_session_last_activity">Pēdējā darbība %1$s</string>
|
||||
<string name="device_manager_sessions_other_description">Vislielākajai drošībai jāapliecina savas sesijas un jāatsakās no jebkuras neatpazītas vai neizmantotas sesijas.</string>
|
||||
<string name="device_manager_filter_option_unverified">Neapliecinātas</string>
|
||||
<string name="device_manager_verification_status_verified">Apliecināta sesija</string>
|
||||
<string name="device_manager_verification_status_detail_current_session_verified">Pašreizējā sesija ir gatava drošai ziņapmaiņai.</string>
|
||||
<string name="device_manager_verification_status_detail_other_session_unverified">Vislabākajai drošībai un uzticamībai jāapliecina šī sesija vai jāatsakās no tās.</string>
|
||||
<plurals name="device_manager_filter_option_inactive_description">
|
||||
<item quantity="zero">Neizmantotas %1$d dienu vai ilgāk</item>
|
||||
<item quantity="one">Neizmantotas %1$d dienu vai ilgāk</item>
|
||||
<item quantity="other">Neizmantotas %1$d dienas vai ilgāk</item>
|
||||
</plurals>
|
||||
<string name="device_manager_verification_status_detail_session_encryption_not_supported">Šī sesija nenodrošina šifrēšanu, tādēļ to nevar apliecināt.</string>
|
||||
<string name="device_manager_other_sessions_recommendation_title_inactive">Neizmantotas</string>
|
||||
<string name="device_manager_other_sessions_description_verified">Apliecināta · Pēdējā darbība %1$s</string>
|
||||
<plurals name="device_manager_other_sessions_recommendation_description_inactive">
|
||||
<item quantity="zero">Jāapsver atteikšanās no vecajām sesijām (%1$d dienu vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
<item quantity="one">Jāapsver atteikšanās no vecajām sesijām (%1$d diena vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
<item quantity="other">Jāapsver atteikšanās no vecajām sesijām (%1$d dienas vai vairāk), kas vairs netiek izmantotas.</item>
|
||||
</plurals>
|
||||
<string name="device_manager_other_sessions_description_unverified_current_session">Neapliecināta · Pašreizējā sesija</string>
|
||||
<string name="device_manager_other_sessions_no_unverified_sessions_found">Neapliecinātas sesijas netika atrastas.</string>
|
||||
<string name="device_manager_session_rename_description">Pielāgoti sesiju nosaukumi var palīdzēt vieglāk atpazīt savas ierīces.</string>
|
||||
<string name="device_manager_sessions_sign_in_with_qr_code_description">Tu vari izmanto šo ierīci, lai pieteiktos tālruņa vai tīmekļa ierīcē ar kvadrātkodu. Ir divi veidi, kā to izdarīt:</string>
|
||||
<string name="device_manager_learn_more_sessions_inactive">Neizmantotās sesijas ir sesijas, kas kādu laiku nav izmantotas, bet tās turpina saņemt šifrēšanas atslēgas.
|
||||
\n
|
||||
\nNeizmantotas sesijas noņemšana uzlabo drošību un veiktspēju un ir vieglāk noteikt, vai jauna sesija ir aizdomīga.</string>
|
||||
<string name="pill_message_from_unknown_user">Ziņa</string>
|
||||
<string name="qr_code_login_header_failed_other_device_not_signed_in_description">Otrā ierīcē ir jāpiesakās.</string>
|
||||
<string name="rich_text_editor_link">Iestatīt saiti</string>
|
||||
<string name="labs_enable_voice_broadcast_summary">Padara iespējamu ierakstīt un nosūtīt balss apraidi istabas laika joslā.</string>
|
||||
<string name="home_empty_no_unreads_message">Šeit tiks parādītas nelasītās ziņas, kad tādas būs.</string>
|
||||
<string name="qr_code_login_confirm_security_code">Apstiprināt</string>
|
||||
<string name="pill_message_in_room">Ziņa %s</string>
|
||||
<string name="home_empty_no_unreads_title">Nav nekā, par ko ziņot.</string>
|
||||
<string name="qr_code_login_new_device_instruction_2">Jādodas uz Iestatījumi -> Drošība un privātums</string>
|
||||
<string name="qr_code_login_signing_in_a_mobile_device">Piesakies mobilajā ierīcē\?</string>
|
||||
<string name="rich_text_editor_format_italic">Pielietot slīprakstu</string>
|
||||
<string name="message_reply_to_sender_sent_video">nosūtīja video.</string>
|
||||
<string name="qr_code_login_header_failed_device_is_not_supported_description">Sasaistīšana ar šo ierīci nav nodrošināta.</string>
|
||||
<string name="qr_code_login_link_a_device_show_qr_code_instruction_2">Jāatlasa \'Nolasīt kvadrātkodu\'</string>
|
||||
<string name="message_reply_to_sender_created_poll">izveidoja aptauju.</string>
|
||||
<string name="qr_code_login_header_connected_title">Izveidots drošs savienojums</string>
|
||||
<string name="qr_code_login_header_failed_title">Neveiksmīgs savienojums</string>
|
||||
<string name="qr_code_login_connecting_to_device">Savienojas ar ierīci</string>
|
||||
<string name="qr_code_login_signing_in">Piesakās</string>
|
||||
<string name="rich_text_editor_numbered_list">Pārslēgt numurētu sarakstu</string>
|
||||
<string name="rich_text_editor_inline_code">Pielietot iekļautu kodu</string>
|
||||
<string name="rich_text_editor_code_block">Pārslēgt koda bloku</string>
|
||||
<string name="message_reply_to_sender_sent_audio_file">nosūtīja skaņas datni.</string>
|
||||
<string name="settings_access_token_summary">Piekļuves pilnvara sniedz pilnu piekļuvi kontam. To nevajag kopīgot ne ar vienu.</string>
|
||||
<string name="onboarding_new_app_layout_welcome_title">Laipni lūdzam jaunajā skatā!</string>
|
||||
<string name="qr_code_login_new_device_instruction_3">Jāatlasa \'Parādīt kvadrātkodu\'</string>
|
||||
<string name="qr_code_login_link_a_device_scan_qr_code_instruction_1">Jāsāk pieteikšanās skatā</string>
|
||||
<string name="qr_code_login_show_qr_code_button">Rādīt kvadrātkodu šajā ierīcē</string>
|
||||
<string name="qr_code_login_status_no_match">Nav atbilstības\?</string>
|
||||
<string name="qr_code_login_try_again">Jāmēģina vēlreiz</string>
|
||||
<string name="rich_text_editor_format_underline">Pielietot pasvītrojumu</string>
|
||||
<string name="labs_enable_session_manager_title">Iespējot jauno sesiju pārvaldnieku</string>
|
||||
<string name="home_empty_no_rooms_title">Laipni lūdzam ${app_name},
|
||||
\n%s!</string>
|
||||
<string name="rich_text_editor_format_bold">Pielietot treknrakstu</string>
|
||||
<string name="message_reply_to_sender_sent_image">nosūtīja attēlu.</string>
|
||||
<string name="rich_text_editor_bullet_list">Pārslēgt vienkāršu sarakstu</string>
|
||||
<string name="set_link_text">Teksts</string>
|
||||
<string name="labs_enable_client_info_recording_title">Iespējot klienta informācijas ierakstīšanu</string>
|
||||
<string name="onboarding_new_app_layout_spaces_title">Piekļūt vietām</string>
|
||||
<string name="qr_code_login_header_failed_other_description">Pieprasījums neizdevās.</string>
|
||||
<string name="rich_text_editor_full_screen_toggle">Pārslēgt pilnekrānu</string>
|
||||
<string name="home_empty_space_no_rooms_title">%s
|
||||
\nizskatās mazliet tukša.</string>
|
||||
<string name="qr_code_login_header_failed_timeout_description">Sasaistīšana netika pabeigta nepieciešamajā laikā.</string>
|
||||
<string name="qr_code_login_new_device_instruction_1">Jāatver lietotne otrā ierīcē</string>
|
||||
<string name="qr_code_login_link_a_device_show_qr_code_instruction_1">Jāsāk pieteikšanās skatā</string>
|
||||
<string name="set_link_link">Saite</string>
|
||||
<string name="set_link_edit">Labot saiti</string>
|
||||
<string name="message_reply_to_sender_sent_voice_message">nosūtīja balss ziņu.</string>
|
||||
<string name="message_reply_to_sender_ended_poll">noslēdza aptauju.</string>
|
||||
<string name="message_reply_to_poll_preview">Aptauja</string>
|
||||
<string name="pill_message_in_unknown_room">Ziņa istabā</string>
|
||||
<string name="message_reply_to_ended_poll_preview">Noslēgtās aptaujas</string>
|
||||
<string name="labs_enable_session_manager_summary">Iegūsti labāku pārredzamību un pārraudzību pār visām savām sesijām!</string>
|
||||
<string name="labs_enable_voice_broadcast_title">Iespējot balss apraidi</string>
|
||||
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">Otrā ierīcē jau ir pieteikšanās.</string>
|
||||
<string name="rich_text_editor_indent">Ievietot atkāpi</string>
|
||||
<string name="rich_text_editor_unindent">Noņemt atkāpi</string>
|
||||
<string name="rich_text_editor_quote">Pārslēgt citātu</string>
|
||||
<string name="set_link_create">Izveidot saiti</string>
|
||||
<string name="settings_access_token">Piekļuves pilnvara</string>
|
||||
<string name="qr_code_login_header_failed_invalid_qr_code_description">Šis kvadrātkods ir nederīgs.</string>
|
||||
<string name="qr_code_login_header_failed_user_cancelled_description">Pieteikšanās tika atcelta otrā ierīcē.</string>
|
||||
<string name="qr_code_login_link_a_device_scan_qr_code_instruction_2">Jāatlasa \'Pieteikties ar kvadrātkodu\'</string>
|
||||
<string name="rich_text_editor_format_strikethrough">Pielietot pārsvītrojumu</string>
|
||||
<string name="message_reply_to_prefix">Atbildē uz</string>
|
||||
<string name="message_reply_to_sender_sent_file">nosūtīja datni.</string>
|
||||
<string name="message_reply_to_sender_sent_sticker">nosūtīja uzlīmi.</string>
|
||||
<string name="pill_message_from_user">Ziņa no %s</string>
|
||||
<string name="pill_message_unknown_room_or_space">Istaba/vieta</string>
|
||||
<string name="labs_enable_client_info_recording_summary">Ierakstīt klienta nosaukumu, versiju un URL, lai sesiju pārvaldniekā vieglāk atpazītu sesijas.</string>
|
||||
<string name="onboarding_new_app_layout_spaces_message">Piekļūsti savām vietām (apakšējā labajā stūrī) ātrāk un vienkāršāk kā jebkad iepriekš!</string>
|
||||
<string name="qr_code_login_header_connected_description">Jāpārbauda ierīce, kurā esi pieteicies, tajā vajadzētu būt attēlotam zemāk redzamajam kodam. Jāapstiprina, ka zemāk esošais kods saskan ar to, kas ir ierīcē:</string>
|
||||
<string name="qr_code_login_header_failed_denied_description">Pieprasījums tika atteikts otrā ierīcē.</string>
|
||||
<string name="qr_code_login_scan_qr_code_button">Nolasīt kvadrātkodu</string>
|
||||
<string name="qr_code_login_confirm_security_code_description">Lūgums nodrošināt, ka šī koda izcelsme ir zināma. Ar ierīču sasaistīšanu kādam tiks nodrošināta pilna piekļuve kontam.</string>
|
||||
</resources>
|
@@ -2909,4 +2909,53 @@
|
||||
<string name="sign_out_anyway">Terminar sessão ainda assim</string>
|
||||
<string name="verification_request_waiting_for_recovery">Verificação a partir de Chave ou Frase Segura…</string>
|
||||
<string name="verification_profile_other_device_untrust_info">Até que este utilizador confie nesta sessão, as mensagens enviadas para e a partir dela são marcadas com avisos.</string>
|
||||
<string name="pill_message_in_unknown_room">Mensagem na sala</string>
|
||||
<string name="rich_text_editor_numbered_list">Alternar lista numerada</string>
|
||||
<string name="room_polls_active">Votações ativas</string>
|
||||
<string name="room_poll_details_go_to_timeline">Ver votação na linha do tempo</string>
|
||||
<string name="unable_to_decrypt_some_events_in_poll">Devido a erros de descriptografia, alguns votos podem não serem contados</string>
|
||||
<string name="confirm_your_identity_after_update">A mensageria segura fora aprimorada com a atualização mais recente. Verifique novamente seu dispositivo.</string>
|
||||
<string name="ended_poll_indicator">Votação encerrada.</string>
|
||||
<string name="room_polls_ended">Votações anteriores</string>
|
||||
<string name="rich_text_editor_bullet_list">Alternar lista de marcadores</string>
|
||||
<string name="pill_message_unknown_room_or_space">Sala/Espaço</string>
|
||||
<string name="direct_room_encryption_enabled_waiting_users">Aguardando os usuários entrarem no ${app_name}</string>
|
||||
<string name="direct_room_encryption_enabled_waiting_users_tile_description">Assim que os usuários convidados entrarem no ${app_name}, vocês poderão conversar e a sala terá criptografia de ponta a ponta</string>
|
||||
<string name="room_polls_active_no_item">Não há votação ativa nesta sala</string>
|
||||
<string name="room_polls_ended_no_item">Não há votações anteriores nesta sala</string>
|
||||
<string name="message_reply_to_ended_poll_preview">Votação encerrada</string>
|
||||
<string name="secure_backup_reset_danger_warning">Prossiga apenas se você tem certeza que você perdeu todos os outros dispositivos e sua chave de segurança.</string>
|
||||
<string name="encrypted_by_deleted">Criptografado por um dispositivo apagado</string>
|
||||
<string name="rich_text_editor_unindent">Remover recuo</string>
|
||||
<string name="rich_text_editor_quote">Alternar aspas</string>
|
||||
<string name="rich_text_editor_inline_code">Aplicar formatação de código em linha</string>
|
||||
<string name="_resume">Prosseguir</string>
|
||||
<string name="verification_not_found">O pedido de verificação não foi encontrado. Ele pode ter sido cancelado ou tratado por outra sessão.</string>
|
||||
<string name="rich_text_editor_indent">Recuar</string>
|
||||
<string name="rich_text_editor_code_block">Alternar bloco de código</string>
|
||||
<string name="message_reply_to_sender_ended_poll">terminou uma votação.</string>
|
||||
<string name="message_reply_to_poll_preview">Votação</string>
|
||||
<string name="settings_access_token">Token de acesso</string>
|
||||
<string name="error_voice_message_broadcast_in_progress_message">Você não pode iniciar uma mensagem de voz porque está gravando uma transmissão ao vivo. Termine sua transmissão ao vivo para iniciar uma mensagem de voz</string>
|
||||
<plurals name="room_polls_ended_no_item_for_loaded_period">
|
||||
<item quantity="one">Não há votação anterior para antes de ontem.
|
||||
\nCarregue mais votações para ver as votações dos dias anteriores.</item>
|
||||
<item quantity="other">Não há votação anterior para os últimos %1$d dias.
|
||||
\nCarregue mais votações para ver as votações dos dias anteriores.</item>
|
||||
</plurals>
|
||||
<plurals name="room_polls_active_no_item_for_loaded_period">
|
||||
<item quantity="one">Não há votação ativa para antes de ontem.
|
||||
\nCarregue mais votações para ver as votações dos dias anteriores.</item>
|
||||
<item quantity="other">Não há votação ativa para os últimos %1$d dias.
|
||||
\nCarregue mais votações para ver as votações dos dias anteriores.</item>
|
||||
</plurals>
|
||||
<string name="room_polls_load_more">Carregar mais votações</string>
|
||||
<string name="room_polls_loading_error">Erro ao buscar votações.</string>
|
||||
<string name="settings_access_token_summary">Seu token de acesso fornece acesso completo à sua conta. Não o compartilhe com outras pessoais.</string>
|
||||
<string name="pill_message_from_user">Mensagem de %s</string>
|
||||
<string name="secure_backup_reset_all_no_other_devices_long">A redefinição de suas chaves de verificação não pode ser desfeita. Após a redefinição, você não terá acesso às mensagens criptografadas antigas e todos os amigos que fizeram a verificação anteriormente verão avisos de segurança até que você faça a verificação novamente com eles.</string>
|
||||
<string name="error_voice_message_broadcast_in_progress">Não foi possível iniciar a mensagem de voz</string>
|
||||
<string name="room_polls_wait_for_display">Exibindo votações</string>
|
||||
<string name="pill_message_from_unknown_user">Mensagem</string>
|
||||
<string name="pill_message_in_room">Mensagem em %s</string>
|
||||
</resources>
|
@@ -3060,4 +3060,5 @@
|
||||
<string name="rich_text_editor_code_block">Блок кода</string>
|
||||
<string name="rich_text_editor_indent">Подпункт</string>
|
||||
<string name="rich_text_editor_unindent">Пункт</string>
|
||||
<string name="settings_acceptable_use_policy">Политика пользования</string>
|
||||
</resources>
|
@@ -2945,4 +2945,13 @@
|
||||
<string name="crosssigning_verify_after_update">Aplikacioni u përditësua</string>
|
||||
<string name="sign_out_anyway">Dil, sido qoftë</string>
|
||||
<string name="sign_out_failed_dialog_message">S’kapet dot shërbyesi Home. Nëse keni dalë, sido qoftë, kjo pajisje s’do të fshihet te lista e pajisjeve tuaja, mund të doni ta hiqni duke përdorur klient tjetër.</string>
|
||||
<string name="create_room_unknown_users_dialog_content">S’arrihet të gjenden profile për ID-rat Matrix të radhitura më poshtë. Do të donit të fillohej një fjalosje, sido që të jetë\?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="settings_enable_direct_share_title">Aktivizo ndarje të drejtpërdrejtë me të tjerët</string>
|
||||
<string name="create_room_unknown_users_dialog_submit">Fillo fjalosje, sido që të jetë</string>
|
||||
<string name="invite_unknown_users_dialog_content">S’arrihet të gjenden profile për ID-rat Matrix të radhitura më poshtë. Do të donit të ftohen, sido qoftë\?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="invite_unknown_users_dialog_submit">Ftoji, sido qoftë</string>
|
||||
</resources>
|
@@ -2958,4 +2958,12 @@
|
||||
<string name="crosssigning_verify_after_update">App uppdaterad</string>
|
||||
<string name="sign_out_failed_dialog_message">Kan inte nå hemservern. Om du ändå loggar ut kommer den här enheten inte att raderas från din enhetslista, du kanske vill ta bort den med en annan klient.</string>
|
||||
<string name="sign_out_anyway">Logga ut ändå</string>
|
||||
<string name="create_room_unknown_users_dialog_content">Kunde in hitta profiler för Matrix-ID:n nedan. Vill du starta en chatt ändå\?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="create_room_unknown_users_dialog_submit">Starta chatt ändå</string>
|
||||
<string name="invite_unknown_users_dialog_content">Kunde inte hitta profiler för Matrix-ID:n nedan. Vill du bjuda in dem ändå\?
|
||||
\n
|
||||
\n%s</string>
|
||||
<string name="invite_unknown_users_dialog_submit">Bjud in ändå</string>
|
||||
</resources>
|
@@ -1734,4 +1734,32 @@
|
||||
<string name="action_view_threads">Konuları Görüntüle</string>
|
||||
<string name="initial_sync_request_title">Başlangıç eşitleme isteği</string>
|
||||
<string name="all_chats">Tüm Sohbetler</string>
|
||||
</resources>
|
||||
<string name="start_chat">Sohbet Başlat</string>
|
||||
<string name="notice_voice_broadcast_ended">%1$s bir ses yayınını sonlandırdı.</string>
|
||||
<string name="notice_voice_broadcast_ended_by_you">Bir sesli yayını sonlandırdınız.</string>
|
||||
<string name="create_room">Oda yarat</string>
|
||||
<string name="explore_rooms">Odaları keşfedin</string>
|
||||
<string name="initial_sync_request_content">${app_name}, aşağıdaki nedenden dolayı güncel olması için önbelleği temizlemelidir:
|
||||
\n %s
|
||||
\n
|
||||
\n Bu eylemin uygulamayı yeniden başlatacağını ve biraz zaman alabileceğini unutmayın.</string>
|
||||
<string name="initial_sync_request_reason_unignored_users">- Bazı kullanıcılar göz ardı edildi</string>
|
||||
<string name="a11y_expand_space_children">%s çocuğu genişlet</string>
|
||||
<string name="time_unit_minute_short">dk.</string>
|
||||
<string name="action_disable">Devre dışı bırak</string>
|
||||
<string name="time_unit_hour_short">sa.</string>
|
||||
<string name="change_space">Alanı Değiştir</string>
|
||||
<string name="sign_out_anyway">Yine de oturumu kapat</string>
|
||||
<plurals name="x_selected">
|
||||
<item quantity="one">%1$d seçildi</item>
|
||||
<item quantity="other">%1$d seçildi</item>
|
||||
</plurals>
|
||||
<string name="notice_display_name_changed_to">%1$s görünen adını %2$s olarak değiştirdi</string>
|
||||
<string name="sign_out_failed_dialog_message">Ana sunucuya ulaşılamıyor. Yine de çıkış yaparsanız, bu cihaz, cihaz listenizden silinmez, başka bir istemci kullanarak kaldırmak isteyebilirsiniz.</string>
|
||||
<plurals name="notice_room_server_acl_changes">
|
||||
<item quantity="one">%d sunucu ACL\'si değişti</item>
|
||||
<item quantity="other">%d sunucu ACL değişikliği</item>
|
||||
</plurals>
|
||||
<string name="time_unit_second_short">s.</string>
|
||||
<string name="a11y_collapse_space_children">%s çocuğu daralt</string>
|
||||
</resources>
|
@@ -57,7 +57,7 @@
|
||||
<string name="notice_room_name_changed_by_you">Ви змінили назву кімнати на: %1$s</string>
|
||||
<string name="notice_room_avatar_changed_by_you">Ви змінили аватар кімнати</string>
|
||||
<string name="notice_room_topic_changed_by_you">Ви змінили тему на: %1$s</string>
|
||||
<string name="notice_display_name_changed_from_by_you">Ви змінили показуване ім\'я з %1$s на %2$s</string>
|
||||
<string name="notice_display_name_changed_from_by_you">Ви змінили псевдонім з %1$s на %2$s</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Ви змінили свій аватар</string>
|
||||
<plurals name="room_displayname_four_and_more_members">
|
||||
<item quantity="one">%1$s, %2$s, %3$s та %4$d інший</item>
|
||||
@@ -85,7 +85,7 @@
|
||||
<string name="notice_placed_voice_call_by_you">Ви починаєте голосовий виклик.</string>
|
||||
<string name="notice_placed_video_call_by_you">Ви починаєте відеовиклик.</string>
|
||||
<string name="notice_room_avatar_changed">%1$s змінює аватар кімнати</string>
|
||||
<string name="notice_display_name_removed_by_you">Ви прибрали показуване ім\'я (%1$s)</string>
|
||||
<string name="notice_display_name_removed_by_you">Ви вилучили псевдонім (%1$s)</string>
|
||||
<string name="notice_room_remove_by_you">Ви вилучили %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s запрошує %2$s</string>
|
||||
<string name="notice_room_reject_by_you">Ви відхилили запрошення</string>
|
||||
@@ -137,7 +137,7 @@
|
||||
<string name="notice_answered_call_by_you">Ви відповіли на виклик.</string>
|
||||
<string name="notice_call_candidates_by_you">Ви надіслали дані для налаштування виклику.</string>
|
||||
<string name="notice_call_candidates">%s надсилає дані для налаштування виклику.</string>
|
||||
<string name="notice_display_name_set_by_you">Ви встановили собі показуване ім\'я %1$s</string>
|
||||
<string name="notice_display_name_set_by_you">Ви налаштували псевдонімом %1$s</string>
|
||||
<string name="notice_room_withdraw_by_you">Ви відкликали запрошення для %1$s</string>
|
||||
<string name="notice_room_ban_by_you">Ви заблокували %1$s</string>
|
||||
<string name="notice_room_unban_by_you">Ви заблокували %1$s</string>
|
||||
@@ -268,7 +268,7 @@
|
||||
<string name="search_members_hint">Фільтр переліку користувачів</string>
|
||||
<string name="search_no_results">Тут порожньо</string>
|
||||
<string name="settings_profile_picture">Аватар</string>
|
||||
<string name="settings_display_name">Показуване ім\'я</string>
|
||||
<string name="settings_display_name">Псевдонім</string>
|
||||
<string name="settings_add_email_address">Додати адресу е-пошти</string>
|
||||
<string name="settings_add_phone_number">Додати номер телефону</string>
|
||||
<string name="settings_app_info_link_summary">Екран системної інформації застосунку.</string>
|
||||
@@ -276,7 +276,7 @@
|
||||
<string name="settings_notification_ringtone">Тон сповіщення</string>
|
||||
<string name="settings_enable_all_notif">Увімкнути сповіщення для цього облікового запису</string>
|
||||
<string name="settings_enable_this_device">Увімкнути сповіщення для цього пристрою</string>
|
||||
<string name="settings_containing_my_display_name">Повідомлення, що містять моє показуване ім\'я</string>
|
||||
<string name="settings_containing_my_display_name">Повідомлення з моїм псевдонімом</string>
|
||||
<string name="settings_containing_my_user_name">Повідомлення, що містять моє ім\'я користувача</string>
|
||||
<string name="settings_messages_in_one_to_one">В особистих чатах</string>
|
||||
<string name="settings_messages_in_group_chat">У групових чатах</string>
|
||||
@@ -495,7 +495,7 @@
|
||||
<string name="command_description_part_room">Вийти з кімнати</string>
|
||||
<string name="command_description_topic">Встановити тему кімнати</string>
|
||||
<string name="command_description_remove_user">Вилучити користувача із вказаним ID</string>
|
||||
<string name="command_description_nick">Змінити Ваш псевдонім</string>
|
||||
<string name="command_description_nick">Змінити псевдонім</string>
|
||||
<string name="command_description_markdown">Увімкнути/Вимкнути розмітку Markdown</string>
|
||||
<string name="command_description_clear_scalar_token">Для виправлення керування застосунками Matrix</string>
|
||||
<string name="create">Створити</string>
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="room_widget_permission_theme">Ваша тема</string>
|
||||
<string name="room_widget_permission_user_id">Ваш ідентифікатор користувача</string>
|
||||
<string name="room_widget_permission_avatar_url">URL-адреса аватара</string>
|
||||
<string name="room_widget_permission_display_name">Ваше показуване ім\'я</string>
|
||||
<string name="room_widget_permission_display_name">Ваш псевдонім</string>
|
||||
<string name="room_widget_revoke_access">Скасувати доступ для мене</string>
|
||||
<string name="room_widget_open_in_browser">Відкрити у браузері</string>
|
||||
<string name="room_widget_reload">Перезавантажити віджет</string>
|
||||
@@ -1564,7 +1564,7 @@
|
||||
<string name="settings_encrypted_group_messages">Зашифровані групові повідомлення</string>
|
||||
<string name="settings_group_messages">Групові повідомлення</string>
|
||||
<string name="settings_messages_containing_username">Моє користувацьке ім\'я</string>
|
||||
<string name="settings_messages_containing_display_name">Моє показуване ім\'я</string>
|
||||
<string name="settings_messages_containing_display_name">Мій псевдонім</string>
|
||||
<string name="settings_messages_at_room">Повідомлення, які містять @room</string>
|
||||
<string name="settings_when_rooms_are_upgraded">Коли кімнати оновлено</string>
|
||||
<string name="settings_messages_in_e2e_group_chat">Зашифровані повідомлення в групових бесідах</string>
|
||||
@@ -2060,7 +2060,7 @@
|
||||
<string name="command_description_whois">Показує відомості про користувача</string>
|
||||
<string name="command_description_avatar_for_room">Змінює ваш аватар лише у поточній кімнаті</string>
|
||||
<string name="command_description_room_avatar">Змінює аватар поточної кімнати</string>
|
||||
<string name="command_description_nick_for_room">Змінює ваше показуване ім\'я лише у поточній кімнаті</string>
|
||||
<string name="command_description_nick_for_room">Змінює ваш псевдонім лише у поточній кімнаті</string>
|
||||
<string name="command_description_room_name">Установлює назву кімнати</string>
|
||||
<string name="settings_discovery_no_policy_provided">Сервер ідентифікації не надав жодних правил</string>
|
||||
<string name="spaces_feeling_experimental_subspace">Бажаєте поекспериментувати\?
|
||||
@@ -2408,7 +2408,7 @@
|
||||
<string name="tooltip_attachment_photo">Відкрити камеру</string>
|
||||
<string name="labs_auto_report_uisi_desc">Ваша система автоматично надсилатиме журнали, коли виникне помилка неможливості розшифрування</string>
|
||||
<string name="labs_auto_report_uisi">Автозвіт про помилки шифрування.</string>
|
||||
<string name="room_member_override_nick_color">Замінити колір показуваного імені</string>
|
||||
<string name="room_member_override_nick_color">Замінити колір псевдоніма</string>
|
||||
<string name="login_splash_already_have_account">Я вже маю обліковий запис</string>
|
||||
<string name="ftue_auth_carousel_encrypted_title">Захищене спілкування.</string>
|
||||
<string name="ftue_auth_carousel_control_title">Ви контролюєте все.</string>
|
||||
@@ -2517,8 +2517,8 @@
|
||||
<string name="ftue_profile_picture_subtitle">Час вказати ім’я</string>
|
||||
<string name="ftue_profile_picture_title">Додати зображення профілю</string>
|
||||
<string name="ftue_display_name_entry_footer">Ви можете змінити його пізніше</string>
|
||||
<string name="ftue_display_name_entry_title">Показуване ім\'я</string>
|
||||
<string name="ftue_display_name_title">Виберіть показуване ім\'я</string>
|
||||
<string name="ftue_display_name_entry_title">Псевдонім</string>
|
||||
<string name="ftue_display_name_title">Оберіть псевдонім</string>
|
||||
<string name="ftue_account_created_subtitle">Ваш обліковий запис %s створений</string>
|
||||
<string name="ftue_account_created_congratulations_title">Вітаємо!</string>
|
||||
<string name="ftue_account_created_take_me_home">На головну</string>
|
||||
@@ -2562,7 +2562,7 @@
|
||||
\n
|
||||
\nЗауважте, що ця дія перезапустить застосунок, і це може тривати деякий час.</string>
|
||||
<string name="initial_sync_request_title">Початковий запит синхронізації</string>
|
||||
<string name="settings_show_latest_profile_description">Показувати найновіші дані профілю (аватар і показуване ім\'я) для всіх повідомлень.</string>
|
||||
<string name="settings_show_latest_profile_description">Показувати найновіші дані профілю (аватар і псевдонім) для всіх повідомлень.</string>
|
||||
<string name="settings_show_latest_profile">Показувати найновіші дані користувача</string>
|
||||
<string name="a11y_presence_busy">Зайнятий</string>
|
||||
<string name="keys_backup_settings_signature_from_this_user">Резервна копія має дійсний підпис від цього користувача.</string>
|
||||
|
@@ -2554,4 +2554,6 @@
|
||||
<string name="device_manager_session_title">Phiên</string>
|
||||
<string name="device_manager_device_title">Thiết bị</string>
|
||||
<string name="device_manager_session_last_activity">Hoạt động cuối %1$s</string>
|
||||
<string name="_resume">Tiếp tục</string>
|
||||
<string name="a11y_presence_busy">Bận</string>
|
||||
</resources>
|
@@ -654,7 +654,7 @@
|
||||
<string name="settings_silent_notifications_preferences">设置静音通知</string>
|
||||
<string name="settings_system_preferences_summary">选择LED颜色、震动、铃声……</string>
|
||||
<string name="settings_cryptography_manage_keys">加密密钥管理</string>
|
||||
<string name="encryption_message_recovery">恢复已加密消息</string>
|
||||
<string name="encryption_message_recovery">已加密消息恢复</string>
|
||||
<string name="encryption_settings_manage_message_recovery_summary">管理密钥备份</string>
|
||||
<string name="notification_silent">静音</string>
|
||||
<string name="error_empty_field_enter_user_name">请输入一个用户名。</string>
|
||||
|
@@ -748,7 +748,7 @@
|
||||
<string name="backup">備份</string>
|
||||
<string name="sign_out_bottom_sheet_will_lose_secure_messages">除非您在登出前備份好金鑰,否則將無法再存取所有加密訊息。</string>
|
||||
<string name="action_sign_out_confirmation_simple">您確定要登出嗎?</string>
|
||||
<string name="encryption_message_recovery">還原加密訊息</string>
|
||||
<string name="encryption_message_recovery">加密訊息還原</string>
|
||||
<string name="error_empty_field_enter_user_name">請輸入使用者名稱。</string>
|
||||
<string name="keys_backup_setup">開始使用金鑰備份</string>
|
||||
<string name="keys_backup_setup_step1_advanced">(進階)</string>
|
||||
|
@@ -3,7 +3,6 @@ plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
apply from: '../flavor.gradle'
|
||||
|
||||
android {
|
||||
namespace "org.matrix.android.sdk.flow"
|
||||
|
@@ -41,7 +41,6 @@ dokkaHtml {
|
||||
}
|
||||
}
|
||||
}
|
||||
apply from: '../flavor.gradle'
|
||||
|
||||
android {
|
||||
namespace "org.matrix.android.sdk"
|
||||
@@ -63,7 +62,7 @@ android {
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.6.5\""
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.6.8\""
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||
@@ -158,7 +157,7 @@ dependencies {
|
||||
// implementation libs.androidx.appCompat
|
||||
implementation libs.androidx.core
|
||||
|
||||
rustCryptoImplementation libs.androidx.lifecycleLivedata
|
||||
implementation libs.androidx.lifecycleLivedata
|
||||
|
||||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleCommon
|
||||
@@ -216,8 +215,8 @@ dependencies {
|
||||
|
||||
implementation libs.google.phonenumber
|
||||
|
||||
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.10")
|
||||
// rustCryptoApi project(":library:rustCrypto")
|
||||
implementation("org.matrix.rustcomponents:crypto-android:0.3.16")
|
||||
// api project(":library:rustCrypto")
|
||||
|
||||
testImplementation libs.tests.junit
|
||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||
|
@@ -261,7 +261,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
return MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
||||
authData = createFakeMegolmBackupAuthData(),
|
||||
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")!!
|
||||
recoveryKey = BackupUtils.recoveryKeyFromPassphrase("3cnTdW")
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -34,7 +34,6 @@ import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@@ -542,7 +542,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertFails {
|
||||
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
|
||||
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key")!!,
|
||||
BackupUtils.recoveryKeyFromPassphrase("Bad recovery key"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -680,7 +680,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertFailsWith<InvalidParameterException> {
|
||||
keysBackupService.restoreKeysWithRecoveryKey(
|
||||
keysBackupService.keysBackupVersion!!,
|
||||
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")!!,
|
||||
BackupUtils.recoveryKeyFromBase58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CryptoStoreHelper {
|
||||
|
||||
fun createStore(): IMXCryptoStore {
|
||||
return RealmCryptoStore(
|
||||
realmConfiguration = RealmConfiguration.Builder()
|
||||
.name("test.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.build(),
|
||||
crossSigningKeysMapper = CrossSigningKeysMapper(MoshiProvider.providesMoshi()),
|
||||
userId = "userId_" + Random.nextInt(),
|
||||
deviceId = "deviceId_sample",
|
||||
clock = DefaultClock(),
|
||||
myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,142 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.realm.Realm
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.util.time.DefaultClock
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmManager
|
||||
import org.matrix.olm.OlmSession
|
||||
|
||||
private const val DUMMY_DEVICE_KEY = "DeviceKey"
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Ignore
|
||||
class CryptoStoreTest : InstrumentedTest {
|
||||
|
||||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
||||
private val cryptoStoreHelper = CryptoStoreHelper()
|
||||
private val clock = DefaultClock()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Realm.init(context())
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun test_metadata_realm_ok() {
|
||||
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
//
|
||||
// assertFalse(cryptoStore.hasData())
|
||||
//
|
||||
// cryptoStore.open()
|
||||
//
|
||||
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
//
|
||||
// assertTrue(cryptoStore.hasData())
|
||||
//
|
||||
// // Cleanup
|
||||
// cryptoStore.close()
|
||||
// cryptoStore.deleteStore()
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun test_lastSessionUsed() {
|
||||
// Ensure Olm is initialized
|
||||
OlmManager()
|
||||
|
||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
|
||||
assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
val olmAccount1 = OlmAccount().apply {
|
||||
generateOneTimeKeys(1)
|
||||
}
|
||||
|
||||
val olmSession1 = OlmSession().apply {
|
||||
initOutboundSession(
|
||||
olmAccount1,
|
||||
olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
||||
olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
|
||||
)
|
||||
}
|
||||
|
||||
val sessionId1 = olmSession1.sessionIdentifier()
|
||||
val olmSessionWrapper1 = OlmSessionWrapper(olmSession1)
|
||||
|
||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
||||
|
||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
val olmAccount2 = OlmAccount().apply {
|
||||
generateOneTimeKeys(1)
|
||||
}
|
||||
|
||||
val olmSession2 = OlmSession().apply {
|
||||
initOutboundSession(
|
||||
olmAccount2,
|
||||
olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
||||
olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first()
|
||||
)
|
||||
}
|
||||
|
||||
val sessionId2 = olmSession2.sessionIdentifier()
|
||||
val olmSessionWrapper2 = OlmSessionWrapper(olmSession2)
|
||||
|
||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
||||
|
||||
// Ensure sessionIds are distinct
|
||||
assertNotEquals(sessionId1, sessionId2)
|
||||
|
||||
// Note: we cannot be sure what will be the result of getLastUsedSessionId() here
|
||||
|
||||
olmSessionWrapper2.onMessageReceived(clock.epochMillis())
|
||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
||||
|
||||
// sessionId2 is returned now
|
||||
assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
Thread.sleep(2)
|
||||
|
||||
olmSessionWrapper1.onMessageReceived(clock.epochMillis())
|
||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
||||
|
||||
// sessionId1 is returned now
|
||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
// Cleanup
|
||||
olmSession1.releaseSession()
|
||||
olmSession2.releaseSession()
|
||||
|
||||
olmAccount1.releaseAccount()
|
||||
olmAccount2.releaseAccount()
|
||||
}
|
||||
}
|
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class PreShareKeysTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val e2eRoomID = testData.roomId
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
||||
// clear any outbound session
|
||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||
|
||||
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
|
||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
||||
Log.d("#E2E", "Room Key Received from alice $preShareCount")
|
||||
|
||||
// Force presharing of new outbound key
|
||||
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID)
|
||||
|
||||
testHelper.retryPeriodically {
|
||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
newKeysCount > preShareCount
|
||||
}
|
||||
|
||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
|
||||
// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
// val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
||||
//
|
||||
// val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
// val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId)!!
|
||||
// val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
||||
// assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||
// assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||
|
||||
// val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
|
||||
//
|
||||
// assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
||||
|
||||
// val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
||||
// .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
||||
//
|
||||
// assertEquals("The session received by bob should match what alice sent", 0, sharedIndex)
|
||||
|
||||
// Just send a real message as test
|
||||
val sentEventId = testHelper.sendMessageInRoom(aliceSession.getRoom(e2eRoomID)!!, "Allo")
|
||||
|
||||
val sentEvent = aliceSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!
|
||||
|
||||
// assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
|
||||
testHelper.retryPeriodically {
|
||||
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
|
||||
// check that no additional key was shared
|
||||
assertEquals(newKeysCount, bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys())
|
||||
}
|
||||
}
|
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
||||
import org.matrix.olm.OlmSession
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Ref:
|
||||
* - https://github.com/matrix-org/matrix-doc/pull/1719
|
||||
* - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages
|
||||
* - https://github.com/matrix-org/matrix-js-sdk/pull/780
|
||||
* - https://github.com/matrix-org/matrix-ios-sdk/pull/778
|
||||
* - https://github.com/matrix-org/matrix-ios-sdk/pull/784
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class UnwedgingTest : InstrumentedTest {
|
||||
|
||||
private lateinit var messagesReceivedByBob: List<TimelineEvent>
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
messagesReceivedByBob = emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* - Alice & Bob in a e2e room
|
||||
* - Alice sends a 1st message with a 1st megolm session
|
||||
* - Store the olm session between A&B devices
|
||||
* - Alice sends a 2nd message with a 2nd megolm session
|
||||
* - Simulate Alice using a backup of her OS and make her crypto state like after the first message
|
||||
* - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
|
||||
*
|
||||
* What Bob must see:
|
||||
* -> No issue with the 2 first messages
|
||||
* -> The third event must fail to decrypt at first because Bob the olm session is wedged
|
||||
* -> This is automatically fixed after SDKs restarted the olm session
|
||||
*/
|
||||
@Test
|
||||
fun testUnwedging() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
|
||||
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20))
|
||||
bobTimeline.start()
|
||||
|
||||
messagesReceivedByBob = emptyList()
|
||||
|
||||
// - Alice sends a 1st message with a 1st megolm session
|
||||
roomFromAlicePOV.sendService().sendTextMessage("First message")
|
||||
|
||||
// Wait for the message to be received by Bob
|
||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1)
|
||||
|
||||
messagesReceivedByBob.size shouldBeEqualTo 1
|
||||
val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
// - Store the olm session between A&B devices
|
||||
// Let us pickle our session with bob here so we can later unpickle it
|
||||
// and wedge our session.
|
||||
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)
|
||||
sessionIdsForBob!!.size shouldBeEqualTo 1
|
||||
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyCryptoDevice().identityKey()!!)!!
|
||||
|
||||
val oldSession = serializeForRealm(olmSession.olmSession)
|
||||
|
||||
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
|
||||
|
||||
messagesReceivedByBob = emptyList()
|
||||
Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
|
||||
// - Alice sends a 2nd message with a 2nd megolm session
|
||||
roomFromAlicePOV.sendService().sendTextMessage("Second message")
|
||||
|
||||
// Wait for the message to be received by Bob
|
||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2)
|
||||
|
||||
messagesReceivedByBob.size shouldBeEqualTo 2
|
||||
// Session should have changed
|
||||
val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
Assert.assertNotEquals(firstMessageSession, secondMessageSession)
|
||||
|
||||
// Let us wedge the session now. Set crypto state like after the first message
|
||||
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
|
||||
|
||||
aliceCryptoStore.storeSession(
|
||||
OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!),
|
||||
bobSession.cryptoService().getMyCryptoDevice().identityKey()!!
|
||||
)
|
||||
olmDevice.clearOlmSessionCache()
|
||||
|
||||
// Force new session, and key share
|
||||
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
|
||||
|
||||
Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
|
||||
// - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
|
||||
roomFromAlicePOV.sendService().sendTextMessage("Third message")
|
||||
// Bob should not be able to decrypt, because the session key could not be sent
|
||||
// Wait for the message to be received by Bob
|
||||
messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3)
|
||||
|
||||
messagesReceivedByBob.size shouldBeEqualTo 3
|
||||
|
||||
val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
|
||||
Assert.assertNotEquals(secondMessageSession, thirdMessageSession)
|
||||
|
||||
Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType())
|
||||
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
|
||||
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
|
||||
// Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
|
||||
|
||||
Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
|
||||
// It's a trick to force key request on fail to decrypt
|
||||
bobSession.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||
promise.resume(
|
||||
UserPasswordAuth(
|
||||
user = bobSession.myUserId,
|
||||
password = TestConstants.PASSWORD,
|
||||
session = flowResponse.session
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait until we received back the key
|
||||
testHelper.retryPeriodically {
|
||||
// we should get back the key and be able to decrypt
|
||||
val result = tryOrNull {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
}
|
||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
||||
result != null
|
||||
}
|
||||
|
||||
bobTimeline.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val listener = object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED }
|
||||
|
||||
if (messagesReceived.size == expectedCount) {
|
||||
removeListener(this)
|
||||
continuation.resume(messagesReceived)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addListener(listener)
|
||||
continuation.invokeOnCancellation { removeListener(listener) }
|
||||
}
|
||||
}
|
@@ -1,609 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.dbgState
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
class SASTest : InstrumentedTest {
|
||||
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
@Test
|
||||
fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
|
||||
Log.d("#E2E", "verification: doE2ETestWithAliceAndBobInARoom")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
Log.d("#E2E", "verification: initializeCrossSigning")
|
||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
||||
|
||||
Log.d("#E2E", "verification: requestVerificationAndWaitForReadyState")
|
||||
val txId = SasVerificationTestHelper(testHelper)
|
||||
.requestVerificationAndWaitForReadyState(scope, cryptoTestData, listOf(VerificationMethod.SAS))
|
||||
|
||||
Log.d("#E2E", "verification: startKeyVerification")
|
||||
aliceVerificationService.startKeyVerification(
|
||||
VerificationMethod.SAS,
|
||||
bobSession.myUserId,
|
||||
txId
|
||||
)
|
||||
|
||||
Log.d("#E2E", "verification: ensure bob has received start")
|
||||
testHelper.retryWithBackoff {
|
||||
Log.d("#E2E", "verification: ${bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state}")
|
||||
bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId, txId)?.state == EVerificationState.Started
|
||||
}
|
||||
|
||||
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId)
|
||||
|
||||
assertNotNull("Bob should have started verif transaction", bobKeyTx)
|
||||
assertTrue(bobKeyTx is SasVerificationTransaction)
|
||||
|
||||
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId)
|
||||
assertTrue(aliceKeyTx is SasVerificationTransaction)
|
||||
|
||||
assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId)
|
||||
|
||||
val aliceCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
|
||||
aliceVerificationService.requestEventFlow().onEach {
|
||||
Log.d("#E2E", "alice flow event $it | ${it.getTransaction()?.dbgState()}")
|
||||
val tx = it.getTransaction()
|
||||
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
|
||||
if (tx.state() is SasTransactionState.Cancelled) {
|
||||
aliceCancelled.complete(tx.state() as SasTransactionState.Cancelled)
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
|
||||
val bobCancelled = CompletableDeferred<SasTransactionState.Cancelled>()
|
||||
bobVerificationService.requestEventFlow().onEach {
|
||||
Log.d("#E2E", "bob flow event $it | ${it.getTransaction()?.dbgState()}")
|
||||
val tx = it.getTransaction()
|
||||
if (tx?.transactionId == txId && tx is SasVerificationTransaction) {
|
||||
if (tx.state() is SasTransactionState.Cancelled) {
|
||||
bobCancelled.complete(tx.state() as SasTransactionState.Cancelled)
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
|
||||
aliceVerificationService.cancelVerificationRequest(bobSession.myUserId, txId)
|
||||
|
||||
val cancelledAlice = aliceCancelled.await()
|
||||
val cancelledBob = bobCancelled.await()
|
||||
|
||||
assertEquals("Should be User cancelled on alice side", CancelCode.User, cancelledAlice.cancelCode)
|
||||
assertEquals("Should be User cancelled on bob side", CancelCode.User, cancelledBob.cancelCode)
|
||||
|
||||
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txId))
|
||||
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txId))
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val protocols = listOf("meh_dont_know")
|
||||
val tid = "00000000"
|
||||
|
||||
// Bob should receive a cancel
|
||||
var cancelReason: CancelCode? = null
|
||||
val cancelLatch = CountDownLatch(1)
|
||||
|
||||
val bobListener = object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
tx as SasVerificationTransaction
|
||||
if (tx.transactionId == tid && tx.state() is SasTransactionState.Cancelled) {
|
||||
cancelReason = (tx.state() as SasTransactionState.Cancelled).cancelCode
|
||||
cancelLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
// bobSession.cryptoService().verificationService().addListener(bobListener)
|
||||
|
||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
||||
// TODO canceledToDeviceEvent = event
|
||||
// TODO cancelLatch.countDown()
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO })
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceUserID = aliceSession.myUserId
|
||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
|
||||
val aliceListener = object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
tx as SasVerificationTransaction
|
||||
if (tx.state() is SasTransactionState.SasStarted) {
|
||||
runBlocking {
|
||||
tx.acceptVerification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
|
||||
|
||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
|
||||
|
||||
testHelper.await(cancelLatch)
|
||||
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val mac = listOf("shaBit")
|
||||
val tid = "00000000"
|
||||
|
||||
// Bob should receive a cancel
|
||||
val canceledToDeviceEvent: Event? = null
|
||||
val cancelLatch = CountDownLatch(1)
|
||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
||||
// TODO canceledToDeviceEvent = event
|
||||
// TODO cancelLatch.countDown()
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO })
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceUserID = aliceSession.myUserId
|
||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
|
||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, mac = mac)
|
||||
|
||||
testHelper.await(cancelLatch)
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fail("Not passing for the moment")
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val codes = listOf("bin", "foo", "bar")
|
||||
val tid = "00000000"
|
||||
|
||||
// Bob should receive a cancel
|
||||
var canceledToDeviceEvent: Event? = null
|
||||
val cancelLatch = CountDownLatch(1)
|
||||
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
|
||||
// TODO override fun onToDeviceEvent(event: Event?) {
|
||||
// TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) {
|
||||
// TODO if (event.contentAsJsonObject?.get("transaction_id")?.asString == tid) {
|
||||
// TODO canceledToDeviceEvent = event
|
||||
// TODO cancelLatch.countDown()
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO }
|
||||
// TODO })
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceUserID = aliceSession.myUserId
|
||||
val aliceDevice = aliceSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
|
||||
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, codes = codes)
|
||||
|
||||
testHelper.await(cancelLatch)
|
||||
|
||||
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
|
||||
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
|
||||
}
|
||||
|
||||
private suspend fun fakeBobStart(
|
||||
bobSession: Session,
|
||||
aliceUserID: String?,
|
||||
aliceDevice: String?,
|
||||
tid: String,
|
||||
protocols: List<String> = SasVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS,
|
||||
hashes: List<String> = SasVerificationTransaction.KNOWN_HASHES,
|
||||
mac: List<String> = SasVerificationTransaction.KNOWN_MACS,
|
||||
codes: List<String> = SasVerificationTransaction.KNOWN_SHORT_CODES
|
||||
) {
|
||||
val startMessage = KeyVerificationStart(
|
||||
fromDevice = bobSession.cryptoService().getMyCryptoDevice().deviceId,
|
||||
method = VerificationMethod.SAS.toValue(),
|
||||
transactionId = tid,
|
||||
keyAgreementProtocols = protocols,
|
||||
hashes = hashes,
|
||||
messageAuthenticationCodes = mac,
|
||||
shortAuthenticationStrings = codes
|
||||
)
|
||||
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
contentMap.setObject(aliceUserID, aliceDevice, startMessage)
|
||||
|
||||
// TODO val sendLatch = CountDownLatch(1)
|
||||
// TODO bobSession.cryptoRestClient.sendToDevice(
|
||||
// TODO EventType.KEY_VERIFICATION_START,
|
||||
// TODO contentMap,
|
||||
// TODO tid,
|
||||
// TODO TestMatrixCallback<Void>(sendLatch)
|
||||
// TODO )
|
||||
}
|
||||
|
||||
// any two devices may only have at most one key verification in flight at a time.
|
||||
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
|
||||
@Test
|
||||
fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
|
||||
val aliceCreatedLatch = CountDownLatch(2)
|
||||
val aliceCancelledLatch = CountDownLatch(1)
|
||||
val createdTx = mutableListOf<VerificationTransaction>()
|
||||
val aliceListener = object : VerificationService.Listener {
|
||||
override fun transactionCreated(tx: VerificationTransaction) {
|
||||
createdTx.add(tx)
|
||||
aliceCreatedLatch.countDown()
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
tx as SasVerificationTransaction
|
||||
if (tx.state() is SasTransactionState.Cancelled && !(tx.state() as SasTransactionState.Cancelled).byMe) {
|
||||
aliceCancelledLatch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
// aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobUserId = bobSession!!.myUserId
|
||||
val bobDeviceId = bobSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
|
||||
// TODO
|
||||
// aliceSession.cryptoService().downloadKeysIfNeeded(listOf(bobUserId), forceDownload = true)
|
||||
// aliceVerificationService.beginKeyVerification(listOf(VerificationMethod.SAS), bobUserId, bobDeviceId)
|
||||
// aliceVerificationService.beginKeyVerification(bobUserId, bobDeviceId)
|
||||
// testHelper.await(aliceCreatedLatch)
|
||||
// testHelper.await(aliceCancelledLatch)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that when alice starts a 'correct' request, bob agrees.
|
||||
*/
|
||||
// @Test
|
||||
// @Ignore("This test will be ignored until it is fixed")
|
||||
// fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
//
|
||||
// val aliceSession = cryptoTestData.firstSession
|
||||
// val bobSession = cryptoTestData.secondSession
|
||||
//
|
||||
// val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
// val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
||||
//
|
||||
// val aliceAcceptedLatch = CountDownLatch(1)
|
||||
// val aliceListener = object : VerificationService.Listener {
|
||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
// if (tx.state() is VerificationTxState.OnAccepted) {
|
||||
// aliceAcceptedLatch.countDown()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// aliceVerificationService.addListener(aliceListener)
|
||||
//
|
||||
// val bobListener = object : VerificationService.Listener {
|
||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
// if (tx.state() is VerificationTxState.OnStarted && tx is SasVerificationTransaction) {
|
||||
// bobVerificationService.removeListener(this)
|
||||
// runBlocking {
|
||||
// tx.acceptVerification()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// bobVerificationService.addListener(bobListener)
|
||||
//
|
||||
// val bobUserId = bobSession.myUserId
|
||||
// val bobDeviceId = runBlocking {
|
||||
// bobSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
// }
|
||||
//
|
||||
// aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null)
|
||||
// testHelper.await(aliceAcceptedLatch)
|
||||
//
|
||||
// aliceVerificationService.getExistingTransaction(bobUserId, )
|
||||
//
|
||||
// assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
|
||||
//
|
||||
// // check that agreement is valid
|
||||
// assertTrue("Agreed Protocol should be Valid", accepted != null)
|
||||
// assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol))
|
||||
// assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash))
|
||||
// assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode))
|
||||
//
|
||||
// accepted!!.shortAuthenticationStrings.forEach {
|
||||
// assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
|
||||
// }
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
// val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
// cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
||||
// val sasTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
|
||||
// val aliceSession = cryptoTestData.firstSession
|
||||
// val bobSession = cryptoTestData.secondSession!!
|
||||
// val transactionId = sasTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, supportedMethods)
|
||||
//
|
||||
// val latch = CountDownLatch(2)
|
||||
// val aliceListener = object : VerificationService.Listener {
|
||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
// Timber.v("Alice transactionUpdated: ${tx.state()}")
|
||||
// latch.countDown()
|
||||
// }
|
||||
// }
|
||||
// aliceSession.cryptoService().verificationService().addListener(aliceListener)
|
||||
// val bobListener = object : VerificationService.Listener {
|
||||
// override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
// Timber.v("Bob transactionUpdated: ${tx.state()}")
|
||||
// latch.countDown()
|
||||
// }
|
||||
// }
|
||||
// bobSession.cryptoService().verificationService().addListener(bobListener)
|
||||
// aliceSession.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, transactionId)
|
||||
//
|
||||
// testHelper.await(latch)
|
||||
// val aliceTx =
|
||||
// aliceSession.cryptoService().verificationService().getExistingTransaction(bobSession.myUserId, transactionId) as SasVerificationTransaction
|
||||
// val bobTx = bobSession.cryptoService().verificationService().getExistingTransaction(aliceSession.myUserId, transactionId) as SasVerificationTransaction
|
||||
//
|
||||
// assertEquals("Should have same SAS", aliceTx.getDecimalCodeRepresentation(), bobTx.getDecimalCodeRepresentation())
|
||||
//
|
||||
// val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
|
||||
// val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
|
||||
//
|
||||
// assertEquals(
|
||||
// "Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
|
||||
// bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
|
||||
// )
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
||||
val sasVerificationTestHelper = SasVerificationTestHelper(testHelper, cryptoTestHelper)
|
||||
val transactionId = sasVerificationTestHelper.requestVerificationAndWaitForReadyState(cryptoTestData, listOf(VerificationMethod.SAS))
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
||||
|
||||
val verifiedLatch = CountDownLatch(2)
|
||||
val aliceListener = object : VerificationService.Listener {
|
||||
|
||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
||||
Timber.v("RequestUpdated pr=$pr")
|
||||
}
|
||||
|
||||
var matched = false
|
||||
var verified = false
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx !is SasVerificationTransaction) return
|
||||
Timber.v("Alice transactionUpdated: ${tx.state()} on thread:${Thread.currentThread()}")
|
||||
when (tx.state()) {
|
||||
SasTransactionState.SasShortCodeReady -> {
|
||||
if (!matched) {
|
||||
matched = true
|
||||
runBlocking {
|
||||
delay(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SasTransactionState.Done -> {
|
||||
if (!verified) {
|
||||
verified = true
|
||||
verifiedLatch.countDown()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
// aliceVerificationService.addListener(aliceListener)
|
||||
|
||||
val bobListener = object : VerificationService.Listener {
|
||||
var accepted = false
|
||||
var matched = false
|
||||
var verified = false
|
||||
|
||||
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
||||
Timber.v("RequestUpdated: pr=$pr")
|
||||
}
|
||||
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
if (tx !is SasVerificationTransaction) return
|
||||
Timber.v("Bob transactionUpdated: ${tx.state()} on thread: ${Thread.currentThread()}")
|
||||
when (tx.state()) {
|
||||
// VerificationTxState.SasStarted -> {
|
||||
// if (!accepted) {
|
||||
// accepted = true
|
||||
// runBlocking {
|
||||
// tx.acceptVerification()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
SasTransactionState.SasShortCodeReady -> {
|
||||
if (!matched) {
|
||||
matched = true
|
||||
runBlocking {
|
||||
delay(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SasTransactionState.Done -> {
|
||||
if (!verified) {
|
||||
verified = true
|
||||
verifiedLatch.countDown()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
// bobVerificationService.addListener(bobListener)
|
||||
|
||||
val bobUserId = bobSession.myUserId
|
||||
val bobDeviceId = runBlocking {
|
||||
bobSession.cryptoService().getMyCryptoDevice().deviceId
|
||||
}
|
||||
aliceVerificationService.startKeyVerification(VerificationMethod.SAS, bobUserId, transactionId)
|
||||
|
||||
Timber.v("Await after beginKey ${Thread.currentThread()}")
|
||||
testHelper.await(verifiedLatch)
|
||||
|
||||
// Assert that devices are verified
|
||||
val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
|
||||
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
|
||||
bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyCryptoDevice().deviceId)
|
||||
|
||||
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
|
||||
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
cryptoTestData.initializeCrossSigning(cryptoTestHelper)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
val bobVerificationService = bobSession.cryptoService().verificationService()
|
||||
|
||||
val req = aliceVerificationService.requestKeyVerificationInDMs(
|
||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
bobSession.myUserId,
|
||||
cryptoTestData.roomId
|
||||
)
|
||||
|
||||
val requestID = req.transactionId
|
||||
|
||||
Log.v("TEST", "== requestID is $requestID")
|
||||
|
||||
testHelper.retryPeriodically {
|
||||
val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
|
||||
Log.v("TEST", "== prBobPOV is $prBobPOV")
|
||||
prBobPOV?.transactionId == requestID
|
||||
}
|
||||
|
||||
bobVerificationService.readyPendingVerification(
|
||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
aliceSession.myUserId,
|
||||
requestID
|
||||
)
|
||||
|
||||
// wait for alice to get the ready
|
||||
testHelper.retryPeriodically {
|
||||
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
|
||||
Log.v("TEST", "== prAlicePOV is $prAlicePOV")
|
||||
prAlicePOV?.transactionId == requestID && prAlicePOV.state == EVerificationState.Ready
|
||||
}
|
||||
|
||||
// Start concurrent!
|
||||
aliceVerificationService.startKeyVerification(
|
||||
method = VerificationMethod.SAS,
|
||||
otherUserId = bobSession.myUserId,
|
||||
requestId = requestID,
|
||||
)
|
||||
|
||||
bobVerificationService.startKeyVerification(
|
||||
method = VerificationMethod.SAS,
|
||||
otherUserId = aliceSession.myUserId,
|
||||
requestId = requestID,
|
||||
)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: SasVerificationTransaction?
|
||||
var bobPovTx: SasVerificationTransaction?
|
||||
|
||||
testHelper.retryPeriodically {
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID) as? SasVerificationTransaction
|
||||
Log.v("TEST", "== alicePovTx is $alicePovTx")
|
||||
alicePovTx?.state() == SasTransactionState.SasShortCodeReady
|
||||
}
|
||||
// wait for alice to get the ready
|
||||
testHelper.retryPeriodically {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID) as? SasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is $bobPovTx")
|
||||
bobPovTx?.state() == SasTransactionState.SasShortCodeReady
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.amshove.kluent.shouldNotBeNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class QrCodeTest : InstrumentedTest {
|
||||
|
||||
private val qrCode1 = QrCodeData.VerifyingAnotherUser(
|
||||
transactionId = "MaTransaction",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value1 =
|
||||
"MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
||||
|
||||
private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted(
|
||||
transactionId = "MaTransaction",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value2 =
|
||||
"MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
||||
|
||||
private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted(
|
||||
transactionId = "MaTransaction",
|
||||
deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value3 =
|
||||
"MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678"
|
||||
|
||||
private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5")
|
||||
|
||||
private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55")
|
||||
|
||||
@Test
|
||||
fun testEncoding1() {
|
||||
qrCode1.toEncodedString() shouldBeEqualTo value1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncoding2() {
|
||||
qrCode2.toEncodedString() shouldBeEqualTo value2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncoding3() {
|
||||
qrCode3.toEncodedString() shouldBeEqualTo value3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry1() {
|
||||
qrCode1.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry2() {
|
||||
qrCode2.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry3() {
|
||||
qrCode3.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase1() {
|
||||
val url = qrCode1.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldBeEqualTo 0
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase2() {
|
||||
val url = qrCode2.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldBeEqualTo 1
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase3() {
|
||||
val url = qrCode3.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldBeEqualTo 2
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongTransactionId() {
|
||||
// Size on two bytes (2_000 = 0x07D0)
|
||||
val longTransactionId = "PatternId_".repeat(200)
|
||||
|
||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
||||
|
||||
val result = qrCode.toEncodedString()
|
||||
val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId")
|
||||
|
||||
result shouldBeEqualTo expected
|
||||
|
||||
// Reverse operation
|
||||
expected.toQrCodeData() shouldBeEqualTo qrCode
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnyTransactionId() {
|
||||
for (qty in 0 until 0x1FFF step 200) {
|
||||
val longTransactionId = "a".repeat(qty)
|
||||
|
||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
||||
|
||||
// Symmetric operation
|
||||
qrCode.toEncodedString().toQrCodeData() shouldBeEqualTo qrCode
|
||||
}
|
||||
}
|
||||
|
||||
// Error cases
|
||||
@Test
|
||||
fun testErrorHeader() {
|
||||
value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX", "").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorVersion() {
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorSecretTooShort() {
|
||||
value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorNoTransactionNoKeyNoSecret() {
|
||||
// But keep transaction length
|
||||
"MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorNoKeyNoSecret() {
|
||||
"MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorTransactionLengthTooShort() {
|
||||
// In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch
|
||||
value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorTransactionLengthTooBig() {
|
||||
value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
private fun compareArray(actual: ByteArray, expected: ByteArray) {
|
||||
actual.size shouldBeEqualTo expected.size
|
||||
|
||||
for (i in actual.indices) {
|
||||
actual[i] shouldBeEqualTo expected[i]
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkHeader(byteArray: ByteArray) {
|
||||
// MATRIX
|
||||
byteArray[0] shouldBeEqualTo 'M'.code.toByte()
|
||||
byteArray[1] shouldBeEqualTo 'A'.code.toByte()
|
||||
byteArray[2] shouldBeEqualTo 'T'.code.toByte()
|
||||
byteArray[3] shouldBeEqualTo 'R'.code.toByte()
|
||||
byteArray[4] shouldBeEqualTo 'I'.code.toByte()
|
||||
byteArray[5] shouldBeEqualTo 'X'.code.toByte()
|
||||
|
||||
// Version
|
||||
byteArray[6] shouldBeEqualTo 2
|
||||
}
|
||||
|
||||
private fun checkSizeAndTransaction(byteArray: ByteArray) {
|
||||
// Size
|
||||
byteArray[8] shouldBeEqualTo 0
|
||||
byteArray[9] shouldBeEqualTo 13
|
||||
|
||||
// Transaction
|
||||
byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldBeEqualTo "MaTransaction"
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.realm.Realm
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
|
||||
class CryptoSanityMigrationTest {
|
||||
@get:Rule val configurationFactory = TestRealmConfigurationFactory()
|
||||
|
||||
lateinit var context: Context
|
||||
var realm: Realm? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = InstrumentationRegistry.getInstrumentation().context
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
realm?.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cryptoDatabaseShouldMigrateGracefully() {
|
||||
val realmName = "crypto_store_20.realm"
|
||||
|
||||
val migration = RealmCryptoStoreMigration(
|
||||
object : Clock {
|
||||
override fun epochMillis(): Long {
|
||||
return 0L
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val realmConfiguration = configurationFactory.createConfiguration(
|
||||
realmName,
|
||||
"7b9a21a8a311e85d75b069a343c23fc952fc3fec5e0c83ecfa13f24b787479c487c3ed587db3dd1f5805d52041fc0ac246516e94b27ffa699ff928622e621aca",
|
||||
RealmCryptoStoreModule(),
|
||||
migration.schemaVersion,
|
||||
migration
|
||||
)
|
||||
configurationFactory.copyRealmFromAssets(context, realmName, realmName)
|
||||
|
||||
realm = Realm.getInstance(realmConfiguration)
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
||||
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
|
||||
class BackupRecoveryKey(private val key: ByteArray) : IBackupRecoveryKey {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is BackupRecoveryKey) return false
|
||||
return this.toBase58() == other.toBase58()
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return key.contentHashCode()
|
||||
}
|
||||
|
||||
override fun toBase58() = computeRecoveryKey(key)
|
||||
|
||||
override fun toBase64() = key.toBase64NoPadding()
|
||||
|
||||
override fun decryptV1(ephemeralKey: String, mac: String, ciphertext: String): String = withOlmDecryption {
|
||||
it.setPrivateKey(key)
|
||||
it.decrypt(OlmPkMessage().apply {
|
||||
this.mEphemeralKey = ephemeralKey
|
||||
this.mCipherText = ciphertext
|
||||
this.mMac = mac
|
||||
})
|
||||
}
|
||||
|
||||
override fun megolmV1PublicKey() = v1pk
|
||||
|
||||
private val v1pk = object : IMegolmV1PublicKey {
|
||||
override val publicKey: String
|
||||
get() = withOlmDecryption {
|
||||
it.setPrivateKey(key)
|
||||
}
|
||||
override val privateKeySalt: String?
|
||||
get() = null // not use in kotlin sdk
|
||||
override val privateKeyIterations: Int?
|
||||
get() = null // not use in kotlin sdk
|
||||
override val backupAlgorithm: String
|
||||
get() = "" // not use in kotlin sdk
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
||||
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||
|
||||
object BackupUtils {
|
||||
|
||||
fun recoveryKeyFromBase58(base58: String): IBackupRecoveryKey? {
|
||||
return extractCurveKeyFromRecoveryKey(base58)?.let {
|
||||
BackupRecoveryKey(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun recoveryKeyFromPassphrase(passphrase: String): IBackupRecoveryKey? {
|
||||
return BackupRecoveryKey(generatePrivateKeyWithPassword(passphrase, null).privateKey)
|
||||
}
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationAcceptContent(
|
||||
@Json(name = "hash") override val hash: String?,
|
||||
@Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?,
|
||||
@Json(name = "message_authentication_code") override val messageAuthenticationCode: String?,
|
||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
|
||||
@Json(name = "commitment") override var commitment: String? = null
|
||||
) : VerificationInfoAccept {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : VerificationInfoAcceptFactory {
|
||||
|
||||
override fun create(
|
||||
tid: String,
|
||||
keyAgreementProtocol: String,
|
||||
hash: String,
|
||||
commitment: String,
|
||||
messageAuthenticationCode: String,
|
||||
shortAuthenticationStrings: List<String>
|
||||
): VerificationInfoAccept {
|
||||
return MessageVerificationAcceptContent(
|
||||
hash,
|
||||
keyAgreementProtocol,
|
||||
messageAuthenticationCode,
|
||||
shortAuthenticationStrings,
|
||||
RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
tid
|
||||
),
|
||||
commitment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessageVerificationCancelContent(
|
||||
@Json(name = "code") override val code: String? = null,
|
||||
@Json(name = "reason") override val reason: String? = null,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfoCancel {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object {
|
||||
fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent {
|
||||
return MessageVerificationCancelContent(
|
||||
reason.value,
|
||||
reason.humanReadable,
|
||||
RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
transactionId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationDoneContent(
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfo<ValidVerificationDone> {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent(): Content? = toContent()
|
||||
|
||||
override fun asValidObject(): ValidVerificationDone? {
|
||||
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
||||
|
||||
return ValidVerificationDone(
|
||||
validTransactionId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ValidVerificationDone(
|
||||
val transactionId: String
|
||||
)
|
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationKeyContent(
|
||||
/**
|
||||
* The device’s ephemeral public key, as an unpadded base64 string.
|
||||
*/
|
||||
@Json(name = "key") override val key: String? = null,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfoKey {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : VerificationInfoKeyFactory {
|
||||
|
||||
override fun create(tid: String, pubKey: String): VerificationInfoKey {
|
||||
return MessageVerificationKeyContent(
|
||||
pubKey,
|
||||
RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
tid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationMacContent(
|
||||
@Json(name = "mac") override val mac: Map<String, String>? = null,
|
||||
@Json(name = "keys") override val keys: String? = null,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfoMac {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : VerificationInfoMacFactory {
|
||||
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
|
||||
return MessageVerificationMacContent(
|
||||
mac,
|
||||
keys,
|
||||
RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
tid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.MessageVerificationReadyFactory
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationReadyContent(
|
||||
@Json(name = "from_device") override val fromDevice: String? = null,
|
||||
@Json(name = "methods") override val methods: List<String>? = null,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||
) : VerificationInfoReady {
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
|
||||
companion object : MessageVerificationReadyFactory {
|
||||
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
|
||||
return MessageVerificationReadyContent(
|
||||
fromDevice = fromDevice,
|
||||
methods = methods,
|
||||
relatesTo = RelationDefaultContent(
|
||||
RelationType.REFERENCE,
|
||||
tid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessageVerificationRequestContent(
|
||||
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
|
||||
@Json(name = "body") override val body: String,
|
||||
@Json(name = "from_device") override val fromDevice: String?,
|
||||
@Json(name = "methods") override val methods: List<String>,
|
||||
@Json(name = "to") val toUserId: String,
|
||||
@Json(name = "timestamp") override val timestamp: Long?,
|
||||
@Json(name = "format") val format: String? = null,
|
||||
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
// Not parsed, but set after, using the eventId
|
||||
override val transactionId: String? = null
|
||||
) : MessageContent, VerificationInfoRequest {
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.api.session.room.model.message
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class MessageVerificationStartContent(
|
||||
@Json(name = "from_device") override val fromDevice: String?,
|
||||
@Json(name = "hashes") override val hashes: List<String>?,
|
||||
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>?,
|
||||
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
|
||||
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
|
||||
@Json(name = "method") override val method: String?,
|
||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
|
||||
@Json(name = "secret") override val sharedSecret: String?
|
||||
) : VerificationInfoStart {
|
||||
|
||||
override fun toCanonicalJson(): String {
|
||||
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
|
||||
}
|
||||
|
||||
override val transactionId: String?
|
||||
get() = relatesTo?.eventId
|
||||
|
||||
override fun toEventContent() = toContent()
|
||||
}
|
@@ -1,266 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
||||
import org.matrix.android.sdk.internal.database.RealmKeysUtils
|
||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
||||
import org.matrix.android.sdk.internal.di.UserMd5
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.cache.ClearCacheTask
|
||||
import org.matrix.android.sdk.internal.session.cache.RealmClearCacheTask
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
|
||||
@Module
|
||||
internal abstract class CryptoModule {
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(
|
||||
@SessionFilesDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmKeysUtils: RealmKeysUtils,
|
||||
realmCryptoStoreMigration: RealmCryptoStoreMigration
|
||||
): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
||||
}
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.allowWritesOnUiThread(true)
|
||||
.schemaVersion(realmCryptoStoreMigration.schemaVersion)
|
||||
.migration(realmCryptoStoreMigration)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionScope
|
||||
fun providesCryptoCoroutineScope(coroutineDispatchers: MatrixCoroutineDispatchers): CoroutineScope {
|
||||
return CoroutineScope(SupervisorJob() + coroutineDispatchers.crypto)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask {
|
||||
return RealmClearCacheTask(realmConfiguration)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionScope
|
||||
fun providesCryptoAPI(retrofit: Retrofit): CryptoApi {
|
||||
return retrofit.create(CryptoApi::class.java)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionScope
|
||||
fun providesRoomKeysAPI(retrofit: Retrofit): RoomKeysApi {
|
||||
return retrofit.create(RoomKeysApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService
|
||||
|
||||
@Binds
|
||||
abstract fun bindKeysBackupService(service: DefaultKeysBackupService): KeysBackupService
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService
|
||||
|
||||
@Binds
|
||||
abstract fun bindVerificationService(service: DefaultVerificationService): VerificationService
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitalizeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
}
|
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DecryptRoomEventUseCase @Inject constructor(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(event: Event, requestKeysOnFail: Boolean = true): MXEventDecryptionResult {
|
||||
if (event.roomId.isNullOrBlank()) {
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
|
||||
if (encryptedEventContent.senderKey.isNullOrBlank() ||
|
||||
encryptedEventContent.sessionId.isNullOrBlank() ||
|
||||
encryptedEventContent.ciphertext.isNullOrBlank()) {
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
|
||||
try {
|
||||
val olmDecryptionResult = olmDevice.decryptGroupMessage(
|
||||
encryptedEventContent.ciphertext,
|
||||
event.roomId,
|
||||
"",
|
||||
eventId = event.eventId.orEmpty(),
|
||||
encryptedEventContent.sessionId,
|
||||
encryptedEventContent.senderKey
|
||||
)
|
||||
if (olmDecryptionResult.payload != null) {
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = olmDecryptionResult.payload,
|
||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
||||
.orEmpty(),
|
||||
messageVerificationState = olmDecryptionResult.verificationState
|
||||
)
|
||||
} else {
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable is MXCryptoError.OlmError) {
|
||||
// TODO Check the value of .message
|
||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
||||
// Check if partially withheld
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
withHeldInfo.code?.value ?: "",
|
||||
withHeldInfo.reason
|
||||
)
|
||||
}
|
||||
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||
"UNKNOWN_MESSAGE_INDEX",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
||||
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
|
||||
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.OLM,
|
||||
reason,
|
||||
detailedReason
|
||||
)
|
||||
}
|
||||
if (throwable is MXCryptoError.Base) {
|
||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
// Check if it was withheld by sender to enrich error code
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
}
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
withHeldInfo.code?.value ?: "",
|
||||
withHeldInfo.reason
|
||||
)
|
||||
}
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestKeysForEvent(event: Event) {
|
||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||
}
|
||||
|
||||
suspend fun decryptAndSaveResult(event: Event) {
|
||||
tryOrNull(message = "Unable to decrypt the event") {
|
||||
invoke(event)
|
||||
}
|
||||
?.let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
verificationState = result.messageVerificationState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,603 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.extensions.measureMetric
|
||||
import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.UserDataToStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
||||
import org.matrix.android.sdk.internal.util.logLimit
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// Legacy name: MXDeviceList
|
||||
@SessionScope
|
||||
internal class DeviceListManager @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val syncTokenStore: SyncTokenStore,
|
||||
private val credentials: Credentials,
|
||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val clock: Clock,
|
||||
matrixConfiguration: MatrixConfiguration
|
||||
) {
|
||||
|
||||
private val metricPlugins = matrixConfiguration.metricPlugins
|
||||
|
||||
interface UserDevicesUpdateListener {
|
||||
fun onUsersDeviceUpdate(userIds: List<String>)
|
||||
}
|
||||
|
||||
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
|
||||
|
||||
fun addListener(listener: UserDevicesUpdateListener) {
|
||||
synchronized(deviceChangeListeners) {
|
||||
deviceChangeListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeListener(listener: UserDevicesUpdateListener) {
|
||||
synchronized(deviceChangeListeners) {
|
||||
deviceChangeListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchDeviceChange(users: List<String>) {
|
||||
synchronized(deviceChangeListeners) {
|
||||
deviceChangeListeners.forEach {
|
||||
try {
|
||||
it.onUsersDeviceUpdate(users)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to dispatch device change")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HS not ready for retry
|
||||
private val notReadyToRetryHS = mutableSetOf<String>()
|
||||
|
||||
private val cryptoCoroutineContext = coroutineDispatchers.crypto
|
||||
|
||||
// Reset in progress status in case of restart
|
||||
suspend fun recover() {
|
||||
withContext(cryptoCoroutineContext) {
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
for ((userId, status) in deviceTrackingStatuses) {
|
||||
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
if (isUpdated) {
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the key downloads should be tried.
|
||||
*
|
||||
* @param userId the userId
|
||||
* @return true if the keys download can be retrieved
|
||||
*/
|
||||
private fun canRetryKeysDownload(userId: String): Boolean {
|
||||
var res = false
|
||||
|
||||
if (':' in userId) {
|
||||
try {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the unavailable server lists.
|
||||
*/
|
||||
private fun clearUnavailableServersList() {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun onRoomMembersLoadedFor(roomId: String) {
|
||||
cryptoCoroutineScope.launch(cryptoCoroutineContext) {
|
||||
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
|
||||
// It's OK to track also device for invited users
|
||||
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
|
||||
startTrackingDeviceList(userIds)
|
||||
refreshOutdatedDeviceLists()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the cached device list for the given user outdated
|
||||
* flag the given user for device-list tracking, if they are not already.
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
*/
|
||||
fun startTrackingDeviceList(userIds: List<String>) {
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
for (userId in userIds) {
|
||||
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
|
||||
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the devices list statuses.
|
||||
*
|
||||
* @param changed the user ids list which have new devices
|
||||
* @param left the user ids list which left a room
|
||||
*/
|
||||
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
|
||||
Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
if (changed.isNotEmpty() || left.isNotEmpty()) {
|
||||
clearUnavailableServersList()
|
||||
}
|
||||
|
||||
for (userId in changed) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
for (userId in left) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will flag each user whose devices we are tracking as in need of an update.
|
||||
*/
|
||||
fun invalidateAllDeviceLists() {
|
||||
handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys download failed.
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
*/
|
||||
private fun onKeysDownloadFailed(userIds: List<String>) {
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys download succeeded.
|
||||
*
|
||||
* @param userIds the userIds list
|
||||
* @param failures the failure map.
|
||||
*/
|
||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
if (failures != null) {
|
||||
for ((k, value) in failures) {
|
||||
val statusCode = when (val status = value["status"]) {
|
||||
is Double -> status.toInt()
|
||||
is Int -> status.toInt()
|
||||
else -> 0
|
||||
}
|
||||
if (statusCode == 503) {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.add(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
if (null == devices) {
|
||||
if (canRetryKeysDownload(userId)) {
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
Timber.e("failed to retry the devices of $userId : retry later")
|
||||
} else {
|
||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER
|
||||
Timber.e("failed to retry the devices of $userId : the HS is not available")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
||||
// we didn't get any new invalidations since this download started:
|
||||
// this user's device list is now up to date.
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE
|
||||
Timber.v("Device list for $userId now up to date")
|
||||
}
|
||||
// And the response result
|
||||
usersDevicesInfoMap.setObjects(userId, devices)
|
||||
}
|
||||
}
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
|
||||
dispatchDeviceChange(userIds)
|
||||
return usersDevicesInfoMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the device keys for a list of users and stores the keys in the MXStore.
|
||||
* It must be called in getEncryptingThreadHandler() thread.
|
||||
*
|
||||
* @param userIds The users to fetch.
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
// Map from userId -> deviceId -> MXDeviceInfo
|
||||
val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
|
||||
// List of user ids we need to download keys for
|
||||
val downloadUsers = ArrayList<String>()
|
||||
if (null != userIds) {
|
||||
if (forceDownload) {
|
||||
downloadUsers.addAll(userIds)
|
||||
} else {
|
||||
for (userId in userIds) {
|
||||
val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED)
|
||||
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
|
||||
// not yet retrieved
|
||||
if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) {
|
||||
downloadUsers.add(userId)
|
||||
} else {
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
// should always be true
|
||||
if (devices != null) {
|
||||
stored.setObjects(userId, devices)
|
||||
} else {
|
||||
downloadUsers.add(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (downloadUsers.isEmpty()) {
|
||||
Timber.v("## CRYPTO | downloadKeys() : no new user device")
|
||||
stored
|
||||
} else {
|
||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
||||
val t0 = clock.epochMillis()
|
||||
try {
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
|
||||
if (forceDownload) {
|
||||
throw failure
|
||||
} else {
|
||||
stored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the devices keys for a set of users.
|
||||
*
|
||||
* @param downloadUsers the user ids list
|
||||
*/
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
|
||||
// get the user ids which did not already trigger a keys download
|
||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
||||
if (filteredUsers.isEmpty()) {
|
||||
// trigger nothing
|
||||
return MXUsersDevicesMap()
|
||||
}
|
||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
||||
val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
|
||||
|
||||
val response: KeysQueryResponse
|
||||
relevantPlugins.measureMetric {
|
||||
response = try {
|
||||
downloadKeysForUsersTask.execute(params)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
||||
if (throwable is CancellationException) {
|
||||
// the crypto module is getting closed, so we cannot access the DB anymore
|
||||
Timber.w("The crypto module is closed, ignoring this error")
|
||||
} else {
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
}
|
||||
throw throwable
|
||||
}
|
||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
}
|
||||
|
||||
val userDataToStore = UserDataToStore()
|
||||
|
||||
for (userId in filteredUsers) {
|
||||
// al devices =
|
||||
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
|
||||
|
||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
|
||||
if (!models.isNullOrEmpty()) {
|
||||
val workingCopy = models.toMutableMap()
|
||||
for ((deviceId, deviceInfo) in models) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
|
||||
deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
workingCopy.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
workingCopy[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
|
||||
}
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
userDataToStore.userDevices[userId] = workingCopy
|
||||
}
|
||||
|
||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
userDataToStore.userIdentities[userId] = UserIdentity(
|
||||
masterKey = masterKey,
|
||||
selfSigningKey = selfSigningKey,
|
||||
userSigningKey = userSigningKey
|
||||
)
|
||||
}
|
||||
|
||||
cryptoStore.storeData(userDataToStore)
|
||||
|
||||
// Update devices trust for these users
|
||||
// dispatchDeviceChange(downloadUsers)
|
||||
|
||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate device keys.
|
||||
* This method must called on getEncryptingThreadHandler() thread.
|
||||
*
|
||||
* @param deviceKeys the device keys to validate.
|
||||
* @param userId the id of the user of the device.
|
||||
* @param deviceId the id of the device.
|
||||
* @param previouslyStoredDeviceKeys the device keys we received before for this device
|
||||
* @return true if succeeds
|
||||
*/
|
||||
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
|
||||
if (null == deviceKeys) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
if (null == deviceKeys.keys) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
if (null == deviceKeys.signatures) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the user_id and device_id in the received deviceKeys are correct
|
||||
if (deviceKeys.userId != userId) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
if (deviceKeys.deviceId != deviceId) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
val signKeyId = "ed25519:" + deviceKeys.deviceId
|
||||
val signKey = deviceKeys.keys[signKeyId]
|
||||
|
||||
if (null == signKey) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
|
||||
return false
|
||||
}
|
||||
|
||||
val signatureMap = deviceKeys.signatures[userId]
|
||||
|
||||
if (null == signatureMap) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
|
||||
return false
|
||||
}
|
||||
|
||||
val signature = signatureMap[signKeyId]
|
||||
|
||||
if (null == signature) {
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
|
||||
return false
|
||||
}
|
||||
|
||||
var isVerified = false
|
||||
var errorMessage: String? = null
|
||||
|
||||
try {
|
||||
olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature)
|
||||
isVerified = true
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
}
|
||||
|
||||
if (!isVerified) {
|
||||
Timber.e(
|
||||
"## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" +
|
||||
deviceKeys.deviceId + " with error " + errorMessage
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
|
||||
// This should only happen if the list has been MITMed; we are
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
Timber.e(
|
||||
"## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" +
|
||||
deviceKeys.deviceId + " has changed : " +
|
||||
previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey
|
||||
)
|
||||
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
||||
Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Start device queries for any users who sent us an m.new_device recently
|
||||
* This method must be called on getEncryptingThreadHandler() thread.
|
||||
*/
|
||||
suspend fun refreshOutdatedDeviceLists() {
|
||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
||||
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
|
||||
}
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// update the statuses
|
||||
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
|
||||
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
runCatching {
|
||||
doKeyDownloadForUsers(users)
|
||||
}.fold(
|
||||
{
|
||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
|
||||
},
|
||||
{
|
||||
Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* State transition diagram for DeviceList.deviceTrackingStatus.
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* stopTrackingDeviceList V
|
||||
* +---------------------> NOT_TRACKED
|
||||
* | |
|
||||
* +<--------------------+ | startTrackingDeviceList
|
||||
* | | V
|
||||
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
|
||||
* | | ^ | | |
|
||||
* | | restart download | | start download | | invalidateUserDeviceList
|
||||
* | | client failed | | | |
|
||||
* | | | V | |
|
||||
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
|
||||
* | | | |
|
||||
* +<-------------------+ | download successful |
|
||||
* ^ V |
|
||||
* +----------------------- UP_TO_DATE ------------------------+
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
const val TRACKING_STATUS_NOT_TRACKED = -1
|
||||
const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
|
||||
const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
|
||||
const val TRACKING_STATUS_UP_TO_DATE = 3
|
||||
const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
|
||||
}
|
||||
}
|
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
||||
|
||||
private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class EventDecryptor @Inject constructor(
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val clock: Clock,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Rate limit unwedge attempt, should we persist that?
|
||||
*/
|
||||
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
|
||||
|
||||
data class WedgedDeviceInfo(
|
||||
val userId: String,
|
||||
val senderKey: String?
|
||||
)
|
||||
|
||||
private val wedgedMutex = Mutex()
|
||||
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
|
||||
|
||||
/**
|
||||
* Decrypt an event.
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event and save the result in the given event.
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
*/
|
||||
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
|
||||
// event is not encrypted or already decrypted
|
||||
if (event.getClearType() != EventType.ENCRYPTED) return
|
||||
|
||||
tryOrNull(message = "decryptEventAndSaveResult | Unable to decrypt the event") {
|
||||
decryptEvent(event, timeline)
|
||||
}
|
||||
?.let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
verificationState = result.messageVerificationState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event asynchronously.
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @param callback the callback to return data or null
|
||||
*/
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
// is it needed to do that on the crypto scope??
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event.
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else if (event.isRedacted()) {
|
||||
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = mapOf(
|
||||
"room_id" to event.roomId.orEmpty(),
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to emptyMap<String, Any>(),
|
||||
"unsigned" to event.unsignedData.toContent()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
try {
|
||||
return alg.decryptEvent(event, timeline)
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base &&
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
||||
}
|
||||
}
|
||||
}
|
||||
throw mxCryptoError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
|
||||
wedgedMutex.withLock {
|
||||
val info = WedgedDeviceInfo(senderId, senderKey)
|
||||
if (!wedgedDevices.contains(info)) {
|
||||
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
|
||||
wedgedDevices.add(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// coroutineDispatchers.crypto scope
|
||||
suspend fun unwedgeDevicesIfNeeded() {
|
||||
// handle wedged devices
|
||||
// Some olm decryption have failed and some device are wedged
|
||||
// we should force start a new session for those
|
||||
Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
|
||||
// get the one that should be retried according to rate limit
|
||||
val now = clock.epochMillis()
|
||||
val toUnwedge = wedgedMutex.withLock {
|
||||
wedgedDevices.filter {
|
||||
val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
|
||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
|
||||
return@filter false
|
||||
}
|
||||
// let's already mark that we tried now
|
||||
lastNewSessionForcedDates[it] = now
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
if (toUnwedge.isEmpty()) {
|
||||
Timber.tag(loggerTag.value).v("Nothing to unwedge")
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
|
||||
|
||||
toUnwedge
|
||||
.chunked(100) // safer to chunk if we ever have lots of wedged devices
|
||||
.forEach { wedgedList ->
|
||||
val groupedByUserId = wedgedList.groupBy { it.userId }
|
||||
// lets download keys if needed
|
||||
withContext(coroutineDispatchers.io) {
|
||||
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
|
||||
}
|
||||
|
||||
// find the matching devices
|
||||
groupedByUserId
|
||||
.map { groupedByUser ->
|
||||
val userId = groupedByUser.key
|
||||
val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
|
||||
val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
||||
userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
|
||||
knownDevices.firstOrNull { it.identityKey() == senderKey }
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
.let { deviceList ->
|
||||
try {
|
||||
// force creating new outbound session and mark them as most recent to
|
||||
// be used for next encryption (dummy)
|
||||
val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
|
||||
Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
|
||||
|
||||
// Now send a dummy message on that session so the other side knows about it.
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.DUMMY
|
||||
)
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sessionToUse.map.values
|
||||
.flatMap { it.values }
|
||||
.map { it.deviceInfo }
|
||||
.forEach { deviceInfo ->
|
||||
Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
|
||||
}
|
||||
|
||||
// now let's send that
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
||||
}
|
||||
|
||||
deviceList.values.flatten().forEach { deviceInfo ->
|
||||
wedgedMutex.withLock {
|
||||
wedgedDevices.removeAll {
|
||||
it.senderKey == deviceInfo.identityKey() &&
|
||||
it.userId == deviceInfo.userId
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
|
||||
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.LruCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal data class InboundGroupSessionHolder(
|
||||
val wrapper: MXInboundMegolmSessionWrapper,
|
||||
val mutex: Mutex = Mutex()
|
||||
)
|
||||
|
||||
private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
|
||||
|
||||
/**
|
||||
* Allows to cache and batch store operations on inbound group session store.
|
||||
* Because it is used in the decrypt flow, that can be called quite rapidly
|
||||
*/
|
||||
internal class InboundGroupSessionStore @Inject constructor(
|
||||
private val store: IMXCryptoStore,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
|
||||
private data class CacheKey(
|
||||
val sessionId: String,
|
||||
val senderKey: String
|
||||
)
|
||||
|
||||
private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
|
||||
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
|
||||
if (oldValue != null) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
||||
// store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
|
||||
oldValue.wrapper.session.releaseSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
sessionCache.evictAll()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
|
||||
val known = sessionCache[CacheKey(sessionId, senderKey)]
|
||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}")
|
||||
return known
|
||||
?: store.getInboundGroupSession(sessionId, senderKey)?.also {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
|
||||
sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
|
||||
}?.let {
|
||||
InboundGroupSessionHolder(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
||||
store.removeInboundGroupSession(sessionId, senderKey)
|
||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||
|
||||
// release removed session
|
||||
old.wrapper.session.releaseSession()
|
||||
|
||||
internalStoreGroupSession(new, sessionId, senderKey)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
||||
|
||||
store.storeInboundGroupSessions(
|
||||
listOf(
|
||||
old.wrapper.copy(
|
||||
sessionData = old.wrapper.sessionData.copy(trusted = true)
|
||||
)
|
||||
)
|
||||
)
|
||||
// will release it :/
|
||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
internalStoreGroupSession(holder, sessionId, senderKey)
|
||||
}
|
||||
|
||||
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
|
||||
|
||||
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
||||
// first time seen, put it in memory cache while waiting for batch insert
|
||||
// If it's already known, no need to update cache it's already there
|
||||
sessionCache.put(CacheKey(sessionId, senderKey), holder)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
store.storeInboundGroupSessions(listOf(holder.wrapper))
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,465 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class IncomingKeyRequestManager @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val cryptoConfig: MXCryptoConfig,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
val sequencer = SemaphoreCoroutineSequencer()
|
||||
|
||||
private val incomingRequestBuffer = mutableListOf<ValidMegolmRequestBody>()
|
||||
|
||||
// the listeners
|
||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
||||
|
||||
enum class MegolmRequestAction {
|
||||
Request, Cancel
|
||||
}
|
||||
|
||||
data class ValidMegolmRequestBody(
|
||||
val requestId: String,
|
||||
val requestingUserId: String,
|
||||
val requestingDeviceId: String,
|
||||
val roomId: String,
|
||||
val senderKey: String,
|
||||
val sessionId: String,
|
||||
val action: MegolmRequestAction
|
||||
) {
|
||||
fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId"
|
||||
}
|
||||
|
||||
private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? {
|
||||
val deviceId = requestingDeviceId ?: return null
|
||||
val body = body ?: return null
|
||||
val roomId = body.roomId ?: return null
|
||||
val sessionId = body.sessionId ?: return null
|
||||
val senderKey = body.senderKey ?: return null
|
||||
val requestId = this.requestId ?: return null
|
||||
if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
||||
val action = when (this.action) {
|
||||
"request" -> MegolmRequestAction.Request
|
||||
"request_cancellation" -> MegolmRequestAction.Cancel
|
||||
else -> null
|
||||
} ?: return null
|
||||
return ValidMegolmRequestBody(
|
||||
requestId = requestId,
|
||||
requestingUserId = senderId,
|
||||
requestingDeviceId = deviceId,
|
||||
roomId = roomId,
|
||||
senderKey = senderKey,
|
||||
sessionId = sessionId,
|
||||
action = action
|
||||
)
|
||||
}
|
||||
|
||||
fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) {
|
||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}")
|
||||
return
|
||||
}
|
||||
outgoingRequestScope.launch {
|
||||
// It is important to handle requests in order
|
||||
sequencer.post {
|
||||
val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also {
|
||||
Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}")
|
||||
}
|
||||
|
||||
// is there already one like that?
|
||||
val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest }
|
||||
if (existing == null) {
|
||||
when (validMegolmRequest.action) {
|
||||
MegolmRequestAction.Request -> {
|
||||
// just add to the buffer
|
||||
incomingRequestBuffer.add(validMegolmRequest)
|
||||
}
|
||||
MegolmRequestAction.Cancel -> {
|
||||
// ignore, we can't cancel as it's not known (probably already processed)
|
||||
// still notify app layer if it was passed up previously
|
||||
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
|
||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.toList()
|
||||
}
|
||||
listenersCopy.onEach {
|
||||
tryOrNull {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
it.onRequestCancelled(iReq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (validMegolmRequest.action) {
|
||||
MegolmRequestAction.Request -> {
|
||||
// it's already in buffer, nop keep existing
|
||||
}
|
||||
MegolmRequestAction.Cancel -> {
|
||||
// discard the request in buffer
|
||||
incomingRequestBuffer.remove(existing)
|
||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.toList()
|
||||
}
|
||||
listenersCopy.onEach {
|
||||
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
|
||||
withContext(coroutineDispatchers.main) {
|
||||
tryOrNull { it.onRequestCancelled(iReq) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processIncomingRequests() {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
measureTimeMillis {
|
||||
Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process")
|
||||
incomingRequestBuffer.forEach {
|
||||
// should not happen, we only store requests
|
||||
if (it.action != MegolmRequestAction.Request) return@forEach
|
||||
try {
|
||||
handleIncomingRequest(it)
|
||||
} catch (failure: Throwable) {
|
||||
// ignore and continue, should not happen
|
||||
Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it")
|
||||
}
|
||||
}
|
||||
incomingRequestBuffer.clear()
|
||||
}.let { duration ->
|
||||
Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) {
|
||||
// We don't want to download keys, if we don't know the device yet we won't share any how?
|
||||
val requestingDevice =
|
||||
cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId)
|
||||
?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}")
|
||||
}
|
||||
|
||||
cryptoStore.saveIncomingKeyRequestAuditTrail(
|
||||
request.requestId,
|
||||
request.roomId,
|
||||
request.sessionId,
|
||||
request.senderKey,
|
||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
request.requestingUserId,
|
||||
request.requestingDeviceId
|
||||
)
|
||||
|
||||
val roomAlgorithm = // withContext(coroutineDispatchers.crypto) {
|
||||
cryptoStore.getRoomAlgorithm(request.roomId)
|
||||
// }
|
||||
if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
// strange we received a request for a room that is not encrypted
|
||||
// maybe a broken state?
|
||||
Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}")
|
||||
return
|
||||
}
|
||||
|
||||
// Is it for one of our sessions?
|
||||
if (request.requestingUserId == credentials.userId) {
|
||||
Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}")
|
||||
|
||||
if (request.requestingDeviceId == credentials.deviceId) {
|
||||
// ignore it's a remote echo
|
||||
return
|
||||
}
|
||||
// If it's verified we share from the early index we know
|
||||
// if not we check if it was originaly shared or not
|
||||
if (requestingDevice.isVerified) {
|
||||
// we share from the earliest known chain index
|
||||
shareMegolmKey(request, requestingDevice, null)
|
||||
} else {
|
||||
shareIfItWasPreviouslyShared(request, requestingDevice)
|
||||
}
|
||||
} else {
|
||||
if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
|
||||
Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}")
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}")
|
||||
if (requestingDevice.isBlocked) {
|
||||
// it's blocked, so send a withheld code
|
||||
sendWithheldForRequest(request, WithHeldCode.BLACKLISTED)
|
||||
} else {
|
||||
shareIfItWasPreviouslyShared(request, requestingDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) {
|
||||
// we don't reshare unless it was previously shared with
|
||||
val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) {
|
||||
cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice)
|
||||
}
|
||||
if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) {
|
||||
// we share from the index it was previously shared with
|
||||
shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong())
|
||||
} else {
|
||||
val isOwnDevice = requestingDevice.userId == credentials.userId
|
||||
sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED)
|
||||
// if it's our device we could delegate to the app layer to decide
|
||||
if (isOwnDevice) {
|
||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.toList()
|
||||
}
|
||||
val iReq = IncomingRoomKeyRequest(
|
||||
userId = requestingDevice.userId,
|
||||
deviceId = requestingDevice.deviceId,
|
||||
requestId = request.requestId,
|
||||
requestBody = RoomKeyRequestBody(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
senderKey = request.senderKey,
|
||||
sessionId = request.sessionId,
|
||||
roomId = request.roomId
|
||||
),
|
||||
localCreationTimestamp = clock.epochMillis()
|
||||
)
|
||||
listenersCopy.onEach {
|
||||
withContext(coroutineDispatchers.main) {
|
||||
tryOrNull { it.onRoomKeyRequest(iReq) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Send withheld $code for req: ${request.shortDbgString()}")
|
||||
val withHeldContent = RoomKeyWithHeldContent(
|
||||
roomId = request.roomId,
|
||||
senderKey = request.senderKey,
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
sessionId = request.sessionId,
|
||||
codeString = code.value,
|
||||
fromDevice = credentials.deviceId
|
||||
)
|
||||
|
||||
val params = SendToDeviceTask.Params(
|
||||
EventType.ROOM_KEY_WITHHELD.stable,
|
||||
MXUsersDevicesMap<Any>().apply {
|
||||
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
|
||||
}
|
||||
)
|
||||
try {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.execute(params)
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("Send withheld $code req: ${request.shortDbgString()}")
|
||||
}
|
||||
|
||||
cryptoStore.saveWithheldAuditTrail(
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
senderKey = request.senderKey,
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
code = code,
|
||||
userId = request.requestingUserId,
|
||||
deviceId = request.requestingDeviceId
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
// Ignore it's not that important?
|
||||
// do we want to fallback to a worker?
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
request.requestId ?: return
|
||||
request.deviceId ?: return
|
||||
request.userId ?: return
|
||||
request.requestBody?.roomId ?: return
|
||||
request.requestBody.senderKey ?: return
|
||||
request.requestBody.sessionId ?: return
|
||||
val validReq = ValidMegolmRequestBody(
|
||||
requestId = request.requestId,
|
||||
requestingDeviceId = request.deviceId,
|
||||
requestingUserId = request.userId,
|
||||
roomId = request.requestBody.roomId,
|
||||
senderKey = request.requestBody.senderKey,
|
||||
sessionId = request.requestBody.sessionId,
|
||||
action = MegolmRequestAction.Request
|
||||
)
|
||||
val requestingDevice =
|
||||
cryptoStore.getUserDevice(request.userId, request.deviceId)
|
||||
?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}")
|
||||
}
|
||||
|
||||
shareMegolmKey(validReq, requestingDevice, null)
|
||||
}
|
||||
|
||||
private suspend fun shareMegolmKey(
|
||||
validRequest: ValidMegolmRequestBody,
|
||||
requestingDevice: CryptoDeviceInfo,
|
||||
chainIndex: Long?
|
||||
): Boolean {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}")
|
||||
|
||||
val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice))
|
||||
val usersDeviceMap = try {
|
||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Failed to establish olm session")
|
||||
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
|
||||
return false
|
||||
}
|
||||
|
||||
val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("reshareKey: no session with this device, probably because there were no one-time keys")
|
||||
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
|
||||
return false
|
||||
}
|
||||
val sessionHolder = try {
|
||||
olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}")
|
||||
// It's unavailable
|
||||
sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE)
|
||||
return false
|
||||
}
|
||||
|
||||
val export = sessionHolder.mutex.withLock {
|
||||
sessionHolder.wrapper.exportKeys(chainIndex)
|
||||
} ?: return false.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}")
|
||||
}
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
||||
"content" to export
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload)
|
||||
Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
return try {
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
||||
cryptoStore.saveForwardKeyAuditTrail(
|
||||
validRequest.roomId,
|
||||
validRequest.sessionId,
|
||||
validRequest.senderKey,
|
||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
requestingDevice.userId,
|
||||
requestingDevice.deviceId,
|
||||
chainIndex
|
||||
)
|
||||
true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
try {
|
||||
outgoingRequestScope.cancel("User Terminate")
|
||||
incomingRequestBuffer.clear()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class MyDeviceInfoHolder @Inject constructor(
|
||||
// The credentials,
|
||||
credentials: Credentials,
|
||||
// the crypto store
|
||||
cryptoStore: IMXCryptoStore,
|
||||
// Olm device
|
||||
olmDevice: MXOlmDevice
|
||||
) {
|
||||
// Our device keys
|
||||
/**
|
||||
* my device info.
|
||||
*/
|
||||
val myDevice: CryptoDeviceInfo
|
||||
|
||||
init {
|
||||
|
||||
val keys = HashMap<String, String>()
|
||||
|
||||
// TODO it's a bit strange, why not load from DB?
|
||||
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
||||
}
|
||||
|
||||
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
|
||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
||||
}
|
||||
|
||||
// myDevice.keys = keys
|
||||
//
|
||||
// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
||||
|
||||
// TODO hwo to really check cross signed status?
|
||||
//
|
||||
val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false
|
||||
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
|
||||
|
||||
myDevice = CryptoDeviceInfo(
|
||||
credentials.deviceId,
|
||||
credentials.userId,
|
||||
keys = keys,
|
||||
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
|
||||
trustLevel = DeviceTrustLevel(crossSigned, true)
|
||||
)
|
||||
|
||||
// Add our own deviceinfo to the store
|
||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
||||
|
||||
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
|
||||
|
||||
myDevices[myDevice.deviceId] = myDevice
|
||||
|
||||
cryptoStore.storeUserDevices(credentials.userId, myDevices)
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ObjectSigner @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice
|
||||
) {
|
||||
|
||||
/**
|
||||
* Sign Object.
|
||||
*
|
||||
* Example:
|
||||
* <pre>
|
||||
* {
|
||||
* "[MY_USER_ID]": {
|
||||
* "ed25519:[MY_DEVICE_ID]": "sign(str)"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param strToSign the String to sign and to include in the Map
|
||||
* @return a Map (see example)
|
||||
*/
|
||||
fun signObject(strToSign: String): Map<String, Map<String, String>> {
|
||||
val result = HashMap<String, Map<String, String>>()
|
||||
|
||||
val content = HashMap<String, String>()
|
||||
|
||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
|
||||
?: "" // null reported by rageshake if happens during logout
|
||||
|
||||
result[credentials.userId] = content
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.olm.OlmSession
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
|
||||
|
||||
/**
|
||||
* Keep the used olm session in memory and load them from the data layer when needed.
|
||||
* Access is synchronized for thread safety.
|
||||
*/
|
||||
internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
|
||||
/**
|
||||
* Map of device key to list of olm sessions (it is possible to have several active sessions with a device).
|
||||
*/
|
||||
private val olmSessions = HashMap<String, MutableList<OlmSessionWrapper>>()
|
||||
|
||||
/**
|
||||
* Store a session between our own device and another device.
|
||||
* This will be called after the session has been created but also every time it has been used
|
||||
* in order to persist the correct state for next run
|
||||
* @param olmSessionWrapper the end-to-end session.
|
||||
* @param deviceKey the public key of the other device.
|
||||
*/
|
||||
@Synchronized
|
||||
fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
|
||||
// This could be a newly created session or one that was just created
|
||||
// Anyhow we should persist ratchet state for future app lifecycle
|
||||
addNewSessionInCache(olmSessionWrapper, deviceKey)
|
||||
store.storeSession(olmSessionWrapper, deviceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the Olm Sessions we are sharing with the given device.
|
||||
*
|
||||
* @param deviceKey the public key of the other device.
|
||||
* @return A set of sessionId, or empty if device is not known
|
||||
*/
|
||||
@Synchronized
|
||||
fun getDeviceSessionIds(deviceKey: String): List<String> {
|
||||
// we need to get the persisted ids first
|
||||
val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
|
||||
.orEmpty()
|
||||
.toMutableList()
|
||||
// Do we have some in cache not yet persisted?
|
||||
olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
|
||||
getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
|
||||
if (!persistedKnownSessions.contains(cachedSessionId)) {
|
||||
// as it's in cache put in on top
|
||||
persistedKnownSessions.add(0, cachedSessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return persistedKnownSessions
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an end-to-end session between our own device and another
|
||||
* device.
|
||||
*
|
||||
* @param sessionId the session Id.
|
||||
* @param deviceKey the public key of the other device.
|
||||
* @return the session wrapper if found
|
||||
*/
|
||||
@Synchronized
|
||||
fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
||||
// get from cache or load and add to cache
|
||||
return internalGetSession(sessionId, deviceKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist.
|
||||
*
|
||||
* @param deviceKey the public key of the other device.
|
||||
* @return last used sessionId, or null if not found
|
||||
*/
|
||||
@Synchronized
|
||||
fun getLastUsedSessionId(deviceKey: String): String? {
|
||||
// We want to avoid to load in memory old session if possible
|
||||
val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
|
||||
var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
|
||||
// we should check if we have one in cache with a higher last message received?
|
||||
olmSessions[deviceKey].orEmpty().forEach { inCache ->
|
||||
if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
|
||||
candidate = inCache
|
||||
}
|
||||
}
|
||||
|
||||
return candidate?.olmSession?.sessionIdentifier()
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all sessions and clear cache.
|
||||
*/
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
olmSessions.entries.onEach { entry ->
|
||||
entry.value.onEach { it.olmSession.releaseSession() }
|
||||
}
|
||||
olmSessions.clear()
|
||||
}
|
||||
|
||||
private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
||||
return getSessionInCache(sessionId, deviceKey)
|
||||
?: // deserialize from store
|
||||
return store.getDeviceSession(sessionId, deviceKey)?.also {
|
||||
addNewSessionInCache(it, deviceKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
|
||||
return olmSessions[deviceKey]?.firstOrNull {
|
||||
getSafeSessionIdentifier(it.olmSession) == sessionId
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSafeSessionIdentifier(session: OlmSession): String? {
|
||||
return try {
|
||||
session.sessionIdentifier()
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
|
||||
val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
|
||||
olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
|
||||
val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
|
||||
it.add(session)
|
||||
// remove and release if was there but with different instance
|
||||
if (existing != null && existing.olmSession != session.olmSession) {
|
||||
// mm not sure when this could happen
|
||||
// anyhow we should remove and release the one known
|
||||
it.remove(existing)
|
||||
existing.olmSession.releaseSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import org.matrix.olm.OlmAccount
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
|
||||
// The spec recommend a 5mn delay, but due to federation
|
||||
// or server downtime we give it a bit more time (1 hour)
|
||||
private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
|
||||
|
||||
@SessionScope
|
||||
internal class OneTimeKeysUploader @Inject constructor(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val objectSigner: ObjectSigner,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val clock: Clock,
|
||||
context: Context
|
||||
) {
|
||||
// tell if there is a OTK check in progress
|
||||
private var oneTimeKeyCheckInProgress = false
|
||||
|
||||
// last OTK check timestamp
|
||||
private var lastOneTimeKeyCheck: Long = 0
|
||||
private var oneTimeKeyCount: Int? = null
|
||||
|
||||
// Simple storage to remember when was uploaded the last fallback key
|
||||
private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* Stores the current one_time_key count which will be handled later (in a call of
|
||||
* _onSyncCompleted). The count is e.g. coming from a /sync response.
|
||||
*
|
||||
* @param currentCount the new count
|
||||
*/
|
||||
fun updateOneTimeKeyCount(currentCount: Int) {
|
||||
oneTimeKeyCount = currentCount
|
||||
}
|
||||
|
||||
fun needsNewFallback() {
|
||||
if (olmDevice.generateFallbackKeyIfNeeded()) {
|
||||
// As we generated a new one, it's already forgetting one
|
||||
// so we can clear the last publish time
|
||||
// (in case the network calls fails after to avoid calling forgetKey)
|
||||
saveLastFallbackKeyPublishTime(0L)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the OTK must be uploaded.
|
||||
*/
|
||||
suspend fun maybeUploadOneTimeKeys() {
|
||||
if (oneTimeKeyCheckInProgress) {
|
||||
Timber.v("maybeUploadOneTimeKeys: already in progress")
|
||||
return
|
||||
}
|
||||
if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
||||
// we've done a key upload recently.
|
||||
Timber.v("maybeUploadOneTimeKeys: executed too recently")
|
||||
return
|
||||
}
|
||||
|
||||
oneTimeKeyCheckInProgress = true
|
||||
|
||||
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
||||
?: fetchOtkCount() // we don't have count from sync so get from server
|
||||
?: return Unit.also {
|
||||
oneTimeKeyCheckInProgress = false
|
||||
Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server")
|
||||
}
|
||||
|
||||
Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}")
|
||||
|
||||
lastOneTimeKeyCheck = clock.epochMillis()
|
||||
|
||||
// We then check how many keys we can store in the Account object.
|
||||
val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys()
|
||||
|
||||
// Try to keep at most half that number on the server. This leaves the
|
||||
// rest of the slots free to hold keys that have been claimed from the
|
||||
// server but we haven't received a message for.
|
||||
// If we run out of slots when generating new keys then olm will
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
||||
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
tryOrNull("Unable to upload OTK") {
|
||||
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
||||
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
||||
}
|
||||
oneTimeKeyCheckInProgress = false
|
||||
|
||||
// Check if we need to forget a fallback key
|
||||
val latestPublishedTime = getLastFallbackKeyPublishTime()
|
||||
if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) {
|
||||
// This should be called once you are reasonably certain that you will not receive any more messages
|
||||
// that use the old fallback key
|
||||
Timber.d("## forgetFallbackKey()")
|
||||
olmDevice.forgetFallbackKey()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchOtkCount(): Int? {
|
||||
return tryOrNull("Unable to get OTK count") {
|
||||
val result = uploadKeysTask.execute(UploadKeysTask.Params(KeysUploadBody()))
|
||||
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload some the OTKs.
|
||||
*
|
||||
* @param keyCount the key count
|
||||
* @param keyLimit the limit
|
||||
* @return the number of uploaded keys
|
||||
*/
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
|
||||
if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return 0
|
||||
}
|
||||
var keysThisLoop = 0
|
||||
if (keyLimit > keyCount) {
|
||||
// Creating keys can be an expensive operation so we limit the
|
||||
// number we generate in one go to avoid blocking the application
|
||||
// for too long.
|
||||
keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||
}
|
||||
|
||||
// We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed
|
||||
val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey()
|
||||
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
|
||||
olmDevice.markKeysAsPublished()
|
||||
if (hadUnpublishedFallbackKey) {
|
||||
// It had an unpublished fallback key that was published just now
|
||||
saveLastFallbackKeyPublishTime(clock.epochMillis())
|
||||
}
|
||||
|
||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
// Maybe upload other keys
|
||||
return keysThisLoop +
|
||||
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) +
|
||||
(if (hadUnpublishedFallbackKey) 1 else 0)
|
||||
} else {
|
||||
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLastFallbackKeyPublishTime(timeMillis: Long) {
|
||||
storage.edit().putLong("last_fb_key_publish", timeMillis).apply()
|
||||
}
|
||||
|
||||
private fun getLastFallbackKeyPublishTime(): Long {
|
||||
return storage.getLong("last_fb_key_publish", 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload curve25519 one time keys.
|
||||
*/
|
||||
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
|
||||
val oneTimeJson = mutableMapOf<String, Any>()
|
||||
|
||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
||||
|
||||
curve25519Map.forEach { (key_id, value) ->
|
||||
val k = mutableMapOf<String, Any>()
|
||||
k["key"] = value
|
||||
|
||||
// the key is also signed
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
||||
|
||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
||||
|
||||
oneTimeJson["signed_curve25519:$key_id"] = k
|
||||
}
|
||||
|
||||
val fallbackJson = mutableMapOf<String, Any>()
|
||||
val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
||||
fallbackCurve25519Map.forEach { (key_id, key) ->
|
||||
val k = mutableMapOf<String, Any>()
|
||||
k["key"] = key
|
||||
k["fallback"] = true
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
||||
|
||||
fallbackJson["signed_curve25519:$key_id"] = k
|
||||
}
|
||||
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
val uploadParams = UploadKeysTask.Params(
|
||||
KeysUploadBody(
|
||||
deviceKeys = null,
|
||||
oneTimeKeys = oneTimeJson,
|
||||
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
|
||||
)
|
||||
)
|
||||
return uploadKeysTask.executeRetry(uploadParams, 3)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// max number of keys to upload at once
|
||||
// Creating keys can be an expensive operation so we limit the
|
||||
// number we generate in one go to avoid blocking the application
|
||||
// for too long.
|
||||
private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5
|
||||
|
||||
// frequency with which to check & upload one-time keys
|
||||
private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute
|
||||
}
|
||||
}
|
@@ -1,526 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||
import timber.log.Timber
|
||||
import java.util.Stack
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO)
|
||||
|
||||
/**
|
||||
* This class is responsible for sending key requests to other devices when a message failed to decrypt.
|
||||
* It's lifecycle is based on the sync pulse:
|
||||
* - You can post queries for session, or report when you got a session
|
||||
* - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices
|
||||
* If a request failed it will be retried at the end of the next sync
|
||||
*/
|
||||
@SessionScope
|
||||
internal class OutgoingKeyRequestManager @Inject constructor(
|
||||
@SessionId private val sessionId: String,
|
||||
@UserId private val myUserId: String,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoConfig: MXCryptoConfig,
|
||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter
|
||||
) {
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val sequencer = SemaphoreCoroutineSequencer()
|
||||
|
||||
// We only have one active key request per session, so we don't request if it's already requested
|
||||
// But it could make sense to check more the backup, as it's evolving.
|
||||
// We keep a stack as we consider that the key requested last is more likely to be on screen?
|
||||
private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack<Pair<String, String>>()
|
||||
|
||||
fun requestKeyForEvent(event: Event, force: Boolean) {
|
||||
val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return
|
||||
val index = ratchetIndexForMessage(event) ?: 0
|
||||
postRoomKeyRequest(body, targets, index, force)
|
||||
}
|
||||
|
||||
private fun getRoomKeyRequestTargetForEvent(event: Event): Pair<Map<String, List<String>>, RoomKeyRequestBody>? {
|
||||
val sender = event.senderId ?: return null
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return null.also {
|
||||
Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content")
|
||||
}
|
||||
if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
||||
|
||||
val senderDevice = encryptedEventContent.deviceId
|
||||
val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
|
||||
mapOf(
|
||||
myUserId to listOf("*")
|
||||
)
|
||||
} else {
|
||||
if (event.senderId == myUserId) {
|
||||
mapOf(
|
||||
myUserId to listOf("*")
|
||||
)
|
||||
} else {
|
||||
// for the case where you share the key with a device that has a broken olm session
|
||||
// The other user might Re-shares a megolm session key with devices if the key has already been
|
||||
// sent to them.
|
||||
mapOf(
|
||||
myUserId to listOf("*"),
|
||||
|
||||
// We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700
|
||||
// so in this case query to all
|
||||
sender to listOf(senderDevice ?: "*")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
roomId = event.roomId,
|
||||
algorithm = encryptedEventContent.algorithm,
|
||||
senderKey = encryptedEventContent.senderKey,
|
||||
sessionId = encryptedEventContent.sessionId
|
||||
)
|
||||
return recipients to requestBody
|
||||
}
|
||||
|
||||
private fun ratchetIndexForMessage(event: Event): Int? {
|
||||
val encryptedContent = event.content.toModel<EncryptedEventContent>() ?: return null
|
||||
if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
||||
return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let {
|
||||
tryOrNull {
|
||||
val megolmVersion = it.read()
|
||||
if (megolmVersion != 3) return@tryOrNull null
|
||||
/** Int tag */
|
||||
if (it.read() != 8) return@tryOrNull null
|
||||
it.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean = false) {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalQueueRequest(requestBody, recipients, fromIndex, force)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typically called when we the session as been imported or received meanwhile.
|
||||
*/
|
||||
fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelfCrossSigningTrustChanged(newTrust: Boolean) {
|
||||
if (newTrust) {
|
||||
// we were previously not cross signed, but we are now
|
||||
// so there is now more chances to get better replies for existing request
|
||||
// Let's forget about sent request so that next time we try to decrypt we will resend requests
|
||||
// We don't resend all because we don't want to generate a bulk of traffic
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT)
|
||||
}
|
||||
|
||||
sequencer.post {
|
||||
delay(1000)
|
||||
perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRoomKeyForwarded(
|
||||
sessionId: String,
|
||||
algorithm: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
fromDevice: String?,
|
||||
fromIndex: Int,
|
||||
event: Event
|
||||
) {
|
||||
Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex")
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
cryptoStore.updateOutgoingRoomKeyReply(
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
algorithm = algorithm,
|
||||
senderKey = senderKey,
|
||||
fromDevice = fromDevice,
|
||||
// strip out encrypted stuff as it's just a trail?
|
||||
event = event.copy(
|
||||
type = event.getClearType(),
|
||||
content = mapOf(
|
||||
"chain_index" to fromIndex
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRoomKeyWithHeld(
|
||||
sessionId: String,
|
||||
algorithm: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
fromDevice: String?,
|
||||
event: Event
|
||||
) {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice")
|
||||
Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}")
|
||||
|
||||
// We want to store withheld code from the sender of the message (owner of the megolm session), not from
|
||||
// other devices that might gossip the key. If not the initial reason might be overridden
|
||||
// by a request to one of our session.
|
||||
event.getClearContent().toModel<RoomKeyWithHeldContent>()?.let { withheld ->
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
tryOrNull {
|
||||
deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false)
|
||||
}
|
||||
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
||||
.also { devices ->
|
||||
Timber.tag(loggerTag.value)
|
||||
.v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}")
|
||||
}
|
||||
?.firstOrNull {
|
||||
it.identityKey() == senderKey
|
||||
}
|
||||
}.also {
|
||||
Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}")
|
||||
}?.let {
|
||||
if (it.userId == event.senderId) {
|
||||
if (fromDevice != null) {
|
||||
if (it.deviceId == fromDevice) {
|
||||
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
|
||||
cryptoStore.addWithHeldMegolmSession(withheld)
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
|
||||
cryptoStore.addWithHeldMegolmSession(withheld)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we store the replies from a given request
|
||||
cryptoStore.updateOutgoingRoomKeyReply(
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
algorithm = algorithm,
|
||||
senderKey = senderKey,
|
||||
fromDevice = fromDevice,
|
||||
event = event
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those).
|
||||
*/
|
||||
fun requireProcessAllPendingKeyRequests() {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalProcessPendingKeyRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) {
|
||||
// do we have known requests for that session??
|
||||
Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId")
|
||||
val knownRequest = cryptoStore.getOutgoingRoomKeyRequest(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
senderKey = senderKey
|
||||
)
|
||||
if (knownRequest.isEmpty()) return Unit.also {
|
||||
Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested")
|
||||
}
|
||||
if (knownRequest.size > 1) {
|
||||
// It's worth logging, there should be only one
|
||||
Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId")
|
||||
}
|
||||
knownRequest.forEach { request ->
|
||||
when (request.state) {
|
||||
OutgoingRoomKeyRequestState.UNSENT -> {
|
||||
if (request.fromIndex >= localKnownChainIndex) {
|
||||
// we have a good index we can cancel
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
}
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT -> {
|
||||
// It was already sent, and index satisfied we can cancel
|
||||
if (request.fromIndex >= localKnownChainIndex) {
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
||||
}
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
|
||||
// It is already marked to be cancelled
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
if (request.fromIndex >= localKnownChainIndex) {
|
||||
// we just want to cancel now
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
||||
}
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
|
||||
// was already canceled
|
||||
// if we need a better index, should we resend?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
try {
|
||||
outgoingRequestScope.cancel("User Terminate")
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean) {
|
||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
||||
// we might want to try backup?
|
||||
if (requestBody.roomId != null && requestBody.sessionId != null) {
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId)
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force")
|
||||
val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}")
|
||||
when (existing?.state) {
|
||||
null -> {
|
||||
// create a new one
|
||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
|
||||
}
|
||||
OutgoingRoomKeyRequestState.UNSENT -> {
|
||||
// nothing it's new or not yet handled
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT -> {
|
||||
// it was already requested
|
||||
Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested")
|
||||
if (force) {
|
||||
// update to UNSENT
|
||||
Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}")
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
|
||||
} else {
|
||||
if (existing.roomId != null && existing.sessionId != null) {
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
|
||||
// request is canceled only if I got the keys so what to do here...
|
||||
if (force) {
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
|
||||
}
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
// It's already going to resend
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
|
||||
if (force) {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId)
|
||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existing != null && existing.fromIndex >= fromIndex) {
|
||||
// update the required index
|
||||
cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalProcessPendingKeyRequests() {
|
||||
val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates())
|
||||
Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)")
|
||||
|
||||
measureTimeMillis {
|
||||
toProcess.forEach {
|
||||
when (it.state) {
|
||||
OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it)
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it)
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it)
|
||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED,
|
||||
OutgoingRoomKeyRequestState.SENT -> {
|
||||
// these are filtered out
|
||||
}
|
||||
}
|
||||
}
|
||||
}.let {
|
||||
Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms")
|
||||
}
|
||||
|
||||
val maxBackupCallsBySync = 60
|
||||
var currentCalls = 0
|
||||
measureTimeMillis {
|
||||
while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) {
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) ->
|
||||
// we want to rate limit that somehow :/
|
||||
perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)
|
||||
}
|
||||
currentCalls++
|
||||
}
|
||||
}.let {
|
||||
Timber.tag(loggerTag.value).v("Finish querying backup in $it ms")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) {
|
||||
// In order to avoid generating to_device traffic, we can first check if the key is backed up
|
||||
Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}")
|
||||
val sessionId = request.sessionId ?: return
|
||||
val roomId = request.roomId ?: return
|
||||
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
|
||||
// let's see what's the index
|
||||
val knownIndex = tryOrNull {
|
||||
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")
|
||||
?.wrapper
|
||||
?.session
|
||||
?.firstKnownIndex
|
||||
}
|
||||
if (knownIndex != null && knownIndex <= request.fromIndex) {
|
||||
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
|
||||
Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// we need to send the request
|
||||
val toDeviceContent = RoomKeyShareRequest(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
body = request.requestBody
|
||||
)
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
request.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
|
||||
val params = SendToDeviceTask.Params(
|
||||
eventType = EventType.ROOM_KEY_REQUEST,
|
||||
contentMap = contentMap,
|
||||
transactionId = request.requestId
|
||||
)
|
||||
try {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.executeRetry(params, 3)
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}")
|
||||
// The request was sent, so update state
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT)
|
||||
// TODO update the audit trail
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean {
|
||||
Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}")
|
||||
// we have to cancel this
|
||||
val toDeviceContent = RoomKeyShareRequest(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION
|
||||
)
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
request.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
|
||||
val params = SendToDeviceTask.Params(
|
||||
eventType = EventType.ROOM_KEY_REQUEST,
|
||||
contentMap = contentMap,
|
||||
transactionId = request.requestId
|
||||
)
|
||||
return try {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.executeRetry(params, 3)
|
||||
}
|
||||
// The request cancellation was sent, we don't delete yet because we want
|
||||
// to keep trace of the sent replies
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED)
|
||||
true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) {
|
||||
if (handleRequestToCancel(request)) {
|
||||
// this will create a new unsent request with no replies that will be process in the following call
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class RoomDecryptorProvider @Inject constructor(
|
||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
||||
private val megolmDecryptionFactory: MXMegolmDecryptionFactory
|
||||
) {
|
||||
|
||||
// A map from algorithm to MXDecrypting instance, for each room
|
||||
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
|
||||
|
||||
private val newSessionListeners = ArrayList<NewSessionListener>()
|
||||
|
||||
fun addNewSessionListener(listener: NewSessionListener) {
|
||||
if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener) {
|
||||
newSessionListeners.remove(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a decryptor for a given room and algorithm.
|
||||
* If we already have a decryptor for the given room and algorithm, return
|
||||
* it. Otherwise try to instantiate it.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param algorithm the crypto algorithm
|
||||
* @return the decryptor
|
||||
* // TODO Create another method for the case of roomId is null
|
||||
*/
|
||||
fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
||||
// sanity check
|
||||
if (algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## getRoomDecryptor() : null algorithm")
|
||||
return null
|
||||
}
|
||||
if (roomId != null && roomId.isNotEmpty()) {
|
||||
synchronized(roomDecryptors) {
|
||||
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
|
||||
val alg = decryptors[algorithm]
|
||||
if (alg != null) {
|
||||
return alg
|
||||
}
|
||||
}
|
||||
}
|
||||
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
|
||||
if (decryptingClass) {
|
||||
val alg = when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply {
|
||||
this.newSessionListener = object : NewSessionListener {
|
||||
override fun onNewSession(roomId: String?, sessionId: String) {
|
||||
// PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor
|
||||
newSessionListeners.toList().forEach {
|
||||
try {
|
||||
it.onNewSession(roomId, sessionId)
|
||||
} catch (ignore: Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> olmDecryptionFactory.create()
|
||||
}
|
||||
if (!roomId.isNullOrEmpty()) {
|
||||
synchronized(roomDecryptors) {
|
||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
||||
}
|
||||
}
|
||||
return alg
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
||||
if (roomId == null || algorithm == null) {
|
||||
return null
|
||||
}
|
||||
return roomDecryptors[roomId]?.get(algorithm)
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class RoomEncryptorsStore @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
||||
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
||||
) {
|
||||
|
||||
// MXEncrypting instance for each room.
|
||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
||||
|
||||
fun put(roomId: String, alg: IMXEncrypting) {
|
||||
synchronized(roomEncryptors) {
|
||||
roomEncryptors.put(roomId, alg)
|
||||
}
|
||||
}
|
||||
|
||||
fun get(roomId: String): IMXEncrypting? {
|
||||
return synchronized(roomEncryptors) {
|
||||
val cache = roomEncryptors[roomId]
|
||||
if (cache != null) {
|
||||
return@synchronized cache
|
||||
} else {
|
||||
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
||||
else -> null
|
||||
}
|
||||
alg?.let { roomEncryptors.put(roomId, it) }
|
||||
return@synchronized alg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,306 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class SecretShareManager @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Secret gossiping only occurs during a limited window period after interactive verification.
|
||||
* We keep track of recent verification in memory for that purpose (no need to persist)
|
||||
*/
|
||||
private val recentlyVerifiedDevices = mutableMapOf<String, Long>()
|
||||
private val verifMutex = Mutex()
|
||||
|
||||
/**
|
||||
* Secrets are exchanged as part of interactive verification,
|
||||
* so we can just store in memory.
|
||||
*/
|
||||
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
|
||||
|
||||
// the listeners
|
||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
||||
|
||||
fun addListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a session has been verified.
|
||||
* This information can be used by the manager to decide whether or not to fullfill gossiping requests.
|
||||
* This should be called as fast as possible after a successful self interactive verification
|
||||
*/
|
||||
fun onVerificationCompleteForDevice(deviceId: String) {
|
||||
// For now we just keep an in memory cache
|
||||
cryptoCoroutineScope.launch {
|
||||
verifMutex.withLock {
|
||||
recentlyVerifiedDevices[deviceId] = clock.epochMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleSecretRequest(toDevice: Event) {
|
||||
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
|
||||
?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("handleSecretRequest() : malformed request")
|
||||
}
|
||||
|
||||
// val (action, requestingDeviceId, requestId, secretName) = it
|
||||
val secretName = request.secretName ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.v("handleSecretRequest() : Missing secret name")
|
||||
}
|
||||
|
||||
val userId = toDevice.senderId ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.v("handleSecretRequest() : Missing senderId")
|
||||
}
|
||||
|
||||
if (userId != credentials.userId) {
|
||||
// secrets are only shared between our own devices
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("Ignoring secret share request from other users $userId")
|
||||
return
|
||||
}
|
||||
|
||||
val deviceId = request.requestingDeviceId
|
||||
?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("handleSecretRequest() : malformed request norequestingDeviceId ")
|
||||
}
|
||||
|
||||
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
|
||||
?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("Received secret share request from unknown device $deviceId")
|
||||
}
|
||||
|
||||
val isRequestingDeviceTrusted = device.isVerified
|
||||
val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId)
|
||||
if (isRequestingDeviceTrusted && isRecentInteractiveVerification) {
|
||||
// we can share the secret
|
||||
|
||||
val secretValue = when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
|
||||
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
||||
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey?.toBase64()
|
||||
else -> null
|
||||
}
|
||||
if (secretValue == null) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("The secret is unknown $secretName, passing to app layer")
|
||||
val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() }
|
||||
toList.onEach { listener ->
|
||||
listener.onSecretShareRequest(request)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.SEND_SECRET,
|
||||
"content" to mapOf(
|
||||
"request_id" to request.requestId,
|
||||
"secret" to secretValue
|
||||
)
|
||||
)
|
||||
|
||||
// Is it possible that we don't have an olm session?
|
||||
val devicesByUser = mapOf(device.userId to listOf(device))
|
||||
val usersDeviceMap = try {
|
||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Can't share secret ${request.secretName}: Failed to establish olm session")
|
||||
return
|
||||
}
|
||||
|
||||
val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("secret share: no session with this device $deviceId, probably because there were no one-time keys")
|
||||
return
|
||||
}
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload)
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
try {
|
||||
// raise the retries for secret
|
||||
sendToDeviceTask.executeRetry(sendToDeviceParams, 6)
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("successfully shared secret $secretName to ${device.shortDebugString()}")
|
||||
// TODO add a trail for that in audit logs
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d(" Received secret share request from un-authorised device ${device.deviceId}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||
val verifTimestamp = verifMutex.withLock {
|
||||
recentlyVerifiedDevices[deviceId]
|
||||
} ?: return false
|
||||
|
||||
val age = clock.epochMillis() - verifTimestamp
|
||||
|
||||
return age < SECRET_SHARE_WINDOW_DURATION
|
||||
}
|
||||
|
||||
suspend fun requestSecretTo(deviceId: String, secretName: String) {
|
||||
val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("Can't request secret for $secretName unknown device $deviceId")
|
||||
}
|
||||
val toDeviceContent = SecretShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
secretName = secretName,
|
||||
requestId = createUniqueTxnId()
|
||||
)
|
||||
|
||||
verifMutex.withLock {
|
||||
outgoingSecretRequests.add(toDeviceContent)
|
||||
}
|
||||
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent)
|
||||
|
||||
val params = SendToDeviceTask.Params(
|
||||
eventType = EventType.REQUEST_SECRET,
|
||||
contentMap = contentMap
|
||||
)
|
||||
try {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.execute(params)
|
||||
}
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")
|
||||
// TODO update the audit trail
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
|
||||
if (!toDevice.isEncrypted()) {
|
||||
// secret send messages must be encrypted
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
// no need to download keys, after a verification we already forced download
|
||||
val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }
|
||||
if (sendingDevice == null) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (sendingDevice.userId != credentials.userId) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
|
||||
return
|
||||
}
|
||||
|
||||
if (!sendingDevice.isVerified) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}")
|
||||
return
|
||||
}
|
||||
|
||||
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
|
||||
|
||||
val existingRequest = verifMutex.withLock {
|
||||
outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId }
|
||||
}
|
||||
|
||||
// As per spec:
|
||||
// Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to.
|
||||
if (existingRequest?.secretName == null) {
|
||||
Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||
return
|
||||
}
|
||||
// we don't need to cancel the request as we only request to one device
|
||||
// just forget about the request now
|
||||
verifMutex.withLock {
|
||||
outgoingSecretRequests.remove(existingRequest)
|
||||
}
|
||||
|
||||
if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) {
|
||||
// TODO Ask to application layer?
|
||||
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.actions
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ONE_TIME_KEYS_RETRY_COUNT = 3
|
||||
|
||||
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask
|
||||
) {
|
||||
|
||||
private val ensureMutex = Mutex()
|
||||
|
||||
/**
|
||||
* We want to synchronize a bit here, because we are iterating to check existing olm session and
|
||||
* also adding some.
|
||||
*/
|
||||
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
ensureMutex.withLock {
|
||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||
val deviceList = devicesByUser.flatMap { it.value }
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
|
||||
val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
|
||||
if (force) {
|
||||
// we take all devices and will query otk for them
|
||||
devicesToCreateSessionWith.addAll(deviceList)
|
||||
} else {
|
||||
// only peek devices without active session
|
||||
deviceList.forEach { deviceInfo ->
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val userId = deviceInfo.userId
|
||||
val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
|
||||
Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
|
||||
}
|
||||
|
||||
// is there a session that as been already used?
|
||||
val sessionId = olmDevice.getSessionId(key)
|
||||
if (sessionId.isNullOrEmpty()) {
|
||||
Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
|
||||
devicesToCreateSessionWith.add(deviceInfo)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
|
||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
|
||||
results.setObject(userId, deviceId, olmSessionResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesToCreateSessionWith.isEmpty()) {
|
||||
// no session to create
|
||||
return results
|
||||
}
|
||||
val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
|
||||
devicesToCreateSessionWith.forEach {
|
||||
setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
// Let's now claim one time keys
|
||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim.map)
|
||||
val oneTimeKeysForUsers = withContext(coroutineDispatchers.io) {
|
||||
oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
|
||||
}
|
||||
val oneTimeKeys = MXUsersDevicesMap<MXKey>()
|
||||
for ((userId, mapByUserId) in oneTimeKeysForUsers.oneTimeKeys.orEmpty()) {
|
||||
for ((deviceId, deviceKey) in mapByUserId) {
|
||||
val mxKey = MXKey.from(deviceKey)
|
||||
if (mxKey != null) {
|
||||
oneTimeKeys.setObject(userId, deviceId, mxKey)
|
||||
} else {
|
||||
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// let now start olm session using the new otks
|
||||
devicesToCreateSessionWith.forEach { deviceInfo ->
|
||||
val userId = deviceInfo.userId
|
||||
val deviceId = deviceInfo.deviceId
|
||||
// Did we get an OTK
|
||||
val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
|
||||
if (oneTimeKey == null) {
|
||||
Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
|
||||
} else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
|
||||
Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
|
||||
} else {
|
||||
val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
||||
if (olmSessionId != null) {
|
||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
|
||||
results.setObject(userId, deviceId, olmSessionResult)
|
||||
} else {
|
||||
Timber
|
||||
.tag(loggerTag.value)
|
||||
.d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
||||
var sessionId: String? = null
|
||||
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val signKeyId = "ed25519:$deviceId"
|
||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
||||
|
||||
val fingerprint = deviceInfo.fingerprint()
|
||||
if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) {
|
||||
var isVerified = false
|
||||
var errorMessage: String? = null
|
||||
|
||||
try {
|
||||
olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature)
|
||||
isVerified = true
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).d(
|
||||
e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," +
|
||||
" signature:$signature fingerprint:$fingerprint"
|
||||
)
|
||||
Timber.tag(loggerTag.value).e(
|
||||
"verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " +
|
||||
" - signable json ${oneTimeKey.signalableJSONDictionary()}"
|
||||
)
|
||||
errorMessage = e.message
|
||||
}
|
||||
|
||||
// Check one-time key signature
|
||||
if (isVerified) {
|
||||
sessionId = deviceInfo.identityKey()?.let { identityKey ->
|
||||
olmDevice.createOutboundSession(identityKey, oneTimeKey.value)
|
||||
}
|
||||
|
||||
if (sessionId.isNullOrEmpty()) {
|
||||
// Possibly a bad key
|
||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage")
|
||||
}
|
||||
}
|
||||
|
||||
return sessionId
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user