1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

152 Commits

Author SHA1 Message Date
Benoit Marty
552b143f8c Merge branch 'release/1.6.8' into main 2023-11-28 17:18:51 +01:00
Weblate
f46a9d6cc8 Update release script now that there is only on crypto implementation. 2023-11-28 17:17:44 +01:00
Benoit Marty
b27dc02cfd Adding fastlane file for version 1.6.8 2023-11-28 17:13:37 +01:00
Benoit Marty
6ba03f82e4 Changelog for version 1.6.8 2023-11-28 17:12:03 +01:00
jonnyandrew
a6fafb07da Remove quote from message actions menu (#8689) 2023-11-27 16:30:44 +00:00
Benoit Marty
0c1f190035 Merge pull request #8674 from vector-im/feature/bma/infiniteRingCall
Ensure Background sync is not stopped when there is an active call.
2023-11-23 17:45:20 +01:00
Benoit Marty
8d85d047b7 Ensure Background sync is not stopped when there is an active call, even when the app goes to background. 2023-11-22 09:42:39 +01:00
Benoit Marty
84158ece37 Ensure Background sync is not stopped when there is an active call.
It was happening since the application is foregrounded when VectorCallActivity is displayed.
2023-11-22 09:31:20 +01:00
jonnyandrew
63ef40f58b Fix issue with timeline message view reuse while rich text editor is enabled (#8688) 2023-11-17 17:09:18 +00:00
giomfo
fd5530a2f9 Merge pull request #8683 from vector-im/giomfo/fix_custom_gateway_check
Unified Push: Ignore the potential SSL error when the custom gateway is testing
2023-11-15 22:26:06 +01:00
Giom Foret
ee2fd9f123 add changelog 2023-11-15 17:20:37 +01:00
Giom Foret
7c58af735b Unified Push: Ignore the potential SSL error when the custom gateway is testing
When the Unified Push is enabled, the application checks the potential custom gateway before applying it. If an SSL error happens, the application may ignore this error and keep using this custom gateway. The actual SSL check will be done server side where this gateway is actually used.
2023-11-15 17:10:44 +01:00
Valere
1a941149ab Merge pull request #8679 from vector-im/feature/bca/bump_crypto_sdk_version
bump crypto sdk to 0.3.16
2023-11-14 22:26:32 +01:00
Benoit Marty
98e09eedc3 Merge pull request #8671 from yostyle/yostyle/update_email_regex
Update regex for email address to be aligned on RFC 5322
2023-11-14 14:32:00 +01:00
yostyle
3d7489c7c5 Fix PR comment 2023-11-14 12:38:10 +01:00
yostyle
edf23bbb89 Add changelog
Signed-off-by: yostyle <y.pintas@gmail.com>
2023-11-14 12:38:10 +01:00
yostyle
3179dc1400 Update regex for email address 2023-11-14 12:38:10 +01:00
Valere
d3391076b5 fix comment 2023-11-14 12:09:05 +01:00
Valere
36ce42e36e update change log 2023-11-14 11:33:26 +01:00
Valere
f37d918ce6 fix outdated documentation 2023-11-14 11:24:05 +01:00
Valere
b61b2b6f16 bump crypto sdk to 0.3.16 2023-11-13 22:23:11 +01:00
Benoit Marty
979324da84 Merge pull request #8656 from vector-im/feature/bca/clean_up_legacy_crypto
Remove legacy crypto support.
2023-11-13 16:53:02 +01:00
Valere
d045cedb46 ignore paparazzi tests 2023-11-13 15:01:19 +01:00
Valere
58a44ac668 fix test 2023-11-13 14:00:30 +01:00
Valere
e4c1913e01 Merge branch 'develop' into feature/bca/clean_up_legacy_crypto 2023-11-13 09:20:49 +01:00
Valere
4e53d8462f use error instead of require 2023-11-10 16:33:46 +01:00
Benoit Marty
7073b1647c Merge pull request #8607 from vector-im/dependabot/gradle/io.element.android-wysiwyg-2.5.0
Bump io.element.android:wysiwyg from 2.2.2 to 2.6.0
2023-11-09 09:35:30 +01:00
Benoit Marty
dd6410794c Suppress lint warning. MenuBuilder is restricted. 2023-11-08 17:51:03 +01:00
Benoit Marty
2c75f41072 Fix lint false positive 2023-11-08 17:49:04 +01:00
Benoit Marty
454ba7bf7c Merge branch 'develop' into feature/bca/clean_up_legacy_crypto 2023-11-07 14:13:56 +01:00
Yoan Pintas
b14338d2c4 Remove unused WebRTC dependency (#8658) 2023-11-07 14:12:27 +01:00
Benoit Marty
83084f6481 Upgrade lint 2023-11-07 11:09:30 +01:00
Benoit Marty
4341cf8c9c Upgrade Mavericks library to 3.0.7
It fixes crash: java.lang.IllegalStateException: Flow invariant is violated
2023-11-06 18:29:47 +01:00
Benoit Marty
8d8a5d3de2 Upgrade Wysiwyg library to 2.14.1 2023-11-06 18:17:14 +01:00
jonnyandrew
26e2f4e967 Fix compilation error 2023-11-06 18:15:45 +01:00
jonnyandrew
531d9f2802 Update to 2.6.0 2023-11-06 18:15:45 +01:00
jonnyandrew
6112082d07 Update to new mentions API 2023-11-06 18:15:45 +01:00
dependabot[bot]
95070d3664 Bump io.element.android:wysiwyg from 2.2.2 to 2.5.0
Bumps [io.element.android:wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 2.2.2 to 2.5.0.
- [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/2.2.2...2.5.0)

---
updated-dependencies:
- dependency-name: io.element.android:wysiwyg
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 18:15:45 +01:00
Benoit Marty
8bfd5f7c54 Merge pull request #8662 from vector-im/feature/bma/noNetworkConstraint
Take into account well-known config to disable WorkManager network constraint
2023-10-13 22:03:30 +02:00
Benoit Marty
17e9bd200b Use Matrix.org copyright. 2023-10-13 22:02:52 +02:00
Benoit Marty
fd07835e45 Fix IDE warning 2023-10-13 22:01:46 +02:00
Giom Foret
57d224e8ba fix Unexpected newlines 2023-10-13 16:39:05 +02:00
Giom Foret
d26d28f770 Merge branch 'feature/bma/noNetworkConstraint' of github.com:vector-im/element-android into feature/bma/noNetworkConstraint 2023-10-13 15:36:14 +02:00
Giom Foret
3aa5f34ee7 Update the NetworkConstraint handling in WorkManager config 2023-10-13 15:32:47 +02:00
Benoit Marty
8d95eb7b16 disableNetworkConstraint is now nullable, so do not default the Boolean to false. 2023-10-13 15:13:48 +02:00
Benoit Marty
64a7de5326 Ensure Boolean io.element.disable_network_constraint is explicitly set in the .well-known file. 2023-10-13 09:37:58 +02:00
Benoit Marty
7884b9dd5e Cleanup 2023-10-13 09:26:36 +02:00
Benoit Marty
d4c6a46e90 Parse "io.element.disable_network_constraint" from login response, to ensure that the config is retrieved.
The add pusher worker can be configured before the .well-known config is retrieved.
2023-10-12 18:13:54 +02:00
Valere
d4c141664b fix ignored test 2023-10-12 09:12:32 +02:00
Valere
77a576784f fix test compilation 2023-10-11 23:08:35 +02:00
Valere
491f52a3a3 dead code 2023-10-11 17:57:59 +02:00
Valere
5f68f98d3a fix unit test 2023-10-11 17:04:05 +02:00
Valere
cd101f871c missing mock 2023-10-11 15:07:48 +02:00
Valere
e8922a5fa7 property not in schema 2023-10-11 12:13:50 +02:00
Valere
e27916f85e detekt fix 2023-10-10 16:25:03 +02:00
Giom Foret
973246819a Fix Task :matrix-sdk-android:compileKotlinCryptoDebugUnitTestKotlin FAILED
e: file:///home/runner/actions-runner/_work/element-android/element-android/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt:55:5 No value passed for parameter 'homeServerCapabilitiesDataSource'
2023-10-10 14:11:38 +02:00
Valere
25ecd599f3 Merge branch 'develop' into feature/bca/clean_up_legacy_crypto 2023-10-10 13:00:45 +02:00
Benoit Marty
747c81c687 Changelog. 2023-10-10 10:48:13 +02:00
Benoit Marty
f13a15495b Add a log when network constraint is disabled. 2023-10-09 12:04:35 +02:00
Benoit Marty
2a5e233e2c Do not apply network constraint if "io.element.disable_network_constraint" is set to true in .well-known file. 2023-10-09 11:44:40 +02:00
Benoit Marty
842aeb70e0 Parse "io.element.disable_network_constraint" from .well-known file.
Migrate DB to 54.
2023-10-06 18:03:45 +02:00
Benoit Marty
60940c01df Merge pull request #8651 from vector-im/sync-emojis
Sync Emojis
2023-10-06 17:46:54 +02:00
Benoit Marty
ebc81e24af Merge pull request #8660 from vector-im/feature/bma/stopInfiniteRingingCall
Ensure the incoming call will not ring forever, in case the call is not ended by another way
2023-10-06 17:45:46 +02:00
Benoit Marty
52082a9def Ensure the incoming call will not ring forever, in case the call is not ended by another way (#8178)
Add a safe 2 minutes timer.
2023-10-06 10:44:03 +02:00
ganfra
9e74afc9b1 Merge branch 'main' into develop 2023-10-05 18:11:47 +02:00
ganfra
ecd1057ce9 version++ 2023-10-05 17:00:51 +02:00
ganfra
4fa634a283 Merge branch 'release/1.6.6' into main 2023-10-05 16:57:37 +02:00
ganfra
7001f21330 Adding fastlane file for version 1.6.6 2023-10-05 16:57:21 +02:00
ganfra
d379cef0ba Changelog for version 1.6.6 2023-10-05 16:55:44 +02:00
Valere
59ddf1a107 Merge pull request #8654 from vector-im/bca/fix_8653_qr_code
Fix QR code login support in rust
2023-10-03 23:45:37 +02:00
Valere
a015eda72c code review 2023-10-03 11:29:50 +02:00
Valere
87df8ab6f6 Update matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt
Co-authored-by: Benoit Marty <benoitm@matrix.org>
2023-10-03 11:24:38 +02:00
Valere
1bd2da5c99 disable flacky test on legacy crypto 2023-10-02 16:39:08 +02:00
Valere
a6b127cb20 code quality 2023-10-02 15:31:11 +02:00
Valere
df82eee736 Code quality kdocs 2023-10-02 14:37:15 +02:00
Valere
dfbb3122e7 Remove legacy crypto code 2023-10-02 13:58:51 +02:00
bmarty
1e00da6e2f Sync Emojis 2023-10-02 00:19:14 +00:00
Valere
2709cb2973 missing deprecated 2023-10-01 22:19:54 +02:00
Valere
0d70f6eb54 missing mock 2023-10-01 21:59:46 +02:00
Valere
42eec4b557 update changelog 2023-10-01 19:41:46 +02:00
Valere
6ee438d7d5 bump crypto sdk 2023-10-01 19:25:12 +02:00
Valere
3b9daec869 Fix QR code login support in rust 2023-09-27 15:42:05 +02:00
Benoit Marty
1b3be240b3 Merge pull request #8645 from vector-im/hughns/oidc-device-logout-in-chrome-tab
Open OIDC account management URL in chrome tab
2023-09-14 21:54:23 +02:00
Benoit Marty
8c1cc44255 Merge pull request #8627 from vector-im/feature/bma/hideAccountDeactivation
Hide deactivate account section in case of account managed externally.
2023-09-14 21:53:13 +02:00
Hugh Nimmo-Smith
3f2f3860e1 Changelog 2023-09-14 11:27:49 +01:00
Hugh Nimmo-Smith
470557c59e Open OIDC account management URL in chrome tab
Not the external browser
2023-09-14 11:23:19 +01:00
Yoan Pintas
ff548d2f98 Fix crash when max shortcuts count is exceeded (#8644) 2023-09-13 10:43:03 +00:00
Benoit Marty
d31c741f9d Hide deactivate account section in case of account managed externally. 2023-09-12 16:28:28 +02:00
Benoit Marty
ec9a066900 Merge pull request #8620 from vector-im/feature/bma/oidcSessionEnd
Feature/bma/OIDC session end
2023-09-12 16:25:46 +02:00
Benoit Marty
52a06931f4 Change the test to hide multi signout of devices.
We do not need an external account management URL, which is optional, but we need to know if account management is delegate to Oidc.
2023-08-31 16:04:45 +02:00
Benoit Marty
a889d8d678 Store the authentication issuer into DB. 2023-08-31 09:57:47 +02:00
Jorge Martin Espinosa
1f41c54a82 Merge pull request #8630 from vector-im/dependabot/gradle/org.matrix.rustcomponents-crypto-android-0.3.14
Bump org.matrix.rustcomponents:crypto-android from 0.3.10 to 0.3.14
2023-08-29 08:19:17 +02:00
Jorge Martín
fe51ee3956 Try bumping heap size of gradle and the kotlin daemon as a last resort 2023-08-28 17:17:41 +02:00
Jorge Martín
d65459cc59 Try bumping the whole memory heap size in rust tests 2023-08-28 16:05:40 +02:00
Jorge Martín
dc8230e435 Ignore LocaleFolder lint error since we won't fix it. 2023-08-28 15:35:20 +02:00
Jorge Martín
0838a10b65 Bump memory allocated for tests 2023-08-28 15:34:52 +02:00
Benoit Marty
a3be0286ee Merge pull request #8341 from tomtit/bugfix/issue-7758
Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
2023-08-28 13:15:05 +02:00
dependabot[bot]
751bd27c9d Bump org.matrix.rustcomponents:crypto-android from 0.3.10 to 0.3.14
Bumps [org.matrix.rustcomponents:crypto-android](https://github.com/matrix-org/matrix-rust-components-kotlin) from 0.3.10 to 0.3.14.
- [Release notes](https://github.com/matrix-org/matrix-rust-components-kotlin/releases)
- [Commits](https://github.com/matrix-org/matrix-rust-components-kotlin/compare/crypto-v0.3.10...crypto-v0.3.14)

---
updated-dependencies:
- dependency-name: org.matrix.rustcomponents:crypto-android
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-25 23:02:56 +00:00
Benoit Marty
0a6dbeb3fe Remove unsupported language. We may handle that during the next SAS string update. 2023-08-23 16:51:31 +02:00
Benoit Marty
bc23f82ade Merge pull request #8625 from RiotTranslateBot/weblate-element-android-element-app
Translations update from Weblate
2023-08-23 16:08:29 +02:00
Benoit Marty
ca109f70a4 Merge pull request #8626 from vector-im/sync-sas-strings
Sync SAS Strings
2023-08-23 16:07:30 +02:00
Besnik Bleta
b5c224f3e0 Translated using Weblate (Albanian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sq/
2023-08-23 09:18:32 +00:00
Besnik Bleta
e2c7833f93 Translated using Weblate (Albanian)
Currently translated at 99.4% (2626 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sq/
2023-08-23 09:18:30 +00:00
Weblate
6d5f59c67e Merge branch 'origin/develop' into Weblate. 2023-08-23 07:41:31 +00:00
bmarty
e6bd57d88c Sync SAS Strings 2023-08-23 07:21:22 +00:00
Benoit Marty
81f7517560 Be able to trigger manually this workflow. 2023-08-23 09:20:04 +02:00
Weblate
1ceacdd194 Merge branch 'origin/develop' into Weblate. 2023-08-23 07:15:33 +00:00
Benoit Marty
52aa4bb0d8 Translated using Weblate (Turkish)
Currently translated at 58.6% (1546 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/tr/
2023-08-23 07:15:31 +00:00
Florian Renaud
a5d231c259 Merge pull request #8623 from vector-im/travis/fix-sas-import
Fix SAS strings import URL
2023-08-23 08:51:57 +02:00
Linerly
e6a18a2241 Translated using Weblate (Indonesian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/id/
2023-08-23 06:12:22 +00:00
Jeff Huang
003a134f68 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/zh_Hant/
2023-08-23 03:25:15 +00:00
Ihor Hordiichuk
494e824a85 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/uk/
2023-08-22 21:40:19 +00:00
Jozef Gaal
df97229b9c Translated using Weblate (Slovak)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/sk/
2023-08-22 20:18:57 +00:00
Travis Ralston
6e6478a949 add changelog 2023-08-22 12:14:36 -06:00
Travis Ralston
de688aa93b Fix SAS strings import URL
See https://github.com/vector-im/element-android/issues/8525
2023-08-22 12:11:58 -06:00
waclaw66
1eee5c1de7 Translated using Weblate (Czech)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/cs/
2023-08-22 18:09:44 +00:00
Danial Behzadi
ce5d42d484 Translated using Weblate (Persian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/fa/
2023-08-22 17:58:13 +00:00
Danial Behzadi
6379420401 Translated using Weblate (Persian)
Currently translated at 100.0% (2640 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/
2023-08-22 17:58:12 +00:00
Vri
9821487a8e Translated using Weblate (German)
Currently translated at 100.0% (102 of 102 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/de/
2023-08-22 16:08:08 +00:00
Weblate
2b29a57b9b Merge branch 'origin/develop' into Weblate. 2023-08-22 15:14:22 +00:00
Benoit Marty
87e5900dcd Merge pull request #8618 from vector-im/feature/bma/3pidCapability
Hide setting entry point to manage 3Pid if homeserver capability `m.3pid_changes` is set to `false`.
2023-08-22 15:10:12 +02:00
Benoit Marty
dc19380fbf Changelog 2023-08-22 12:41:13 +02:00
Benoit Marty
880ed69f97 OIDC redirect to the web page to delete a session (new session manager) #8616 2023-08-22 12:17:23 +02:00
Benoit Marty
8941e6396c Hide multi signout if we have an external account manager (#8616) 2023-08-22 12:08:33 +02:00
Benoit Marty
425441546e Format 2023-08-22 12:08:06 +02:00
Benoit Marty
12395e9b04 OIDC redirect to the web page to delete a session (legacy session manager) #8616 2023-08-22 11:40:47 +02:00
Benoit Marty
8f6edba403 Fix typo 2023-08-21 18:00:41 +02:00
Benoit Marty
39a783196e Hide setting entry point to manage 3Pid if homeserver capability m.3pid_changes is set to false. 2023-08-21 17:25:48 +02:00
LinAGKar
92399aba07 Translated using Weblate (Swedish)
Currently translated at 100.0% (2640 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/sv/
2023-08-17 21:03:17 +00:00
phardyle
83e2419c30 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (2636 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hant/
2023-08-15 11:56:13 +00:00
phardyle
3216fa6146 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (2613 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/zh_Hans/
2023-08-15 11:56:13 +00:00
franconian
eeb67e1934 Translated using Weblate (German)
Currently translated at 100.0% (2640 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
2023-08-13 23:51:07 +00:00
franconian
23e7bdbae3 Translated using Weblate (German)
Currently translated at 100.0% (2640 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/de/
2023-08-13 21:14:58 +00:00
Someone
ad7934847c Translated using Weblate (Vietnamese)
Currently translated at 88.2% (2326 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/vi/
2023-08-12 10:45:18 +00:00
Nizami
45be2749f6 Translated using Weblate (Azerbaijani)
Currently translated at 9.2% (245 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/az/
2023-08-10 15:54:18 +00:00
Ihor Hordiichuk
754ea6a98d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (2636 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
2023-08-09 22:49:13 +00:00
Nizami
30906885ec Translated using Weblate (Azerbaijani)
Currently translated at 9.0% (240 of 2640 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/az/
2023-08-09 22:49:13 +00:00
Ihor Hordiichuk
5580f307be Translated using Weblate (Ukrainian)
Currently translated at 100.0% (2636 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/
2023-08-07 01:11:10 +00:00
DarkCoder15
8885d14ee5 Translated using Weblate (Russian)
Currently translated at 99.9% (2634 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/
2023-08-04 13:52:59 +00:00
Berk Mirsat KAPOT
380a0b8de3 Translated using Weblate (Turkish)
Currently translated at 58.6% (1546 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/tr/
2023-07-31 06:37:03 +00:00
Edgars Andersons
1bbd4b7e44 Translated using Weblate (Latvian)
Currently translated at 99.8% (2633 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-31 06:37:01 +00:00
Edgars Andersons
27bae30eac Translated using Weblate (Latvian)
Currently translated at 99.8% (2633 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-29 08:36:45 +00:00
Edgars Andersons
40fd9f2f7b Translated using Weblate (Latvian)
Currently translated at 97.0% (2558 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-29 01:16:29 +00:00
Rafael Fontenelle
10cde1f0a6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (100 of 100 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.element.io/projects/element-android/element-store/pt_BR/
2023-07-28 06:43:13 +00:00
Rafael Fontenelle
fd46487270 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (2636 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/pt_BR/
2023-07-28 06:43:11 +00:00
Edgars Andersons
cd7bf12e16 Translated using Weblate (Latvian)
Currently translated at 92.3% (2434 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-28 06:43:09 +00:00
Edgars Andersons
95b63ccefb Translated using Weblate (Latvian)
Currently translated at 88.6% (2336 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-27 12:00:34 +00:00
Weblate
975ef3c06f Merge branch 'origin/develop' into Weblate. 2023-07-25 20:21:06 +00:00
Edgars Andersons
e567b9c9cf Translated using Weblate (Latvian)
Currently translated at 84.0% (2216 of 2636 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.element.io/projects/element-android/element-app/lv/
2023-07-25 20:21:03 +00:00
Benoit Marty
9aeb3b7074 version++ 2023-07-25 14:58:22 +02:00
Benoit Marty
313d4f82f7 Merge tag 'v1.6.5' into develop
tag
2023-07-25 14:56:55 +02:00
Alexey Nechaev
28da02c583 Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
Signed-off-by: Alexey Nechaev <seysane@yahoo.com>
2023-04-22 19:31:24 +03:00
316 changed files with 2519 additions and 24147 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,102 +0,0 @@
name: Test
on:
pull_request: { }
push:
branches: [ main, develop ]
paths-ignore:
- '.github/**'
# Enrich gradle.properties for CI/CD
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-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

View File

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

View File

@@ -1,3 +1,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)
======================================

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ mv towncrier.toml towncrier.toml.bak
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
rm towncrier.toml.bak
yes n | towncrier build --version nightly
./gradlew assembleGplayRustCryptoNightly appDistributionUploadRustKotlinCryptoNightly $CI_GRADLE_ARG_PROPERTIES
./gradlew assembleGplayNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
```
Then you can reset the change on the codebase.

View File

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

View 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

View 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

View File

@@ -0,0 +1,2 @@
Die wichtigsten Änderungen in dieser Version: Fehlerbehebungen.
Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
تغییرات عمده در این نگارش: المنت اندروید اکنون از SDK راست Crypto استفاده می‌کند.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases

View File

@@ -0,0 +1,2 @@
تغغیرات عمده در این نگارش: ارائه تصحیحی.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases

View 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

View File

@@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: rilis perbaikan.
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: opravné vydanie.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

View 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

View 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

View File

@@ -0,0 +1,2 @@
Основні зміни в цій версії: Element для Android відтепер використовує Crypto Rust SDK.
Список усіх змін: https://github.com/vector-im/element-android/releases

View File

@@ -0,0 +1,2 @@
Основні зміни в цій версії: коригувальний випуск.
Список усіх змін: https://github.com/vector-im/element-android/releases

View File

@@ -0,0 +1,2 @@
此版本的主要變更現在起Element Android 使用 Crypto Rust SDK。
完整的變更紀錄https://github.com/vector-im/element-android/releases

View File

@@ -0,0 +1,2 @@
此版本中的主要變動:版本修正。
完整的變更紀錄https://github.com/vector-im/element-android/releases

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 -&gt; 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>

View File

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

View File

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

View File

@@ -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">Skapet dot shërbyesi Home. Nëse keni dalë, sido qoftë, kjo pajisje sdo 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">Sarrihet 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">Sarrihet 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,6 @@ dokkaHtml {
}
}
}
apply from: '../flavor.gradle'
android {
namespace "org.matrix.android.sdk"
@@ -63,7 +62,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.6.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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