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

Compare commits

...

244 Commits

Author SHA1 Message Date
ericdecanini
92492c95c1 Fixes errors and gets first test passing 2022-07-19 15:44:29 +02:00
ericdecanini
ed8212a8c0 Post cherry pick fix 2022-07-19 15:44:24 +02:00
ericdecanini
427f53dce7 Post cherry pick fix 2022-07-19 15:43:15 +02:00
ericdecanini
91008785cb Fixes errors and gets first test passing 2022-07-08 11:56:00 +01:00
ericdecanini
c8e33532f7 Refactors AppStateHandler into interface implementation pattern 2022-07-08 10:05:05 +01:00
ericdecanini
8ef89958b3 Improves formatting in AppStateHandler 2022-07-07 16:59:38 +01:00
ericdecanini
067c485e4f Merge remote-tracking branch 'origin/develop' into feature/nfe/legacy_group_removal
# Conflicts:
#	vector/src/main/java/im/vector/app/features/grouplist/GroupSummaryItem.kt
#	vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt
#	vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt
2022-07-07 15:44:42 +01:00
Adam Brown
8a68b31f1b Merge branch '1.4.27-RC2' into develop 2022-07-07 11:29:49 +01:00
Benoit Marty
1659ca001d Merge pull request #6491 from vector-im/dependabot/gradle/flipper-0.153.0
Bump flipper from 0.152.0 to 0.153.0
2022-07-07 12:28:10 +02:00
Adam Brown
7f1bcacd97 Merge branch 'hotfix/v1.4.27' into develop 2022-07-07 11:26:59 +01:00
Adam Brown
67d5289f01 Merge pull request #6462 from vector-im/feature/fga/fix_6461
Timeline: fix validation of timeline event changes
2022-07-07 11:25:33 +01:00
Adam Brown
3e770f9efa Merge pull request #6474 from vector-im/feature/fga/fix_6463
Fix crashes when opening Thread (#6463)
2022-07-07 11:12:28 +01:00
Adam Brown
4126e6418b Merge pull request #6469 from vector-im/feature/bma/fix_crash_on_DefaultBackgroundDetectionObserver
Fix ConcurrentModificationException on BackgroundDetectionObserver
2022-07-07 10:58:26 +01:00
ganfra
8abae6f917 Use executeTransactionAwait (need realm refresh in this case) 2022-07-07 09:40:54 +01:00
dependabot[bot]
7c3257942c Bump flipper from 0.152.0 to 0.153.0
Bumps `flipper` from 0.152.0 to 0.153.0.

Updates `flipper` from 0.152.0 to 0.153.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.152.0...v0.153.0)

Updates `flipper-network-plugin` from 0.152.0 to 0.153.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.152.0...v0.153.0)

---
updated-dependencies:
- dependency-name: com.facebook.flipper:flipper
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.facebook.flipper:flipper-network-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 23:05:35 +00:00
ganfra
19fc97ba0f Use executeTransactionAwait (need realm refresh in this case) 2022-07-06 19:33:34 +02:00
Benoit Marty
09f158e85d Merge pull request #6468 from vector-im/bmarty-patch-1
Fix typo
2022-07-06 15:52:11 +02:00
Adam Brown
89348995c2 Merge pull request #6467 from vector-im/feature/adm/share-text-crash
Crash - Sharing text
2022-07-06 11:24:56 +01:00
Benoit Marty
7616912411 Merge pull request #6436 from vector-im/feature/bma/activity_fragment_1_5_0
Upgrade androidx activity and fragment to 1.5.0
2022-07-06 11:44:22 +02:00
ganfra
0743140973 Fix crashes when opening Thread (#6463) 2022-07-05 17:00:01 +02:00
Benoit Marty
d957e24747 Merge pull request #6458 from vector-im/feature/bma/android_services
Rename Android Service to use `AndroidService` suffix
2022-07-05 14:07:36 +02:00
Adam Brown
03202080b3 suppressing unused string resource 2022-07-05 11:17:10 +01:00
Benoit Marty
b68e01cda4 Changelog 2022-07-05 12:11:50 +02:00
Benoit Marty
b847d8cf36 Fix ConcurrentModificationException on BackgroundDetectionObserver 2022-07-05 12:06:49 +02:00
Benoit Marty
16c6cdf108 Fix typo 2022-07-05 11:45:01 +02:00
Adam Brown
3c092c4e2a adding changelog entry 2022-07-05 10:37:41 +01:00
Benoit Marty
4f19034a94 Exclude AndroidService from coverage metrics. 2022-07-05 11:17:40 +02:00
ganfra
f58ba13ef0 Timeline: fix validation of timeline event changes 2022-07-05 10:41:22 +02:00
Benoit Marty
a51d626fe8 Changelog 2022-07-05 09:40:27 +02:00
Benoit Marty
35325db407 Merge pull request #6456 from vector-im/bmarty-patch-1
Fix copy paste issue
2022-07-05 09:39:11 +02:00
Benoit Marty
f0c8c3fd63 Rename Android Service to use AndroidService suffix to limit confusion with our Matrix SDK Services 2022-07-04 21:39:01 +02:00
Benoit Marty
293a177148 Quick rework, better to search for expected MenuItem 2022-07-04 21:27:28 +02:00
Benoit Marty
3e42cec4ec Fix detekt issue. 2022-07-04 21:15:38 +02:00
Benoit Marty
57f42ed0f5 Fix copy paste issue 2022-07-04 21:10:51 +02:00
Adam Brown
2c444527bd Merge pull request #6447 from cloudrac3r/fix-html-entities
Fix HTML entities being displayed in messages
2022-07-04 17:31:34 +01:00
Adam Brown
dd397b9a48 splitting the share intent handling from the attachments helper
- decouples the attachment callback
2022-07-04 17:14:49 +01:00
Benoit Marty
d43c802c90 Changelog 2022-07-04 17:48:35 +02:00
Benoit Marty
1355178fee Fix compilation issue for Fragment 2022-07-04 17:42:58 +02:00
Benoit Marty
81505d3802 Menu: extract management to a common interface / Migrate Fragments 2022-07-04 17:30:06 +02:00
Adam Brown
8811f752e5 converting if/else to when 2022-07-04 16:15:01 +01:00
Benoit Marty
be099dcae0 Menu: extract management to a common interface / Migrate Activities 2022-07-04 17:10:12 +02:00
Adam Brown
abf35d730d Merge pull request #6157 from vector-im/feature/adm/ftue-msisdn-confirmation
[FTUE] MSISDN / Phone number confirmation
2022-07-04 11:29:39 +01:00
Maxime NATUREL
f3e7d0daff Merge pull request #6431 from vector-im/fix/mna/closed-poll-visible-votes
[Poll] - Wrong votes in closed poll after removing 2 previous polls (PSG-590)
2022-07-04 11:33:26 +02:00
Maxime NATUREL
906fe8be76 Merge pull request #6425 from vector-im/fix/mna/undisclosed-poll-description
[Poll] - Add a description under undisclosed poll when not ended (PSB-134)
2022-07-04 11:20:59 +02:00
Adam Brown
2a36dc8ee5 using confirmation copy for the confirmation code entry field 2022-07-04 09:44:45 +01:00
Benoit Marty
941c2a792f Merge pull request #6409 from vector-im/dependabot/gradle/com.google.gms-google-services-4.3.13
Bump google-services from 4.3.10 to 4.3.13
2022-07-04 10:24:26 +02:00
Adam Brown
daecd7d43a returning the error result directly from the when 2022-07-04 09:21:15 +01:00
Maxime NATUREL
26aaf84806 Renaming field to votesStatus 2022-07-04 10:14:15 +02:00
Benoit Marty
cadd5c050b Merge pull request #6450 from cloudrac3r/fix-attach-gallery
Gallery picker can pick external images (not just videos)
2022-07-04 09:44:50 +02:00
Cadence Ember
b8734a23a9 Gallery picker can pick external images 2022-07-04 16:42:43 +12:00
Cadence Ember
a53ad39e1a Add comments for the EventHtmlRenderer file 2022-07-04 01:07:38 +12:00
Cadence Ember
84bb11c1bf Fix HTML entities being displayed in messages
Initially reported in #6445. Fixes #6445.
This was a regression from #6357.

The fix is to enable Markwon's HTML entities processor.
2022-07-04 01:07:14 +12:00
Benoit Marty
93b7e1094c Merge pull request #6434 from vector-im/feature/bca/check_serialized_frozen
Simple check to prevent frozen class modificaiton
2022-07-01 18:01:08 +02:00
Benoit Marty
f7a0615105 Fix warning on onMultiWindowModeChanged method overriding. 2022-07-01 17:35:01 +02:00
Valere
8dc57fe2f0 Merge pull request #5853 from vector-im/feature/aris/crypto_share_room_keys_past_messages
Share Megolm session keys when inviting a new user
2022-07-01 17:33:43 +02:00
Benoit Marty
397614121c Fix warning on onPictureInPictureModeChanged method overriding. 2022-07-01 17:30:48 +02:00
Benoit Marty
944b447d93 Fix regression when closing the screen to create a DM from the toolbar. Rework this part a bit. 2022-07-01 17:06:02 +02:00
Benoit Marty
aae6e20f9c Migration to activity 1.5.0. Rework menu management 2022-07-01 17:06:02 +02:00
Eric Decanini
bdb49f5946 Merge pull request #5398 from vector-im/bugfix/eric/softlogout-ux-broken
Fixes broken SoftLogout UX for homeservers that support both Password and SSO
2022-07-01 15:52:48 +01:00
Valere
d281f9dde5 use XXX not TODO 2022-07-01 16:07:03 +02:00
Maxime NATUREL
d5b375e82b Merge pull request #6414 from vector-im/feature/mna/reply-to-lls
[Location sharing] - Reply action on a live message (PSG-343)
2022-07-01 16:03:02 +02:00
Valere
6fd99dc302 resist ConnectivityManager$TooManyRequestsException 2022-07-01 15:56:03 +02:00
Valere
90a4e71b06 update flacky test 2022-07-01 14:30:21 +02:00
Maxime NATUREL
f5e33ca980 Fix unit tests 2022-07-01 14:27:10 +02:00
Adam Brown
82feda476d removing unused imports 2022-07-01 13:18:09 +01:00
Adam Brown
5e9e65e10f using clear error on change extension 2022-07-01 13:05:26 +01:00
Adam Brown
be3d419290 adding phone number confirmation resending 2022-07-01 12:14:34 +01:00
Adam Brown
e6e079a071 adding barebone phone confirmation fragment, copied from the phone number input 2022-07-01 12:14:02 +01:00
Adam Brown
e326188aa8 Merge pull request #6108 from vector-im/feature/adm/ftue-msisdn-entry
FTUE - Msisdn (phone number) entry
2022-07-01 12:07:57 +01:00
Adam Brown
bfa50f186a formatting 2022-07-01 11:14:25 +01:00
Adam Brown
f2db4be479 fixing formatting/unused imports 2022-07-01 11:14:25 +01:00
Adam Brown
0bbc74b193 injects the phonenumberutil and adds testcases around the parsing 2022-07-01 11:14:25 +01:00
Adam Brown
27b1bc9e66 handling msisdn 401 errors as success within the registration wizard delegate 2022-07-01 11:14:25 +01:00
Adam Brown
7617309058 hooking up, styling and applying copy to the phone number fragment 2022-07-01 11:14:24 +01:00
Adam Brown
a8b73f0cf9 applying autofill hints for phonenumber and email entry 2022-07-01 11:13:57 +01:00
Adam Brown
df6ebcacd1 adding msisdn fragment and layout, copied from email input 2022-07-01 11:12:45 +01:00
Valere
a92fae6d25 add change log 2022-07-01 12:00:26 +02:00
Benoit Marty
1297ccd45c Fix first compilation errors 2022-07-01 11:58:01 +02:00
Valere
28ca03cc67 Simple check to prevent frozen class modificaiton 2022-07-01 11:54:30 +02:00
Benoit Marty
c6a89c738a Merge pull request #6419 from vector-im/dependabot/gradle/androidx.activity-activity-1.5.0
Bump activity from 1.4.0 to 1.5.0
2022-07-01 11:47:49 +02:00
Benoit Marty
529898d9fa Merge pull request #6420 from vector-im/dependabot/gradle/fragment-1.5.0
Bump fragment from 1.4.1 to 1.5.0
2022-07-01 11:47:18 +02:00
Benoit Marty
9a2beb5017 Merge pull request #6416 from vector-im/dependabot/gradle/flipper-0.152.0
Bump flipper from 0.151.1 to 0.152.0
2022-07-01 11:43:53 +02:00
Valere
08cb6de83d Fix migration 2022-07-01 11:08:35 +02:00
Valere
fb5f0cbd00 Fix test compilation 2022-07-01 09:43:17 +02:00
Valere
e7322e8524 outdated configuration 2022-07-01 09:43:17 +02:00
Valere
5a67c39c7f reuse code for test 2022-07-01 09:43:17 +02:00
Valere
a885ff5e47 Fix test 2022-07-01 09:43:17 +02:00
Valere
b0907de582 Fix migration 2022-07-01 09:43:17 +02:00
Valere
ddd82441bd kdoc 2022-07-01 09:43:17 +02:00
Valere
8e829c6aad Add lab flag and more tests 2022-07-01 09:43:17 +02:00
Valere
d9fb58fbcb Fix tests 2022-07-01 09:42:31 +02:00
Valere
f64adeba7f fix bad sender key export 2022-07-01 09:42:31 +02:00
Valere
34145f0374 post rebase fix 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
df241dbdb8 Fix broken unit test 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
010cf540b6 Fix broken unit test 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
55fdff4242 Resolve merge conflicts 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
d3a516b05d Enhance key sharing to respect matrix configuration 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
a9a7400fef Add MXCryptoConfig flag for key history sharing
Add shared_history flag to sessionBackupData
2022-07-01 09:42:31 +02:00
Valere
fb352ffa38 quick format 2022-07-01 09:42:31 +02:00
Valere
d8d808d0b4 removed deprecated annotation, CI don't like 2022-07-01 09:42:31 +02:00
Valere
8c26592d46 cleaning 2022-07-01 09:42:31 +02:00
Valere
9b8e45ebfe share keys for history take2 2022-07-01 09:42:31 +02:00
Valere
93aac8faea post rebase fix 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
2e88998b05 Add integration tests for shared keys rotation on room history visibility change 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
3a5b737639 Fix existing E2eeSanityTests to support changes for key history sharing 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
28dd507a74 Add crypto shared history sanity test 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
96f0d52753 Update copyright 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
45c80de333 Add changelog 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
243463adbc Add logs 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
395d48f946 Refactor code structure and improve naming 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
497f7cf044 Rotate our session when there is a room history visibility change since the last outboundSession 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
d6358dcb16 Prevent injecting a forged encrypted message and using session_id/sender_key of another room. 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
28a3ae264c Remove sharedHistory from OlmInboundGroupSessionWrapper2 while there are migration issues, and use only the equivalent DB entity value 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
dd3928f075 Remove sendSharedHistoryKeys while we will only share latest messages 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
b3bfd05ecb - Share only the first chunk of inbound sessions instead of the whole key history
- Download keys if the user is unknown (first invite)
2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
e861edd544 Implement history key sharing functionality with respect to room visibility settings 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
6e57aeb9e5 Add roomId in InboundSessionEntity for better performance
Add shared history flag to InboundSessionEntity
2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
98b55457b5 Add sendSharedHistoryKeys in crypto service 2022-07-01 09:42:31 +02:00
ariskotsomitopoulos
34713d5023 Add sharing existing inbound sessions functionality on new room invites 2022-07-01 09:42:31 +02:00
Benoit Marty
e2a55fb6f6 Merge pull request #4364 from vector-im/yostyle/open_url_on_browser_task
Open URL on external browser task
2022-06-30 22:36:26 +02:00
Benoit Marty
c77ecc6a21 Merge pull request #6288 from deepbluev7/nico/stable-aliases
Use stable endpoint for alias management
2022-06-30 22:30:14 +02:00
Benoit Marty
58580f1e6a Merge pull request #6413 from vector-im/feature/bma/room_member_loading
Show a loader if all the Room Member are not yet loaded.
2022-06-30 17:13:50 +02:00
Benoit Marty
fbbd6b1a90 Remove unused import 2022-06-30 17:13:13 +02:00
Benoit Marty
98a7f7df4d Merge pull request #6429 from vector-im/feature/bma/fragile_data
Add `android:hasFragileUserData="true"` in the manifest
2022-06-30 17:11:27 +02:00
ganfra
6d1dd089f0 Update versions 2022-06-30 16:58:51 +02:00
ganfra
415b0d4820 Merge branch 'release/v1.4.26' into develop 2022-06-30 16:55:52 +02:00
Adam Brown
72c4af0026 Merge pull request #6263 from vector-im/feature/adm/ftue-forgot-password
[FTUE] Forgot password
2022-06-30 15:40:09 +01:00
Adam Brown
054c0435a8 Merge pull request #6130 from vector-im/dependabot/gradle/io.realm-realm-gradle-plugin-10.11.0
Bump realm-gradle-plugin from 10.9.0 to 10.11.0
2022-06-30 15:00:21 +01:00
Adam Brown
ea71a8b5c2 Merge pull request #6415 from vector-im/feature/adm/replacing-epoxy-layout-id
Replacing epoxy item annotation layout references with getDefaultLayoutId
2022-06-30 14:58:46 +01:00
Benoit Marty
90e851a4bc Merge pull request #6392 from vector-im/feature/bma/safe_clearWith
Ensure clearWith lambda is deleting all the list item
2022-06-30 15:46:53 +02:00
Maxime NATUREL
18efa84e3a Adding changelog entry 2022-06-30 14:58:32 +02:00
Maxime NATUREL
a8d43538f1 Explicitely hiding votes when disclosed poll 2022-06-30 14:51:07 +02:00
Benoit Marty
e53dd1e1a1 Improve readability. 2022-06-30 14:50:20 +02:00
Benoit Marty
0c98a2f81f Changelog 2022-06-30 14:20:45 +02:00
Benoit Marty
2118eaea90 Format file 2022-06-30 14:19:20 +02:00
Benoit Marty
52b07021a7 Add android:hasFragileUserData="true" to the manifest. See details in #2352 2022-06-30 14:19:04 +02:00
Adam Brown
0d084648a3 showing a toast on password reset confirmation 2022-06-30 10:44:06 +01:00
Adam Brown
d0e5b3eb21 lifting duplicated event_base layout to the base class, with the option for children to override 2022-06-30 09:46:20 +01:00
Adam Brown
a815ac996b using vector model for consistency 2022-06-30 09:45:56 +01:00
Maxime NATUREL
55bb6fa21a Adding changelog entry 2022-06-30 10:13:41 +02:00
Maxime NATUREL
9fb19af39c Adding a description under undisclosed poll when not yet ended 2022-06-30 10:13:41 +02:00
dependabot[bot]
356718dc9a Bump fragment from 1.4.1 to 1.5.0
Bumps `fragment` from 1.4.1 to 1.5.0.

Updates `fragment-ktx` from 1.4.1 to 1.5.0

Updates `fragment-testing` from 1.4.1 to 1.5.0

---
updated-dependencies:
- dependency-name: androidx.fragment:fragment-ktx
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: androidx.fragment:fragment-testing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-29 23:11:02 +00:00
dependabot[bot]
62a20ba69d Bump activity from 1.4.0 to 1.5.0
Bumps activity from 1.4.0 to 1.5.0.

---
updated-dependencies:
- dependency-name: androidx.activity:activity
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-29 23:10:20 +00:00
dependabot[bot]
85504162a5 Bump flipper from 0.151.1 to 0.152.0
Bumps `flipper` from 0.151.1 to 0.152.0.

Updates `flipper` from 0.151.1 to 0.152.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.151.1...v0.152.0)

Updates `flipper-network-plugin` from 0.151.1 to 0.152.0
- [Release notes](https://github.com/facebook/flipper/releases)
- [Commits](https://github.com/facebook/flipper/compare/v0.151.1...v0.152.0)

---
updated-dependencies:
- dependency-name: com.facebook.flipper:flipper
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.facebook.flipper:flipper-network-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-29 23:09:50 +00:00
Adam Brown
c603b780cf extracting common breaker background selection to ftue extensions 2022-06-29 17:47:28 +01:00
Adam Brown
7df7df7541 removing unused imports 2022-06-29 17:40:25 +01:00
Adam Brown
85264401cc adding changelog entry 2022-06-29 17:35:45 +01:00
Adam Brown
d2fbe26182 removing no longer needed butterknife dependency 2022-06-29 17:22:57 +01:00
Adam Brown
33ca5753f1 replacing epoxy item annotation layout references with getDefaultLayoutId override 2022-06-29 17:21:05 +01:00
ericdecanini
4cf97d48c9 Merge remote-tracking branch 'origin/develop' into bugfix/eric/softlogout-ux-broken
# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
#	vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
#	vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt
2022-06-29 15:53:22 +02:00
Benoit Marty
e75070be91 Use a DataSrouce instead of a Task and return a non Optional Boolean. 2022-06-29 15:35:00 +02:00
Adam Brown
1361852721 triggering an initial enabled state when using associateContentStateWith and extracts the resetting of errors on content change to an extension 2022-06-29 14:15:39 +01:00
Benoit Marty
a0025bc99b Update after PR review. 2022-06-29 15:05:26 +02:00
Maxime NATUREL
f5d3bcbb94 Sending a reply to a live location share 2022-06-29 15:04:57 +02:00
Maxime NATUREL
0a0eb08de9 Adding changelog entry 2022-06-29 15:04:57 +02:00
Maxime NATUREL
65b949071a Introducing a use case to check if a message can have reply action 2022-06-29 15:04:57 +02:00
Adam Brown
7c0d340bd0 formatting 2022-06-29 13:39:36 +01:00
Adam Brown
80ec0aaf05 removing new password isEnabled check using the isEmail validator, this check is wrong but also not needed due to associateContentStateWith above 2022-06-29 13:39:17 +01:00
Adam Brown
382a936e0a fixing method call grouping 2022-06-29 13:35:02 +01:00
Benoit Marty
f278e2884a Fix detekt issue. 2022-06-29 14:18:36 +02:00
Benoit Marty
7c4df42aa8 changelog 2022-06-29 12:24:10 +02:00
Benoit Marty
e91be2b599 Show a loader if all the Room Member are not yet loaded. 2022-06-29 12:19:17 +02:00
dependabot[bot]
93559aee63 Bump google-services from 4.3.10 to 4.3.13
Bumps google-services from 4.3.10 to 4.3.13.

---
updated-dependencies:
- dependency-name: com.google.gms:google-services
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-28 23:07:28 +00:00
Benoit Marty
98df2d82db Changelog 2022-06-28 12:03:34 +02:00
Benoit Marty
9866bfefed Ensure clearWith lambda is deleting all the list item, else we will get an infinite loop. This specific error will help to figure out what is happening. 2022-06-28 11:56:41 +02:00
Adam Brown
2a46fbe77c ignoring negative margin required by the checkbox alignment 2022-06-27 17:54:45 +01:00
Adam Brown
ef4889a1b3 fixing missing fake given 2022-06-27 15:19:29 +01:00
Adam Brown
5bb397f35e using next instead of done copy when confirming reset email link 2022-06-27 13:08:32 +01:00
Adam Brown
0a0c322963 adding extra padding between reset button and checkbox to match designs 2022-06-27 13:08:32 +01:00
Adam Brown
7971a74614 fixing code quality warnings 2022-06-27 13:08:32 +01:00
Adam Brown
71d1024fe9 adding changelog entry 2022-06-27 13:08:32 +01:00
Adam Brown
eb84072a05 updating test names to read better 2022-06-27 13:08:32 +01:00
Adam Brown
f40f838a9f making the new password visibility toggleable 2022-06-27 13:08:32 +01:00
Adam Brown
1d3b4e9829 formatting 2022-06-27 13:08:31 +01:00
Adam Brown
0d80bdfd41 updating xml ids to reflect their context 2022-06-27 13:06:43 +01:00
Adam Brown
16481df0f7 handling the reset password completion step within the view model and emitting view events to move the flow forwards 2022-06-27 13:06:42 +01:00
Adam Brown
47cedfb522 hiding the reset password input by default 2022-06-27 13:05:27 +01:00
Adam Brown
7ef8193a93 fixing reset fragments not removing previous screens when moving to the next step 2022-06-27 13:05:26 +01:00
Adam Brown
3c03bae4c5 adding test around resending reset password email 2022-06-27 12:55:51 +01:00
Adam Brown
4200b4b5e9 taking into account the servers ability to sign out all devices 2022-06-27 12:55:50 +01:00
Adam Brown
7a4a6030db adding a password entry confirmation page for the reset password flow 2022-06-27 12:54:32 +01:00
Adam Brown
9abf6e37d1 adding updated forgot password email input and breaker screens 2022-06-27 12:43:54 +01:00
Adam Brown
dcffc35041 adding forgot password button to login screen 2022-06-27 12:28:54 +01:00
NIkita Fedrunov
e1ceb227c6 trailing commas 2022-06-13 10:56:10 +02:00
NIkita Fedrunov
be51dba7cf code review fixes + changed text for group links fallback 2022-06-13 10:54:21 +02:00
Nicolas Werner
75a6986770 Remove comment referencing msc2432
Signed-off-by: Nicolas Werner <nicolas.werner@hotmail.de>
2022-06-13 09:57:33 +02:00
Nicolas Werner
d07557a5ee Fix overlooked path as well
Signed-off-by: Nicolas Werner <nicolas.werner@hotmail.de>
2022-06-12 18:05:26 +02:00
Nicolas Werner
a7bb0efc93 Add changelog
Signed-off-by: Nicolas Werner <nicolas.werner@hotmail.de>
2022-06-12 15:13:27 +02:00
Nicolas Werner
0c3ea4f923 Use stable endpoint for alias management
This increases compatibility with homeservers and allows them to remove
Element Android specific workaround.

fixes #4830
see https://github.com/ruma/ruma/pull/936
see https://github.com/matrix-org/synapse/issues/8334
see https://github.com/matrix-org/synapse/pull/9224

Signed-off-by: Nicolas Werner <nicolas.werner@hotmail.de>
2022-06-12 15:05:15 +02:00
dependabot[bot]
2d654da691 Bump realm-gradle-plugin from 10.9.0 to 10.11.0
Bumps [realm-gradle-plugin](https://github.com/realm/realm-java) from 10.9.0 to 10.11.0.
- [Release notes](https://github.com/realm/realm-java/releases)
- [Changelog](https://github.com/realm/realm-java/blob/v10.11.0/CHANGELOG.md)
- [Commits](https://github.com/realm/realm-java/compare/v10.9.0...v10.11.0)

---
updated-dependencies:
- dependency-name: io.realm:realm-gradle-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-10 13:25:27 +02:00
NIkita Fedrunov
70f100fa18 RoomListSectionBuilderSpace name changed 2022-06-09 14:22:37 +02:00
NIkita Fedrunov
fef6da5efd invite chooser fragment clean up 2022-06-09 14:21:22 +02:00
NIkita Fedrunov
85306e0e27 removed unused import 2022-06-09 13:19:33 +02:00
NIkita Fedrunov
1e59442da2 added toast for unsupported groups permalink handling 2022-06-09 13:15:02 +02:00
NIkita Fedrunov
eee10642f5 legacy groups removal 2022-06-09 13:01:55 +02:00
ericdecanini
fe27451532 Fixes lint error 2022-05-18 12:09:34 +02:00
ericdecanini
a71c50c638 Fixes lint error 2022-05-18 11:38:38 +02:00
ericdecanini
61b8053b9b Moves LoginType to sdk api package 2022-05-18 11:23:34 +02:00
ericdecanini
6a3044cb2e Fixes post merge errors 2022-05-17 14:46:02 +02:00
ericdecanini
b12549831e Merge remote-tracking branch 'origin/develop' into bugfix/eric/softlogout-ux-broken
# Conflicts:
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt
#	matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
#	vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
#	vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt
#	vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt
2022-05-17 14:24:35 +02:00
ericdecanini
92d177a68e Makes MigrateAuthTo005 class internal 2022-04-20 20:05:54 +02:00
ericdecanini
9b479ca8c0 Merge remote-tracking branch 'origin/develop' into bugfix/eric/softlogout-ux-broken 2022-04-20 18:23:27 +02:00
ericdecanini
ce579c1dd3 Merge remote-tracking branch 'origin/develop' into bugfix/eric/softlogout-ux-broken
# Conflicts:
#	vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
#	vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt
2022-03-28 15:59:50 +02:00
ericdecanini
5fcae7f4e6 Adds direct and custom types to soft logout disambiguation 2022-03-15 18:03:54 +01:00
ericdecanini
21459db634 Replaces login type unknown value with name in migration 2022-03-15 15:23:03 +01:00
ericdecanini
ef59faf160 Adds error throw for LoginActivity LoginMode Unknown 2022-03-15 15:05:33 +01:00
ericdecanini
a173accfa5 Replaces use of LoginType.value with name 2022-03-15 14:59:09 +01:00
ericdecanini
ae540297b1 Adds custom and direct login types 2022-03-15 14:53:59 +01:00
ericdecanini
70b5b9855a Removes debug global error 2022-03-09 12:26:52 +01:00
ericdecanini
ec57ff1b03 Adds attempt at SSO session restore 2022-03-09 11:07:39 +01:00
ericdecanini
6836a12557 Fixes legal comments 2022-03-07 13:49:23 +01:00
ericdecanini
04af8b2360 Changes LoginActivity onSignModeSelected SSO case 2022-03-07 13:38:30 +01:00
ericdecanini
e1f227a545 Fixes legal comments in matrix sdk files added 2022-03-07 13:14:10 +01:00
ericdecanini
0d75273121 Adds stubbing and verification for migration setRequired 2022-03-07 12:22:25 +01:00
ericdecanini
085dd943ff Fixes wrong legal comment on LoginType 2022-03-07 12:13:33 +01:00
ericdecanini
e8432f3140 Adds setRequired to migration 2022-03-07 12:07:41 +01:00
ericdecanini
22b21b8c7f Adds changelog file 2022-03-07 10:53:43 +01:00
ericdecanini
6338941885 Temporarily removes unit tests 2022-03-07 10:28:48 +01:00
ericdecanini
858923846d Adds Uri static mocking 2022-03-07 09:56:44 +01:00
ericdecanini
9bd3254e41 Removes HomeServerConnectionConfigFixture in fake adapter 2022-03-04 23:27:21 +01:00
ericdecanini
7896bf9023 Removes HomeServerConnectionConfigFixture 2022-03-04 21:36:10 +01:00
ericdecanini
2d07b80ace Fixes broken migration test 2022-03-04 21:14:27 +01:00
ericdecanini
c996f876a2 Fixes Uri EMPTY must not be null 2022-03-04 21:10:52 +01:00
ericdecanini
489670cf6b Adds validation failed test for SessionParamsCreator 2022-03-04 21:07:12 +01:00
ericdecanini
bcd802d335 Changes login types in wizards 2022-03-04 20:58:30 +01:00
ericdecanini
bb2369dad2 Fixes broken tests due to uninitialised HomeServerConnectionConfig homeServerUri 2022-03-04 15:02:03 +01:00
ericdecanini
275505b3e6 Fixes lint errors 2022-03-04 14:53:16 +01:00
ericdecanini
92f87a3a5a Adds login type handling to SoftLogoutController 2022-03-04 14:51:39 +01:00
ericdecanini
2fda593c3c Adds login types to auth flows 2022-03-04 14:29:42 +01:00
ericdecanini
32bde5a344 Adds loginType UNSUPPORTED 2022-03-04 14:24:47 +01:00
ericdecanini
ea53462107 Adds loginType to DefaultLegacySessionImporter 2022-03-03 17:54:45 +01:00
ericdecanini
40dee006dd Refactors SessionCreator with added tests 2022-03-03 17:51:50 +01:00
ericdecanini
47d5d09af2 Fixes lint errors 2022-03-03 14:46:11 +01:00
ericdecanini
d3d99dd3ba Replaces mocks with fixtures in fake json adapters 2022-03-03 13:21:04 +01:00
ericdecanini
b82efe95bd Moves test packages 2022-03-03 13:18:02 +01:00
ericdecanini
dffd568e14 Adds AuthTo005 realm migration 2022-03-03 13:14:49 +01:00
ericdecanini
d33081c349 Refactors SessionParamsMapperTest by adding fake json adapters 2022-03-03 11:47:31 +01:00
ericdecanini
187502c358 Refactors SessionParamsMapperTest by adding fake moshi 2022-03-03 11:32:23 +01:00
ericdecanini
25e73e5bd0 Adds SessionParamsMapper tests 2022-03-03 10:56:07 +01:00
ericdecanini
209a442d5b Adds LoginType to SessionParams and its entity 2022-03-03 10:55:51 +01:00
ericdecanini
12dc8a8112 Tests sso login mode 2022-03-02 15:23:39 +01:00
ericdecanini
448e8e001f Reimplements soft logout simulation 2022-03-02 14:51:22 +01:00
ericdecanini
dab866d170 Forces soft logout to be true for testing 2022-03-02 12:30:28 +01:00
yostyle
31ec8d39d8 Open url on external browser task 2021-10-28 17:36:27 +02:00
497 changed files with 6763 additions and 4627 deletions

View File

@@ -24,7 +24,7 @@ buildscript {
classpath libs.gradle.gradlePlugin
classpath libs.gradle.kotlinPlugin
classpath libs.gradle.hiltPlugin
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.3.13'
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.1.1"

1
changelog.d/5284.wip Normal file
View File

@@ -0,0 +1 @@
FTUE - Adds support for resetting the password during the FTUE onboarding journey

1
changelog.d/5398.bugfix Normal file
View File

@@ -0,0 +1 @@
Adds LoginType to SessionParams to fix soft logout form not showing for SSO and Password type

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

@@ -0,0 +1 @@
Communities/Groups are removed completely

1
changelog.d/5733.sdk Normal file
View File

@@ -0,0 +1 @@
Communities/Groups are removed completely

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

@@ -0,0 +1 @@
Improve user experience when he is first invited to a room. Users will be able to decrypt and view previous messages

1
changelog.d/6288.bugfix Normal file
View File

@@ -0,0 +1 @@
Use stable endpoint for alias management instead of MSC2432. Contributed by Nico.

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

@@ -0,0 +1 @@
Replacing Epoxy annotation layout id references with getDefaultLayoutId

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

@@ -0,0 +1 @@
Ensure `RealmList<T>.clearWith()` extension is correctly used.

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

@@ -0,0 +1 @@
[Location sharing] - Reply action on a live message

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

@@ -0,0 +1 @@
Show a loader if all the Room Members are not yet loaded.

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

@@ -0,0 +1 @@
[Poll] - Add a description under undisclosed poll when not ended

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

@@ -0,0 +1 @@
Add `android:hasFragileUserData="true"` in the manifest

1
changelog.d/6430.bugfix Normal file
View File

@@ -0,0 +1 @@
[Poll] Fixes visible and wrong votes in closed poll after removing 2 previous polls

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

@@ -0,0 +1 @@
Add code check to prevent modification of frozen class

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

@@ -0,0 +1 @@
Let your Activity or Fragment implement `VectorMenuProvider` if they provide a menu.

1
changelog.d/6442.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix HTML entities being displayed in messages

1
changelog.d/6450.bugfix Normal file
View File

@@ -0,0 +1 @@
Gallery picker can pick external images

1
changelog.d/6451.bugfix Normal file
View File

@@ -0,0 +1 @@
Fixes crash when sharing plain text, such as a url

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

@@ -0,0 +1 @@
Rename Android Service to use `AndroidService` suffix

1
changelog.d/6461.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix crashes on Timeline [Thread] due to range validation

1
changelog.d/6463.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix crashes when opening Thread

1
changelog.d/6469.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix ConcurrentModificationException on BackgroundDetectionObserver

View File

@@ -7,6 +7,7 @@ def excludes = [
'**/*Activity*',
'**/*Fragment*',
'**/*Application*',
'**/*AndroidService*',
// We would like to exclude android widgets as well but our naming is inconsistent

View File

@@ -21,7 +21,7 @@ def markwon = "4.6.2"
def moshi = "1.13.0"
def lifecycle = "2.4.1"
def flowBinding = "1.2.0"
def flipper = "0.151.1"
def flipper = "0.153.0"
def epoxy = "4.6.2"
def mavericks = "2.7.0"
def glide = "4.13.2"
@@ -29,7 +29,7 @@ def bigImageViewer = "1.8.1"
def jjwt = "0.11.5"
def vanniktechEmoji = "0.15.0"
def fragment = "1.4.1"
def fragment = "1.5.0"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@@ -50,7 +50,7 @@ ext.libs = [
],
androidx : [
'annotation' : "androidx.annotation:annotation:1.4.0",
'activity' : "androidx.activity:activity:1.4.0",
'activity' : "androidx.activity:activity:1.5.0",
'annotations' : "androidx.annotation:annotation:1.3.0",
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
'biometric' : "androidx.biometric:biometric:1.1.0",

View File

@@ -191,7 +191,7 @@ Examples of prefixes:
- `[Bugfix]`
- etc.
Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a string requirement. We prefer to spend time to add labels on issues.
Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a strong requirement. We prefer to spend time to add labels on issues.
##### PR description

View File

@@ -2,7 +2,6 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.jakewharton.butterknife'
buildscript {
repositories {
@@ -15,9 +14,6 @@ buildscript {
url 'https://repo1.maven.org/maven2'
}
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
}
}
android {

View File

@@ -29,7 +29,7 @@ import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
@EpoxyModelClass(layout = R2.layout.item_jv_base_value)
@EpoxyModelClass
internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
@EpoxyAttribute
@@ -44,6 +44,8 @@ internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var itemClickListener: View.OnClickListener? = null
override fun getDefaultLayout() = R.layout.item_jv_base_value
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.text = text?.charSequence

View File

@@ -49,7 +49,7 @@ class MediaPicker : Picker<MultiPickerBaseMediaType>() {
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
type = "video/*, image/*"
type = "*/*"
val mimeTypes = arrayOf("image/*", "video/*")
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
}

View File

@@ -53,6 +53,7 @@ android {
dependencies {
implementation libs.androidx.appCompat
implementation libs.androidx.fragmentKtx
implementation libs.google.material
// Pref theme
implementation libs.androidx.preferenceKtx

View File

@@ -18,8 +18,12 @@ package im.vector.lib.ui.styles.debug
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import im.vector.lib.ui.styles.R
@@ -31,6 +35,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupMenu()
val views = ActivityDebugMaterialThemeBinding.inflate(layoutInflater)
setContentView(views.root)
@@ -72,6 +77,27 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
}
}
private fun setupMenu() {
addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_debug, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
Toast.makeText(
this@DebugMaterialThemeActivity,
"Menu ${menuItem.title} clicked!",
Toast.LENGTH_SHORT
).show()
return true
}
},
this,
Lifecycle.State.RESUMED
)
}
private fun showTestDialog(theme: Int) {
MaterialAlertDialogBuilder(this, theme)
.setTitle("Dialog title")
@@ -82,9 +108,4 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
.setNeutralButton("Neutral", null)
.show()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_debug, menu)
return true
}
}

View File

@@ -53,6 +53,13 @@ class FlowRoom(private val room: Room) {
}
}
fun liveAreAllMembersLoaded(): Flow<Boolean> {
return room.membershipService().areAllMembersLoadedLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.membershipService().areAllMembersLoaded()
}
}
fun liveAnnotationSummary(eventId: String): Flow<Optional<EventAnnotationsSummary>> {
return room.relationService().getEventAnnotationsSummaryLive(eventId).asFlow()
.startWith(room.coroutineDispatchers.io) {

View File

@@ -26,8 +26,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.pushers.Pusher
import org.matrix.android.sdk.api.session.room.RoomSortOrder
@@ -59,13 +57,6 @@ class FlowSession(private val session: Session) {
}
}
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Flow<List<GroupSummary>> {
return session.groupService().getGroupSummariesLive(queryParams).asFlow()
.startWith(session.coroutineDispatchers.io) {
session.groupService().getGroupSummaries(queryParams)
}
}
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Flow<List<RoomSummary>> {
return session.spaceService().getSpaceSummariesLive(queryParams).asFlow()
.startWith(session.coroutineDispatchers.io) {

View File

@@ -17,7 +17,7 @@ buildscript {
}
}
dependencies {
classpath "io.realm:realm-gradle-plugin:10.9.0"
classpath "io.realm:realm-gradle-plugin:10.11.0"
}
}
@@ -60,7 +60,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.4.27\""
buildConfigField "String", "SDK_VERSION", "\"1.4.28\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View File

@@ -18,12 +18,15 @@ package org.matrix.android.sdk.common
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.Observer
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -38,7 +41,10 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult
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.toModel
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
@@ -47,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.SyncState
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit
* This class exposes methods to be used in common cases
* Registration, login, Sync, Sending messages...
*/
class CommonTestHelper private constructor(context: Context) {
class CommonTestHelper internal constructor(context: Context) {
companion object {
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
@@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) {
return sentEvents
}
fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
waitWithLatch { latch ->
retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.getRoomSummary(roomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("# TEST", "${otherSession.myUserId} can see the invite")
}
}
}
}
// not sure why it's taking so long :/
runBlockingTest(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
try {
otherSession.roomService().joinRoom(roomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
waitWithLatch {
retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(roomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
/**
* Reply in a thread
* @param room the room where to send the messages
@@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) {
)
assertNotNull(session)
return session.also {
// most of the test was created pre-MSC3061 so ensure compatibility
it.cryptoService().enableShareKeyOnInvite(false)
trackedSessions.add(session)
}
}
@@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) {
* @param latch
* @throws InterruptedException
*/
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) {
assertTrue(
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS).also {
if (!it) {
// cancel job on timeout
job?.cancel("Await timeout")
}
}
)
}
suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
while (true) {
delay(1000)
try {
delay(1000)
} catch (ex: CancellationException) {
// the job was canceled, just stop
return
}
if (condition()) {
latch.countDown()
return
@@ -447,10 +497,10 @@ class CommonTestHelper private constructor(context: Context) {
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
coroutineScope.launch(dispatcher) {
val job = coroutineScope.launch(dispatcher) {
block(latch)
}
await(latch, timeout)
await(latch, timeout, job)
}
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {

View File

@@ -53,6 +53,7 @@ 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.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/**
* @return alice session
*/
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = testHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = roomHistoryVisibility
name = "MyRoom"
})
}
if (encryptedRoom) {
testHelper.waitWithLatch { latch ->
@@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/**
* @return alice and bob sessions
*/
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestData
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2EShareKeysConfigTest : InstrumentedTest {
@Test
fun msc3061ShouldBeDisabledByDefault() = runCryptoTest(context()) { _, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
Assert.assertFalse("MSC3061 is lab and should be disabled by default", aliceSession.cryptoService().isShareKeysOnInviteEnabled())
}
@Test
fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
aliceSession.cryptoService().enableShareKeyOnInvite(false)
val roomId = commonTestHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = RoomHistoryVisibility.SHARED
name = "MyRoom"
enableEncryption()
})
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
}
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
// send some messages
val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
aliceSession.cryptoService().discardOutboundSession(roomId)
val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
// Create bob account
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
// Let alice invite bob
commonTestHelper.runBlockingTest {
roomAlice.membershipService().invite(bobSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId)
// Bob has join but should not be able to decrypt history
cryptoTestHelper.ensureCannotDecrypt(
withSession1.map { it.eventId } + withSession2.map { it.eventId },
bobSession,
roomId
)
// We don't need bob anymore
commonTestHelper.signOutAndClose(bobSession)
// Now let's enable history key sharing on alice side
aliceSession.cryptoService().enableShareKeyOnInvite(true)
// let's add a new message first
val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1)
// Worth nothing to check that the session was rotated
Assert.assertNotEquals(
"Session should have been rotated",
withSession2.first().root.content?.get("session_id")!!,
afterFlagOn.first().root.content?.get("session_id")!!
)
// Invite a new user
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam
commonTestHelper.runBlockingTest {
roomAlice.membershipService().invite(samSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
cryptoTestHelper.ensureCannotDecrypt(
withSession1.map { it.eventId } + withSession2.map { it.eventId },
samSession,
roomId
)
cryptoTestHelper.ensureCanDecrypt(
afterFlagOn.map { it.eventId },
samSession,
roomId,
afterFlagOn.map { it.root.getClearContent()?.get("body") as String })
}
@Test
fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(false)
}
val bobSession = testData.secondSession!!.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
// Bob should have shared history keys to sam.
// But has alice hasn't enabled sharing, bob shouldn't send her sessions
cryptoTestHelper.ensureCannotDecrypt(
fromAliceNotSharable.map { it.eventId },
samSession,
testData.roomId
)
cryptoTestHelper.ensureCanDecrypt(
fromBobSharable.map { it.eventId },
samSession,
testData.roomId,
fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
}
@Test
fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val bobSession = testData.secondSession!!.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
cryptoTestHelper.ensureCanDecrypt(
fromAliceNotSharable.map { it.eventId },
samSession,
testData.roomId,
fromAliceNotSharable.map { it.root.getClearContent()?.get("body") as String })
cryptoTestHelper.ensureCanDecrypt(
fromBobSharable.map { it.eventId },
samSession,
testData.roomId,
fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
}
private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1)
val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1)
// Now let bob invite Sam
// Invite a new user
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let bob invite sam
commonTestHelper.runBlockingTest {
bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId)
return Triple(fromAliceNotSharable, fromBobSharable, samSession)
}
// test flag on backup is correct
@Test
fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
aliceSession.cryptoService().enableShareKeyOnInvite(false)
val roomId = commonTestHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = RoomHistoryVisibility.SHARED
name = "MyRoom"
enableEncryption()
})
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
}
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
// send some messages
val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
aliceSession.cryptoService().enableShareKeyOnInvite(true)
val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
Log.v("#E2E TEST", "Create and start key backup for bob ...")
val keysBackupService = aliceSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = commonTestHelper.doSync<KeysVersion> {
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
commonTestHelper.waitWithLatch { latch ->
keysBackupService.backupAllGroupSessions(
null,
TestMatrixCallback(latch, true)
)
}
// signout
commonTestHelper.signOutAndClose(aliceSession)
val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
newAliceSession.cryptoService().enableShareKeyOnInvite(true)
newAliceSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> {
kbs.getVersion(version.version, it)
}
val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
null,
it
)
}
assertEquals(2, importedResult.totalNumberOfKeys)
}
// Now let's invite sam
// Invite a new user
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam
commonTestHelper.runBlockingTest {
newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
cryptoTestHelper.ensureCannotDecrypt(
notSharableMessage.map { it.eventId },
samSession,
roomId
)
cryptoTestHelper.ensureCanDecrypt(
sharableMessage.map { it.eventId },
samSession,
roomId,
sharableMessage.map { it.root.getClearContent()?.get("body") as String })
}
}

View File

@@ -23,7 +23,6 @@ import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,9 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
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.getRoomSummary
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.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail
import java.util.concurrent.CountDownLatch
// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest {
// All user should accept invite
otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
@@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest {
}
newAccount.forEach {
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
}
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
@@ -740,37 +737,6 @@ class E2eeSanityTests : InstrumentedTest {
}
}
private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
}
}
}
}
// not sure why it's taking so long :/
testHelper.runBlockingTest(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId ->

View File

@@ -0,0 +1,424 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertNotEquals
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2eeShareKeysHistoryTest : InstrumentedTest {
@Test
fun testShareMessagesHistoryWithRoomWorldReadable() {
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE)
}
@Test
fun testShareMessagesHistoryWithRoomShared() {
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED)
}
@Test
fun testShareMessagesHistoryWithRoomJoined() {
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED)
}
@Test
fun testShareMessagesHistoryWithRoomInvited() {
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED)
}
/**
* In this test we create a room and test that new members
* can decrypt history when the room visibility is
* RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE.
* We should not be able to view messages/decrypt otherwise
*/
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob
val bobSession = cryptoTestData.secondSession!!.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
Assert.assertTrue("Message should be sent", aliceMessageId != null)
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
// Bob should be able to decrypt the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
if (it) {
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
}
}
}
// Create a new user
val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also {
it.cryptoService().enableShareKeyOnInvite(true)
}
Log.v("#E2E TEST", "Aris user created")
// Alice invites new user to the room
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
aliceRoomPOV.membershipService().invite(arisSession.myUserId)
}
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper)
Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID")
when (roomHistoryVisibility) {
RoomHistoryVisibility.WORLD_READABLE,
RoomHistoryVisibility.SHARED,
null
-> {
// Aris should be able to decrypt the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
).also {
if (it) {
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
}
}
}
}
RoomHistoryVisibility.INVITED,
RoomHistoryVisibility.JOINED -> {
// Aris should not even be able to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
timelineEvent == null
}
}
}
}
testHelper.signOutAndClose(arisSession)
cryptoTestData.cleanUp(testHelper)
}
@Test
fun testNeedsRotationFromWorldReadableToShared() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
}
@Test
fun testNeedsRotationFromWorldReadableToInvited() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
}
@Test
fun testNeedsRotationFromWorldReadableToJoined() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
}
@Test
fun testNeedsRotationFromSharedToWorldReadable() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
}
@Test
fun testNeedsRotationFromSharedToInvited() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited"))
}
@Test
fun testNeedsRotationFromSharedToJoined() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined"))
}
@Test
fun testNeedsRotationFromInvitedToShared() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
}
@Test
fun testNeedsRotationFromInvitedToWorldReadable() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
}
@Test
fun testNeedsRotationFromInvitedToJoined() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
}
@Test
fun testNeedsRotationFromJoinedToShared() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
}
@Test
fun testNeedsRotationFromJoinedToInvited() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
}
@Test
fun testNeedsRotationFromJoinedToWorldReadable() {
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
}
/**
* In this test we will test that a rotation is needed when
* When the room's history visibility setting changes to world_readable or shared
* from invited or joined, or changes to invited or joined from world_readable or shared,
* senders that support this flag must rotate their megolm sessions.
*/
private fun testRotationDueToVisibilityChange(
initRoomHistoryVisibility: RoomHistoryVisibility,
nextRoomHistoryVisibility: RoomHistoryVisibilityContent
) {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true)
}
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
// Bob
val bobSession = cryptoTestData.secondSession!!
val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID")
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
Assert.assertTrue("Message should be sent", aliceMessageId != null)
Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID")
// Bob should be able to decrypt the message
var firstAliceMessageMegolmSessionId: String? = null
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
if (it) {
firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
Log.v(
"#E2E TEST",
"Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
)
}
}
}
}
Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
var secondAliceMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(secondMessage)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
if (it) {
secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
Log.v(
"#E2E TEST",
"Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
)
}
}
}
}
}
assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId)
Log.v("#E2E TEST ROTATION", "No rotation needed yet")
// Let's change the room history visibility
testHelper.runBlockingTest {
aliceRoomPOV.stateService()
.sendStateEvent(
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
body = RoomHistoryVisibilityContent(
historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
).toContent()
)
}
// ensure that the state did synced down
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
.stateService()
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
?.content
?.toModel<RoomHistoryVisibilityContent>()
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
}
var aliceThirdMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(thirdMessage)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
if (it) {
aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
}
}
}
}
}
when {
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
Log.v("#E2E TEST ROTATION", "Rotation is not needed")
}
initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> {
assertNotEquals("Session should have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
Log.v("#E2E TEST ROTATION", "Rotation is needed!")
}
}
cryptoTestData.cleanUp(testHelper)
}
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
}
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
otherAccounts.map {
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
it == Membership.JOIN
}
}
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
}
}
}
}
testHelper.runBlockingTest(60_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
}

View File

@@ -72,7 +72,7 @@ class PreShareKeysTest : InstrumentedTest {
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)

View File

@@ -19,14 +19,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestData
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
/**
* Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
*/
internal data class KeysBackupScenarioData(
val cryptoTestData: CryptoTestData,
val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
val aliceKeys: List<MXInboundMegolmSessionWrapper>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session
) {

View File

@@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest {
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session)
val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
assertNotNull(keyBackupData)
assertNotNull(keyBackupData!!.sessionData)
@@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest {
val sessionData = keysBackup
.decryptKeyBackupData(
keyBackupData,
session.olmInboundGroupSession!!.sessionIdentifier(),
session.safeSessionId!!,
cryptoTestData.roomId,
decryption!!
)

View File

@@ -187,7 +187,7 @@ internal class KeysBackupTestHelper(
// - Alice must have the same keys on both devices
for (aliceKey1 in testData.aliceKeys) {
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
.getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
.getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
Assert.assertNotNull(aliceKey2)
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
}

View File

@@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest {
val roomName = "My Space"
val topic = "A public space for test"
var spaceId: String = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
// wait a bit to let the summary update it self :/
it.countDown()
}
Thread.sleep(4_000)
val syncedSpace = session.spaceService().getSpace(spaceId)
commonTestHelper.waitWithLatch {
commonTestHelper.retryPeriodicallyWithLatch(it) {
syncedSpace?.asRoom()?.roomSummary()?.name != null
session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null
}
}
val syncedSpace = session.spaceService().getSpace(spaceId)
assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name)
assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")

View File

@@ -20,7 +20,6 @@ import android.util.Log
import androidx.lifecycle.Observer
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Ignore
@@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest {
val spaceName = "My Space"
val topic = "A public space for test"
var spaceId = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
it.countDown()
}
val syncedSpace = session.spaceService().getSpace(spaceId)
var roomId = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
it.countDown()
}
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
syncedSpace!!.addChildren(roomId, viaServers, null, true)
it.countDown()
}
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
it.countDown()
}
Thread.sleep(9000)
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
parents?.forEach {
Log.d("## TEST", "parent : $it")
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
parents?.forEach {
Log.d("## TEST", "parent : $it")
}
parents?.size == 1 &&
parents.first().roomSummary?.name == spaceName &&
canonicalParents?.size == 1 &&
canonicalParents.first().roomSummary?.name == spaceName
}
}
assertNotNull(parents)
assertEquals(1, parents!!.size)
assertEquals(spaceName, parents.first().roomSummary?.name)
assertNotNull(canonicalParents)
assertEquals(1, canonicalParents!!.size)
assertEquals(spaceName, canonicalParents.first().roomSummary?.name)
}
// @Test
@@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest {
// }
@Test
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
commonTestHelper,
session, "SpaceA",
listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
/* val spaceBInfo = */ createPublicSpace(
session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
commonTestHelper,
session, "SpaceB",
listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
commonTestHelper,
session, "SpaceC",
listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
)
// add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
it.countDown()
}
// Create orphan rooms
var orphan1 = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
it.countDown()
}
var orphan2 = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
it.countDown()
}
val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
@@ -240,10 +235,9 @@ class SpaceHierarchyTest : InstrumentedTest {
assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" })
// Add a non canonical child and check that it does not appear as orphan
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
spaceA!!.addChildren(a3, viaServers, null, false)
it.countDown()
}
Thread.sleep(6_000)
@@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest {
@Test
@Ignore("This test will be ignored until it is fixed")
fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
fun testBreakCycle() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
commonTestHelper,
session, "SpaceA",
listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
commonTestHelper,
session, "SpaceC",
listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
)
// add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
it.countDown()
}
// add back A as subspace of C
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
it.countDown()
}
// A -> C -> A
@@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest {
}
@Test
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
commonTestHelper,
session,
"SpaceA",
listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
commonTestHelper,
session,
"SpaceB",
listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
// add B as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
runBlocking {
commonTestHelper.runBlockingTest {
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
commonTestHelper,
session,
"SpaceC",
listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
)
commonTestHelper.waitWithLatch { latch ->
@@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest {
}
}
flatAChildren.observeForever(childObserver)
// add C as subspace of B
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
// C1 and C2 should be in flatten child of A now
flatAChildren.observeForever(childObserver)
}
// Test part one of the rooms
@@ -374,10 +379,10 @@ class SpaceHierarchyTest : InstrumentedTest {
}
}
// part from b room
session.roomService().leaveRoom(bRoomId)
// The room should have disapear from flat children
flatAChildren.observeForever(childObserver)
// part from b room
session.roomService().leaveRoom(bRoomId)
}
commonTestHelper.signOutAndClose(session)
}
@@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
)
private fun createPublicSpace(
commonTestHelper: CommonTestHelper,
session: Session,
spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>>
@@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult {
var spaceId = ""
var roomIds: List<String> = emptyList()
runSessionTest(context()) { commonTestHelper ->
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
}
latch.countDown()
}
}
return TestSpaceCreationResult(spaceId, roomIds)
}
private fun createPrivateSpace(
commonTestHelper: CommonTestHelper,
session: Session,
spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>>
@@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
): TestSpaceCreationResult {
var spaceId = ""
var roomIds: List<String> = emptyList()
runSessionTest(context()) { commonTestHelper ->
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds =
childInfo.map { entry ->
val homeServerCapabilities = session
.homeServerCapabilitiesService()
.getHomeServerCapabilities()
session.roomService().createRoom(CreateRoomParams().apply {
name = entry.first
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
)
)
})
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds =
childInfo.map { entry ->
val homeServerCapabilities = session
.homeServerCapabilitiesService()
.getHomeServerCapabilities()
session.roomService().createRoom(CreateRoomParams().apply {
name = entry.first
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
)
)
})
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
latch.countDown()
}
}
return TestSpaceCreationResult(spaceId, roomIds)
@@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
/* val spaceAInfo = */ createPublicSpace(
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
commonTestHelper,
session, "SpaceA",
listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
commonTestHelper,
session, "SpaceB",
listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
commonTestHelper,
session, "SpaceC",
listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/),
Triple("C2", true, true)
)
)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
@@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest {
runBlocking {
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
Thread.sleep(6_000)
}
// Thread.sleep(4_000)
@@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest {
// + C
// + c1, c2
val rootSpaces = commonTestHelper.runBlockingTest {
session.spaceService().getRootSpaceSummaries()
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() }
rootSpaces.size == 2
}
}
assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
}
@Test
@@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest {
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
val spaceAInfo = createPrivateSpace(
aliceSession, "Private Space A", listOf(
Triple("General", true /*suggested*/, true/*canonical*/),
Triple("Random", true, true)
)
commonTestHelper,
aliceSession, "Private Space A",
listOf(
Triple("General", true /*suggested*/, true/*canonical*/),
Triple("Random", true, true)
)
)
commonTestHelper.runBlockingTest {
@@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest {
}
var bobRoomId = ""
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
it.countDown()
}
commonTestHelper.runBlockingTest {
@@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest {
}
}
commonTestHelper.waitWithLatch {
commonTestHelper.runBlockingTest {
bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
it.countDown()
}
commonTestHelper.waitWithLatch { latch ->

View File

@@ -0,0 +1,38 @@
/*
* 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.api.auth
enum class LoginType {
PASSWORD,
SSO,
UNSUPPORTED,
CUSTOM,
DIRECT,
UNKNOWN;
companion object {
fun fromName(name: String) = when (name) {
PASSWORD.name -> PASSWORD
SSO.name -> SSO
UNSUPPORTED.name -> UNSUPPORTED
CUSTOM.name -> CUSTOM
DIRECT.name -> DIRECT
else -> UNKNOWN
}
}
}

View File

@@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.auth.data
import org.matrix.android.sdk.api.auth.LoginType
/**
* This data class holds necessary data to open a session.
* You don't have to manually instantiate it.
@@ -34,7 +36,12 @@ data class SessionParams(
/**
* Set to false if the current token is not valid anymore. Application should not have to use this info.
*/
val isTokenValid: Boolean
val isTokenValid: Boolean,
/**
* The authentication method that was used to create the session.
*/
val loginType: LoginType,
) {
/*
* Shortcuts. Usually the application should only need to use these shortcuts

View File

@@ -38,4 +38,5 @@ data class MXCryptoConfig constructor(
* You can limit request only to your sessions by turning this setting to `true`
*/
val limitRoomKeyRequestsToMyDevices: Boolean = false,
)
)

View File

@@ -162,7 +162,7 @@ enum class ApiPath(val path: String, val method: String) {
KICK_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick", "POST"),
REDACT_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}", "PUT"),
REPORT_CONTENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}", "POST"),
GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases", "GET"),
GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/aliases", "GET"),
SEND_TYPING_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}", "PUT"),
PUT_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "PUT"),
DELETE_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "DELETE"),

View File

@@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.EventService
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.group.GroupService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.identity.IdentityService
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
@@ -154,11 +153,6 @@ interface Session {
*/
fun roomDirectoryService(): RoomDirectoryService
/**
* Returns the GroupService associated with the session.
*/
fun groupService(): GroupService
/**
* Returns the UserService associated with the session.
*/

View File

@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
interface CryptoService {
@@ -84,6 +85,20 @@ interface CryptoService {
fun isKeyGossipingEnabled(): Boolean
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun enableShareKeyOnInvite(enable: Boolean)
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun isShareKeysOnInviteEnabled(): Boolean
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int
@@ -176,4 +191,9 @@ interface CryptoService {
* send, in order to speed up sending of the message.
*/
fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>)
/**
* Share all inbound sessions of the last chunk messages to the provided userId devices.
*/
suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?)
}

View File

@@ -69,5 +69,11 @@ data class ForwardedRoomKeyContent(
* private part of this key unless they have done device verification.
*/
@Json(name = "sender_claimed_ed25519_key")
val senderClaimedEd25519Key: String? = null
val senderClaimedEd25519Key: String? = null,
/**
* MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
*/
@Json(name = "org.matrix.msc3061.shared_history")
val sharedHistory: Boolean? = false,
)

View File

@@ -38,5 +38,12 @@ data class RoomKeyContent(
// should be a Long but it is sometimes a double
@Json(name = "chain_index")
val chainIndex: Any? = null
val chainIndex: Any? = null,
/**
* MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
*/
@Json(name = "org.matrix.msc3061.shared_history")
val sharedHistory: Boolean? = false
)

View File

@@ -1,31 +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.group
/**
* This interface defines methods to interact within a group.
*/
interface Group {
val groupId: String
/**
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
* The SDK also takes care of refreshing group data every hour.
* @return a Cancelable to be able to cancel requests.
*/
suspend fun fetchGroupData()
}

View File

@@ -1,51 +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.group
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.group.model.GroupSummary
/**
* This interface defines methods to get groups. It's implemented at the session level.
*/
interface GroupService {
/**
* Get a group from a groupId.
* @param groupId the groupId to look for.
* @return the group with groupId or null
*/
fun getGroup(groupId: String): Group?
/**
* Get a groupSummary from a groupId.
* @param groupId the groupId to look for.
* @return the groupSummary with groupId or null
*/
fun getGroupSummary(groupId: String): GroupSummary?
/**
* Get a list of group summaries. This list is a snapshot of the data.
* @return the list of [GroupSummary]
*/
fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary>
/**
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary]
*/
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
}

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.api.session.group
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.room.model.Membership
fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams {
return GroupSummaryQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter group summaries.
*/
data class GroupSummaryQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = GroupSummaryQueryParams(
displayName = displayName,
memberships = memberships
)
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.group.model
import org.matrix.android.sdk.api.session.room.model.Membership
/**
* This class holds some data of a group.
* It can be retrieved through [org.matrix.android.sdk.api.session.group.GroupService]
*/
data class GroupSummary(
val groupId: String,
val membership: Membership,
val displayName: String = "",
val shortDescription: String = "",
val avatarUrl: String = "",
val roomIds: List<String> = emptyList(),
val userIds: List<String> = emptyList()
)

View File

@@ -54,7 +54,5 @@ sealed class PermalinkData {
data class UserLink(val userId: String) : PermalinkData()
data class GroupLink(val groupId: String) : PermalinkData()
data class FallbackLink(val uri: Uri) : PermalinkData()
data class FallbackLink(val uri: Uri, val isLegacyGroupLink: Boolean = false) : PermalinkData()
}

View File

@@ -61,27 +61,29 @@ object PermalinkParser {
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX)
.filter { it.isNotEmpty() }
.map { URLDecoder.decode(it, "UTF-8") }
.take(2)
val decodedParams = params
.map { URLDecoder.decode(it, "UTF-8") }
val identifier = params.getOrNull(0)
val extraParameter = params.getOrNull(1)
val decodedIdentifier = decodedParams.getOrNull(0)
val extraParameter = decodedParams.getOrNull(1)
return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
handleRoomIdCase(fragment, identifier, matrixToUri, extraParameter, viaQueryParameters)
identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier)
MatrixPatterns.isRoomId(decodedIdentifier) -> {
handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters)
}
MatrixPatterns.isRoomAlias(identifier) -> {
MatrixPatterns.isRoomAlias(decodedIdentifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
roomIdOrAlias = decodedIdentifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
}
else -> PermalinkData.FallbackLink(uri)
else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier))
}
}

View File

@@ -48,9 +48,10 @@ interface LocationSharingService {
/**
* Starts sharing live location in the room.
* @param timeoutMillis timeout of the live in milliseconds
* @param description description of the live for text fallback
* @return the result of the update of the live
*/
suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult
/**
* Stops sharing live location in the room.

View File

@@ -30,6 +30,20 @@ interface MembershipService {
*/
suspend fun loadRoomMembersIfNeeded()
/**
* All the room members can be not loaded, for instance after an initial sync.
* All the members will be loaded when calling [loadRoomMembersIfNeeded], or when sending an encrypted
* event to the room.
* The fun let the app know if all the members have been loaded for this room.
* @return true if all the members are loaded, or false elsewhere.
*/
suspend fun areAllMembersLoaded(): Boolean
/**
* Live version for [areAllMembersLoaded].
*/
fun areAllMembersLoadedLive(): LiveData<Boolean>
/**
* Return the roomMember with userId or null.
* @param userId the userId param to look for

View File

@@ -48,3 +48,9 @@ enum class RoomHistoryVisibility {
*/
@Json(name = "joined") JOINED
}
/**
* Room history should be shared only if room visibility is world_readable or shared.
*/
internal fun RoomHistoryVisibility.shouldShareHistory() =
this == RoomHistoryVisibility.WORLD_READABLE || this == RoomHistoryVisibility.SHARED

View File

@@ -22,7 +22,6 @@ enum class InitialSyncStep {
ImportingAccount,
ImportingAccountCrypto,
ImportingAccountRoom,
ImportingAccountGroups,
ImportingAccountData,
ImportingAccountJoinedRooms,
ImportingAccountInvitedRooms,

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.sync.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GroupSyncProfile(
/**
* The name of the group, if any. May be nil.
*/
@Json(name = "name") val name: String? = null,
/**
* The URL for the group's avatar. May be nil.
*/
@Json(name = "avatar_url") val avatarUrl: String? = null
)

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.sync.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GroupsSyncResponse(
/**
* Joined groups: An array of groups ids.
*/
@Json(name = "join") val join: Map<String, Any> = emptyMap(),
/**
* Invitations. The groups that the user has been invited to: keys are groups ids.
*/
@Json(name = "invite") val invite: Map<String, InvitedGroupSync> = emptyMap(),
/**
* Left groups. An array of groups ids: the groups that the user has left or been banned from.
*/
@Json(name = "leave") val leave: Map<String, Any> = emptyMap()
)

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.sync.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class InvitedGroupSync(
/**
* The identifier of the inviter.
*/
@Json(name = "inviter") val inviter: String? = null,
/**
* The group profile.
*/
@Json(name = "profile") val profile: GroupSyncProfile? = null
)

View File

@@ -65,10 +65,4 @@ data class SyncResponse(
*/
@Json(name = "org.matrix.msc2732.device_unused_fallback_key_types")
val deviceUnusedFallbackKeyTypes: List<String>? = null,
/**
* List of groups.
*/
@Json(name = "groups") val groups: GroupsSyncResponse? = null
)

View File

@@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
@@ -113,19 +112,6 @@ sealed class MatrixItem(
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
data class GroupItem(
override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null
) :
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
protected fun checkId() {
if (!id.startsWith(getIdPrefix())) {
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
@@ -144,7 +130,6 @@ sealed class MatrixItem(
is RoomItem,
is EveryoneInRoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
@@ -196,8 +181,6 @@ sealed class MatrixItem(
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(roomId, displayName, avatarUrl)
} else {

View File

@@ -83,6 +83,9 @@ internal abstract class AuthModule {
@Binds
abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
@Binds
abstract fun bindSessionParamsCreator(creator: DefaultSessionParamsCreator): SessionParamsCreator
@Binds
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask

View File

@@ -22,6 +22,7 @@ import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
@@ -361,7 +362,7 @@ internal class DefaultAuthenticationService @Inject constructor(
homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials
): Session {
return sessionCreator.createSession(credentials, homeServerConnectionConfig)
return sessionCreator.createSession(credentials, homeServerConnectionConfig, LoginType.SSO)
}
override suspend fun getWellKnownData(

View File

@@ -16,69 +16,41 @@
package org.matrix.android.sdk.internal.auth
import android.net.Uri
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.SessionManager
import timber.log.Timber
import javax.inject.Inject
internal interface SessionCreator {
suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
suspend fun createSession(
credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
loginType: LoginType,
): Session
}
internal class DefaultSessionCreator @Inject constructor(
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
private val pendingSessionStore: PendingSessionStore,
private val isValidClientServerApiTask: IsValidClientServerApiTask
private val sessionParamsCreator: SessionParamsCreator,
) : SessionCreator {
/**
* Credentials can affect the homeServerConnectionConfig, override homeserver url and/or
* identity server url if provided in the credentials.
*/
override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
override suspend fun createSession(
credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
loginType: LoginType,
): Session {
// We can cleanup the pending session params
pendingSessionStore.delete()
val overriddenUrl = credentials.discoveryInformation?.homeServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
// It can be the same value, so in this case, do not check again the validity
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
?.let { Uri.parse(it) }
?.takeIf {
// Validate the URL, if the configuration is wrong server side, do not override
tryOrNull {
isValidClientServerApiTask.execute(
IsValidClientServerApiTask.Params(
homeServerConnectionConfig.copy(homeServerUriBase = it)
)
)
.also { Timber.d("Overriding homeserver url: $it") }
} ?: true // In case of other error (no network, etc.), consider it is valid...
}
val sessionParams = SessionParams(
credentials = credentials,
homeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUriBase = overriddenUrl ?: homeServerConnectionConfig.homeServerUriBase,
identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri
),
isTokenValid = true)
val sessionParams = sessionParamsCreator.create(credentials, homeServerConnectionConfig, loginType)
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
}

View File

@@ -0,0 +1,83 @@
/*
* 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.auth
import android.net.Uri
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryOrNull
import timber.log.Timber
import javax.inject.Inject
internal interface SessionParamsCreator {
suspend fun create(
credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
loginType: LoginType,
): SessionParams
}
internal class DefaultSessionParamsCreator @Inject constructor(
private val isValidClientServerApiTask: IsValidClientServerApiTask
) : SessionParamsCreator {
override suspend fun create(
credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
loginType: LoginType,
) = SessionParams(
credentials = credentials,
homeServerConnectionConfig = homeServerConnectionConfig.overrideWithCredentials(credentials),
isTokenValid = true,
loginType = loginType,
)
private suspend fun HomeServerConnectionConfig.overrideWithCredentials(credentials: Credentials) = copy(
homeServerUriBase = credentials.getHomeServerUri(this) ?: homeServerUriBase,
identityServerUri = credentials.getIdentityServerUri() ?: identityServerUri
)
private suspend fun Credentials.getHomeServerUri(homeServerConnectionConfig: HomeServerConnectionConfig) =
discoveryInformation?.homeServer?.baseURL
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
// It can be the same value, so in this case, do not check again the validity
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
?.let { Uri.parse(it) }
?.takeIf { validateUri(it, homeServerConnectionConfig) }
private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
// Validate the URL, if the configuration is wrong server side, do not override
tryOrNull {
performClientServerApiValidation(uri, homeServerConnectionConfig)
} ?: true // In case of other error (no network, etc.), consider it is valid...
private suspend fun performClientServerApiValidation(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
isValidClientServerApiTask.execute(
IsValidClientServerApiTask.Params(homeServerConnectionConfig.copy(homeServerUriBase = uri))
).also { Timber.d("Overriding homeserver url: $it") }
private fun Credentials.getIdentityServerUri() = discoveryInformation?.identityServer?.baseURL
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
}

View File

@@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo001
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo002
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo004
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo005
import timber.log.Timber
import javax.inject.Inject
@@ -33,7 +34,7 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
override fun equals(other: Any?) = other is AuthRealmMigration
override fun hashCode() = 4000
val schemaVersion = 4L
val schemaVersion = 5L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@@ -42,5 +43,6 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
if (oldVersion < 2) MigrateAuthTo002(realm).perform()
if (oldVersion < 3) MigrateAuthTo003(realm).perform()
if (oldVersion < 4) MigrateAuthTo004(realm).perform()
if (oldVersion < 5) MigrateAuthTo005(realm).perform()
}
}

View File

@@ -26,5 +26,6 @@ internal open class SessionParamsEntity(
var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
var isTokenValid: Boolean = true,
var loginType: String = "",
) : RealmObject()

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth.db
import com.squareup.moshi.Moshi
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.SessionParams
@@ -37,7 +38,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) {
return null
}
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid, LoginType.fromName(entity.loginType))
}
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@@ -54,7 +55,8 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
sessionParams.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid
sessionParams.isTokenValid,
sessionParams.loginType.name,
)
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.auth.db.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
import timber.log.Timber
internal class MigrateAuthTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
override fun doMigrate(realm: DynamicRealm) {
Timber.d("Update SessionParamsEntity to add LoginType")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java)
?.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true)
?.transform { it.set(SessionParamsEntityFields.LOGIN_TYPE, LoginType.UNKNOWN.name) }
}
}

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth.login
import android.util.Patterns
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -78,7 +79,7 @@ internal class DefaultLoginWizard(
authAPI.login(loginParams)
}
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.PASSWORD)
}
/**
@@ -92,7 +93,7 @@ internal class DefaultLoginWizard(
authAPI.login(loginParams)
}
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.SSO)
}
override suspend fun loginCustom(data: JsonDict): Session {
@@ -100,7 +101,7 @@ internal class DefaultLoginWizard(
authAPI.login(data)
}
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.CUSTOM)
}
override suspend fun resetPassword(email: String) {

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.login
import dagger.Lazy
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
@@ -77,7 +78,7 @@ internal class DefaultDirectLoginTask @Inject constructor(
}
}
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig, LoginType.DIRECT)
}
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.auth.registration
import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -36,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
* This class execute the registration request and is responsible to keep the session of interactive authentication.
*/
internal class DefaultRegistrationWizard(
authAPI: AuthAPI,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
authAPI: AuthAPI,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
@@ -64,7 +65,7 @@ internal class DefaultRegistrationWizard(
override suspend fun getRegistrationFlow(): RegistrationResult {
val params = RegistrationParams()
return performRegistrationRequest(params)
return performRegistrationRequest(params, LoginType.PASSWORD)
}
override suspend fun createAccount(
@@ -73,43 +74,43 @@ internal class DefaultRegistrationWizard(
initialDeviceDisplayName: String?
): RegistrationResult {
val params = RegistrationParams(
username = userName,
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
username = userName,
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
)
return performRegistrationRequest(params)
.also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
.also { pendingSessionStore.savePendingSessionData(it) }
}
return performRegistrationRequest(params, LoginType.PASSWORD)
.also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
.also { pendingSessionStore.savePendingSessionData(it) }
}
}
override suspend fun performReCaptcha(response: String): RegistrationResult {
val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first")
?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
return performRegistrationRequest(params)
return performRegistrationRequest(params, LoginType.PASSWORD)
}
override suspend fun acceptTerms(): RegistrationResult {
val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first")
?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
return performRegistrationRequest(params)
return performRegistrationRequest(params, LoginType.PASSWORD)
}
override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
.also { pendingSessionStore.savePendingSessionData(it) }
.also { pendingSessionStore.savePendingSessionData(it) }
return sendThreePid(threePid)
}
override suspend fun sendAgainThreePid(): RegistrationResult {
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
?: throw IllegalStateException("developer error, call createAccount() method first")
?: throw IllegalStateException("developer error, call createAccount() method first")
return sendThreePid(safeCurrentThreePid)
}
@@ -125,7 +126,7 @@ internal class DefaultRegistrationWizard(
)
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
.also { pendingSessionStore.savePendingSessionData(it) }
val params = RegistrationParams(
auth = if (threePid is RegisterThreePid.Email) {
@@ -148,17 +149,17 @@ internal class DefaultRegistrationWizard(
)
// Store data
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
.also { pendingSessionStore.savePendingSessionData(it) }
.also { pendingSessionStore.savePendingSessionData(it) }
// and send the sid a first time
return performRegistrationRequest(params)
return performRegistrationRequest(params, LoginType.PASSWORD)
}
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
val safeParam = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid")
?: throw IllegalStateException("developer error, no pending three pid")
return performRegistrationRequest(safeParam, delayMillis)
return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis)
}
override suspend fun handleValidateThreePid(code: String): RegistrationResult {
@@ -167,19 +168,19 @@ internal class DefaultRegistrationWizard(
private suspend fun validateThreePid(code: String): RegistrationResult {
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid")
?: throw IllegalStateException("developer error, no pending three pid")
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
val validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
code = code
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
code = code
)
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
if (validationResponse.isSuccess()) {
// The entered code is correct
// Same than validate email
return performRegistrationRequest(registrationParams, 3_000)
return performRegistrationRequest(registrationParams, LoginType.PASSWORD, 3_000)
} else {
// The code is not correct
throw Failure.SuccessError
@@ -188,10 +189,10 @@ internal class DefaultRegistrationWizard(
override suspend fun dummy(): RegistrationResult {
val safeSession = pendingSessionData.currentSession
?: throw IllegalStateException("developer error, call createAccount() method first")
?: throw IllegalStateException("developer error, call createAccount() method first")
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
return performRegistrationRequest(params)
return performRegistrationRequest(params, LoginType.PASSWORD)
}
override suspend fun registrationCustom(
@@ -204,25 +205,28 @@ internal class DefaultRegistrationWizard(
mutableParams["session"] = safeSession
val params = RegistrationCustomParams(auth = mutableParams)
return performRegistrationOtherRequest(params)
return performRegistrationOtherRequest(LoginType.CUSTOM, params)
}
private suspend fun performRegistrationRequest(
registrationParams: RegistrationParams,
loginType: LoginType,
delayMillis: Long = 0
): RegistrationResult {
delay(delayMillis)
return register { registerTask.execute(RegisterTask.Params(registrationParams)) }
return register(loginType) { registerTask.execute(RegisterTask.Params(registrationParams)) }
}
private suspend fun performRegistrationOtherRequest(
registrationCustomParams: RegistrationCustomParams
loginType: LoginType,
registrationCustomParams: RegistrationCustomParams,
): RegistrationResult {
return register { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
return register(loginType) { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
}
private suspend fun register(
execute: suspend () -> Credentials
loginType: LoginType,
execute: suspend () -> Credentials,
): RegistrationResult {
val credentials = try {
execute.invoke()
@@ -237,8 +241,7 @@ internal class DefaultRegistrationWizard(
}
}
val session =
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, loginType)
return RegistrationResult.Success(session)
}

View File

@@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
@@ -81,6 +82,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFact
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.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -963,8 +965,12 @@ internal class DefaultCryptoService @Inject constructor(
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
eventContent?.historyVisibility?.let {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
val historyVisibility = eventContent?.historyVisibility
if (historyVisibility == null) {
cryptoStore.setShouldShareHistory(roomId, false)
} else {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
}
}
@@ -1111,6 +1117,10 @@ internal class DefaultCryptoService @Inject constructor(
override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled()
override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable)
/**
* Tells whether the client should ever send encrypted messages to unverified devices.
* The default value is false.
@@ -1335,6 +1345,30 @@ internal class DefaultCryptoService @Inject constructor(
}
}
override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?) {
deviceListManager.downloadKeys(listOf(userId), false)
val userDevices = cryptoStore.getUserDeviceList(userId)
val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo ->
// Get inbound session from sessionId and sessionKey
withContext(coroutineDispatchers.crypto) {
olmDevice.getInboundGroupSession(
sessionId = sessionInfo.sessionId,
senderKey = sessionInfo.senderKey,
roomId = roomId
).takeIf { it.wrapper.sessionData.sharedHistory }
}
}
userDevices?.forEach { deviceInfo ->
// Lets share the provided inbound sessions for every user device
sessionToShare.forEach { inboundGroupSession ->
val encryptor = roomEncryptorsStore.get(roomId)
encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo)
Timber.i("## CRYPTO | Sharing inbound session")
}
}
}
/* ==========================================================================================
* For test only
* ========================================================================================== */

View File

@@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.Mutex
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import java.util.Timer
@@ -31,7 +31,7 @@ import java.util.TimerTask
import javax.inject.Inject
internal data class InboundGroupSessionHolder(
val wrapper: OlmInboundGroupSessionWrapper2,
val wrapper: MXInboundMegolmSessionWrapper,
val mutex: Mutex = Mutex()
)
@@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor(
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.olmInboundGroupSession?.releaseSession()
oldValue.wrapper.session.releaseSession()
}
}
}
@@ -67,7 +67,7 @@ internal class InboundGroupSessionStore @Inject constructor(
private val timer = Timer()
private var timerTask: TimerTask? = null
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
@Synchronized
fun clear() {
@@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor(
@Synchronized
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
dirtySession.remove(old.wrapper)
dirtySession.remove(old)
store.removeInboundGroupSession(sessionId, senderKey)
sessionCache.remove(CacheKey(sessionId, senderKey))
// release removed session
old.wrapper.olmInboundGroupSession?.releaseSession()
old.wrapper.session.releaseSession()
internalStoreGroupSession(new, sessionId, senderKey)
}
@@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor(
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
// We want to batch this a bit for performances
dirtySession.add(holder.wrapper)
dirtySession.add(holder)
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert
@@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor(
@Synchronized
private fun batchSave() {
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
val toSave = mutableListOf<InboundGroupSessionHolder>().apply { addAll(dirtySession) }
dirtySession.clear()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
tryOrNull {
store.storeInboundGroupSessions(toSave)
store.storeInboundGroupSessions(toSave.map { it.wrapper })
}
}
}

View File

@@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -38,6 +39,7 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException
import org.matrix.olm.OlmInboundGroupSession
import org.matrix.olm.OlmMessage
import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession
@@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor(
return MXOutboundSessionInfo(
sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
clock,
restoredOutboundGroupSession.creationTime
clock = clock,
creationTime = restoredOutboundGroupSession.creationTime,
sharedHistory = restoredOutboundGroupSession.sharedHistory
)
}
return null
@@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor(
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
* @param keysClaimed Other keys the sender claims.
* @param exportFormat true if the megolm keys are in export format
* @param sharedHistory MSC3061, this key is sharable on invite
* @return true if the operation succeeds.
*/
fun addInboundGroupSession(
sessionId: String,
sessionKey: String,
roomId: String,
senderKey: String,
forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>,
exportFormat: Boolean
): AddSessionResult {
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
fun addInboundGroupSession(sessionId: String,
sessionKey: String,
roomId: String,
senderKey: String,
forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>,
exportFormat: Boolean,
sharedHistory: Boolean): AddSessionResult {
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
if (exportFormat) {
OlmInboundGroupSession.importSession(sessionKey)
} else {
OlmInboundGroupSession(sessionKey)
}
}
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper
// If we have an existing one we should check if the new one is not better
if (existingSession != null) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
try {
val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
// This is quite unexpected, could throw if native was released?
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
candidateSession.olmInboundGroupSession?.releaseSession()
candidateSession?.releaseSession()
// Probably should discard it?
}
val newKnownFirstIndex = candidateSession.firstKnownIndex
val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex }
// If our existing session is better we keep it
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
candidateSession.olmInboundGroupSession?.releaseSession()
candidateSession?.releaseSession()
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
}
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession.olmInboundGroupSession?.releaseSession()
candidateSession?.releaseSession()
return AddSessionResult.NotImported
}
}
@@ -639,36 +649,42 @@ internal class MXOlmDevice @Inject constructor(
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
// sanity check on the new session
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
if (null == candidateOlmInboundSession) {
if (null == candidateSession) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
return AddSessionResult.NotImported
}
try {
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
if (candidateSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
candidateOlmInboundSession.releaseSession()
candidateSession.releaseSession()
return AddSessionResult.NotImported
}
} catch (e: Throwable) {
candidateOlmInboundSession.releaseSession()
candidateSession.releaseSession()
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
return AddSessionResult.NotImported
}
candidateSession.senderKey = senderKey
candidateSession.roomId = roomId
candidateSession.keysClaimed = keysClaimed
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
val candidateSessionData = InboundGroupSessionData(
senderKey = senderKey,
roomId = roomId,
keysClaimed = keysClaimed,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
sharedHistory = sharedHistory,
)
val wrapper = MXInboundMegolmSessionWrapper(
candidateSession,
candidateSessionData
)
if (existingSession != null) {
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey)
} else {
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey)
}
return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt())
}
/**
@@ -677,41 +693,22 @@ internal class MXOlmDevice @Inject constructor(
* @param megolmSessionsData the megolm sessions data
* @return the successfully imported sessions.
*/
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
val sessions = ArrayList<MXInboundMegolmSessionWrapper>(megolmSessionsData.size)
for (megolmSessionData in megolmSessionsData) {
val sessionId = megolmSessionData.sessionId ?: continue
val senderKey = megolmSessionData.senderKey ?: continue
val roomId = megolmSessionData.roomId
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
try {
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
}
// sanity check
if (candidateSessionToImport?.olmInboundGroupSession == null) {
Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
continue
}
val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
try {
if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
candidateOlmInboundGroupSession?.releaseSession()
continue
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
candidateOlmInboundGroupSession?.releaseSession()
val candidateSessionToImport = try {
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
} catch (e: Throwable) {
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
continue
}
val candidateOlmInboundGroupSession = candidateSessionToImport.session
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper
@@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor(
sessions.add(candidateSessionToImport)
} else {
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex }
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex }
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
// should not happen?
candidateSessionToImport.olmInboundGroupSession?.releaseSession()
candidateSessionToImport.session.releaseSession()
Timber.tag(loggerTag.value)
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
} else {
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
if (existingFirstKnown <= candidateFirstKnownIndex) {
// Ignore this, keep existing
candidateOlmInboundGroupSession.releaseSession()
} else {
@@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor(
): OlmDecryptionResult {
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
val wrapper = sessionHolder.wrapper
val inboundGroupSession = wrapper.olmInboundGroupSession
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
val inboundGroupSession = wrapper.session
if (roomId != wrapper.roomId) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
@@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor(
return OlmDecryptionResult(
payload,
wrapper.keysClaimed,
wrapper.sessionData.keysClaimed,
senderKey,
wrapper.forwardingCurve25519KeyChain
wrapper.sessionData.forwardingCurve25519KeyChain
)
}

View File

@@ -69,5 +69,13 @@ internal data class MegolmSessionData(
* Devices which forwarded this session to us (normally empty).
*/
@Json(name = "forwarding_curve25519_key_chain")
val forwardingCurve25519KeyChain: List<String>? = null
val forwardingCurve25519KeyChain: List<String>? = null,
/**
* Flag that indicates whether or not the current inboundSession will be shared to
* invited users to decrypt past messages.
*/
// When this feature lands in spec name = shared_history should be used
@Json(name = "org.matrix.msc3061.shared_history")
val sharedHistory: Boolean = false,
)

View File

@@ -437,7 +437,10 @@ internal class OutgoingKeyRequestManager @Inject constructor(
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
// let's see what's the index
val knownIndex = tryOrNull {
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex
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

View File

@@ -84,8 +84,9 @@ internal class MegolmSessionDataImporter @Inject constructor(
megolmSessionData.senderKey ?: "",
tryOrNull {
olmInboundGroupSessionWrappers
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
?.firstKnownIndex?.toInt()
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
?.session?.firstKnownIndex
?.toInt()
} ?: 0
)

View File

@@ -16,7 +16,9 @@
package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
/**
* An interface for encrypting data.
@@ -32,4 +34,6 @@ internal interface IMXEncrypting {
* @return the encrypted content
*/
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
}

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
@@ -41,6 +42,7 @@ internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration,
private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting {
@@ -240,13 +242,14 @@ internal class MXMegolmDecryption(
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val addSessionResult = olmDevice.addInboundGroupSession(
roomKeyContent.sessionId,
roomKeyContent.sessionKey,
roomKeyContent.roomId,
senderKey,
forwardingCurve25519KeyChain,
keysClaimed,
exportFormat
sessionId = roomKeyContent.sessionId,
sessionKey = roomKeyContent.sessionKey,
roomId = roomKeyContent.roomId,
senderKey = senderKey,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
keysClaimed = keysClaimed,
exportFormat = exportFormat,
sharedHistory = roomKeyContent.getSharedKey()
)
when (addSessionResult) {
@@ -296,6 +299,14 @@ internal class MXMegolmDecryption(
}
}
/**
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
*/
private fun RoomKeyContent.getSharedKey(): Boolean {
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
return sharedHistory ?: false
}
/**
* Check if the some messages can be decrypted with a new session.
*

View File

@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration,
private val eventsManager: Lazy<StreamEventsManager>
) {
@@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
olmDevice,
outgoingKeyRequestManager,
cryptoStore,
eventsManager
)
matrixConfiguration,
eventsManager)
}
}

View File

@@ -32,6 +32,7 @@ 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.DeviceListManager
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@@ -151,14 +152,27 @@ internal class MXMegolmEncryption(
"ed25519" to olmDevice.deviceEd25519Key!!
)
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
olmDevice.addInboundGroupSession(
sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
emptyList(), keysClaimedMap, false
sessionId = sessionId!!,
sessionKey = olmDevice.getSessionKey(sessionId)!!,
roomId = roomId,
senderKey = olmDevice.deviceCurve25519Key!!,
forwardingCurve25519KeyChain = emptyList(),
keysClaimed = keysClaimedMap,
exportFormat = false,
sharedHistory = sharedHistory
)
defaultKeysBackupService.maybeBackupKeys()
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock)
return MXOutboundSessionInfo(
sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore),
clock = clock,
sharedHistory = sharedHistory
)
}
/**
@@ -172,6 +186,8 @@ internal class MXMegolmEncryption(
if (session == null ||
// Need to make a brand new session?
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
// Is there a room history visibility change since the last outboundSession
cryptoStore.shouldShareHistory(roomId) != session.sharedHistory ||
// Determine if we have shared with anyone we shouldn't have
session.sharedWithTooManyDevices(devicesInRoom)) {
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
@@ -231,26 +247,27 @@ internal class MXMegolmEncryption(
/**
* Share the device keys of a an user.
*
* @param session the session info
* @param sessionInfo the session info
* @param devicesByUser the devices map
*/
private suspend fun shareUserDevicesKey(
session: MXOutboundSessionInfo,
devicesByUser: Map<String, List<CryptoDeviceInfo>>
) {
val sessionKey = olmDevice.getSessionKey(session.sessionId)
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
}
val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
val submap = HashMap<String, Any>()
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
submap["room_id"] = roomId
submap["session_id"] = session.sessionId
submap["session_key"] = sessionKey!!
submap["chain_index"] = chainIndex
val payload = HashMap<String, Any>()
payload["type"] = EventType.ROOM_KEY
payload["content"] = submap
val payload = mapOf(
"type" to EventType.ROOM_KEY,
"content" to mapOf(
"algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
"room_id" to roomId,
"session_id" to sessionInfo.sessionId,
"session_key" to sessionKey,
"chain_index" to chainIndex,
"org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
)
)
var t0 = clock.epochMillis()
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
@@ -292,7 +309,7 @@ internal class MXMegolmEncryption(
// for dead devices on every message.
for ((_, devicesToShareWith) in devicesByUser) {
for (deviceInfo in devicesToShareWith) {
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
// XXX is it needed to add it to the audit trail?
// For now decided that no, we are more interested by forward trail
}
@@ -300,8 +317,8 @@ internal class MXMegolmEncryption(
if (haveTargets) {
t0 = clock.epochMillis()
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
try {
withContext(coroutineDispatchers.io) {
@@ -310,7 +327,7 @@ internal class MXMegolmEncryption(
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
} catch (failure: Throwable) {
// What to do here...
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>")
}
} else {
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
@@ -320,7 +337,7 @@ internal class MXMegolmEncryption(
// XXX offload?, as they won't read the message anyhow?
notifyKeyWithHeld(
noOlmToNotify,
session.sessionId,
sessionInfo.sessionId,
olmDevice.deviceCurve25519Key,
WithHeldCode.NO_OLM
)
@@ -514,6 +531,51 @@ internal class MXMegolmEncryption(
}
}
@Throws
override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
if (!inboundSessionWrapper.wrapper.sessionData.sharedHistory) throw IllegalArgumentException("This key can't be shared")
Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
val userId = deviceInfo.userId
val deviceId = deviceInfo.deviceId
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm")
// process anyway?
null
}
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys")
return
}
val export = inboundSessionWrapper.mutex.withLock {
inboundSessionWrapper.wrapper.exportKeys()
} ?: return Unit.also {
Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}")
}
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload =
withContext(coroutineDispatchers.computation) {
messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
}
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.tag(loggerTag.value)
.d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(sendToDeviceParams)
}
}
data class DeviceInRoomInfo(
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()

View File

@@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo(
private val clock: Clock,
// When the session was created
private val creationTime: Long = clock.epochMillis(),
val sharedHistory: Boolean = false
) {
// Number of times this session has been used

View File

@@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixConfiguration
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_BACKUP
@@ -50,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.ObjectSigner
@@ -71,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
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.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor(
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor
private val taskExecutor: TaskExecutor,
private val matrixConfiguration: MatrixConfiguration,
private val inboundGroupSessionStore: InboundGroupSessionStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : KeysBackupService {
@@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor(
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
try {
encryptGroupSession(olmInboundGroupSessionWrapper)
@@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor(
@VisibleForTesting
@WorkerThread
fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? {
suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? {
olmInboundGroupSessionWrapper.safeSessionId ?: return null
olmInboundGroupSessionWrapper.senderKey ?: return null
// Gather information for each key
val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) }
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey)
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null
val sessionData = inboundGroupSessionStore
.getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
?.let {
withContext(coroutineDispatchers.computation) {
it.mutex.withLock { it.wrapper.exportKeys() }
}
}
?: return null
val sessionBackupData = mapOf(
"algorithm" to sessionData.algorithm,
"sender_key" to sessionData.senderKey,
"sender_claimed_keys" to sessionData.senderClaimedKeys,
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
"session_key" to sessionData.sessionKey
"session_key" to sessionData.sessionKey,
"org.matrix.msc3061.shared_history" to sessionData.sharedHistory
)
val json = MoshiProvider.providesMoshi()
@@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor(
.toJson(sessionBackupData)
val encryptedSessionBackupData = try {
backupOlmPkEncryption?.encrypt(json)
withContext(coroutineDispatchers.computation) {
backupOlmPkEncryption?.encrypt(json)
}
} catch (e: OlmException) {
Timber.e(e, "OlmException")
null
@@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor(
// Build backup data for that key
return KeyBackupData(
firstMessageIndex = try {
olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
olmInboundGroupSessionWrapper.session.firstKnownIndex
} catch (e: OlmException) {
Timber.e(e, "OlmException")
0L
},
forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
isVerified = device?.isVerified == true,
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
sessionData = mapOf(
"ciphertext" to encryptedSessionBackupData.mCipherText,
"mac" to encryptedSessionBackupData.mMac,
@@ -1451,6 +1468,14 @@ internal class DefaultKeysBackupService @Inject constructor(
)
}
/**
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
*/
private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean {
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
return sessionData.sharedHistory
}
@VisibleForTesting
@WorkerThread
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {

View File

@@ -50,5 +50,12 @@ internal data class KeyBackupData(
* Algorithm-dependent data.
*/
@Json(name = "session_data")
val sessionData: JsonDict
val sessionData: JsonDict,
/**
* Flag that indicates whether or not the current inboundSession will be shared to
* invited users to decrypt past messages.
*/
@Json(name = "org.matrix.msc3061.shared_history")
val sharedHistory: Boolean = false
)

View File

@@ -0,0 +1,51 @@
/*
* 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.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class InboundGroupSessionData(
/** The room in which this session is used. */
@Json(name = "room_id")
var roomId: String? = null,
/** The base64-encoded curve25519 key of the sender. */
@Json(name = "sender_key")
var senderKey: String? = null,
/** Other keys the sender claims. */
@Json(name = "keys_claimed")
var keysClaimed: Map<String, String>? = null,
/** Devices which forwarded this session to us (normally emty). */
@Json(name = "forwarding_curve25519_key_chain")
var forwardingCurve25519KeyChain: List<String>? = emptyList(),
/** Not yet used, will be in backup v2
val untrusted?: Boolean = false */
/**
* Flag that indicates whether or not the current inboundSession will be shared to
* invited users to decrypt past messages.
*/
@Json(name = "shared_history")
val sharedHistory: Boolean = false,
)

View File

@@ -0,0 +1,97 @@
/*
* 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.model
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.olm.OlmInboundGroupSession
import timber.log.Timber
data class MXInboundMegolmSessionWrapper(
// olm object
val session: OlmInboundGroupSession,
// data about the session
val sessionData: InboundGroupSessionData
) {
// shortcut
val roomId = sessionData.roomId
val senderKey = sessionData.senderKey
val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() }
/**
* Export the inbound group session keys.
* @param index the index to export. If null, the first known index will be used
* @return the inbound group session as MegolmSessionData if the operation succeeds
*/
internal fun exportKeys(index: Long? = null): MegolmSessionData? {
return try {
val keysClaimed = sessionData.keysClaimed ?: return null
val wantedIndex = index ?: session.firstKnownIndex
MegolmSessionData(
senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(),
sessionKey = session.export(wantedIndex),
senderClaimedKeys = keysClaimed,
roomId = sessionData.roomId,
sessionId = session.sessionIdentifier(),
senderKey = senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sharedHistory = sessionData.sharedHistory
)
} catch (e: Exception) {
Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed")
null
}
}
companion object {
/**
* @exportFormat true if the megolm keys are in export format
* (ie, they lack an ed25519 signature)
*/
@Throws
internal fun newFromMegolmData(megolmSessionData: MegolmSessionData, exportFormat: Boolean): MXInboundMegolmSessionWrapper {
val exportedKey = megolmSessionData.sessionKey ?: throw IllegalArgumentException("key data not found")
val inboundSession = if (exportFormat) {
OlmInboundGroupSession.importSession(exportedKey)
} else {
OlmInboundGroupSession(exportedKey)
}
.also {
if (it.sessionIdentifier() != megolmSessionData.sessionId) {
it.releaseSession()
throw IllegalStateException("Mismatched group session Id")
}
}
val data = InboundGroupSessionData(
roomId = megolmSessionData.roomId,
senderKey = megolmSessionData.senderKey,
keysClaimed = megolmSessionData.senderClaimedKeys,
forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
sharedHistory = megolmSessionData.sharedHistory,
)
return MXInboundMegolmSessionWrapper(
inboundSession,
data
)
}
}
}

View File

@@ -26,6 +26,8 @@ import java.io.Serializable
* This class adds more context to a OlmInboundGroupSession object.
* This allows additional checks. The class implements Serializable so that the context can be stored.
*/
// Note used anymore, just for database migration
// Deprecated("Use MXInboundMegolmSessionWrapper")
internal class OlmInboundGroupSessionWrapper2 : Serializable {
// The associated olm inbound group session.

View File

@@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession
internal data class OutboundGroupSessionWrapper(
val outboundGroupSession: OlmOutboundGroupSession,
val creationTime: Long
val creationTime: Long,
/**
* As per MSC 3061, declares if this key could be shared when inviting a new user to the room.
*/
val sharedHistory: Boolean = false
)

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* 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
* 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,
@@ -14,13 +14,9 @@
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.group.model
package org.matrix.android.sdk.internal.crypto.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class GroupUsers(
@Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int,
@Json(name = "chunk") val users: List<GroupUser> = emptyList()
data class SessionInfo(
val sessionId: String,
val senderKey: String
)

View File

@@ -35,7 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
@@ -64,7 +64,15 @@ internal interface IMXCryptoStore {
*
* @return the list of all known group sessions, to export them.
*/
fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2>
fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
/**
* Retrieve the known inbound group sessions for the specified room.
*
* @param roomId The roomId that the sessions will be returned
* @return the list of all known group sessions, for the provided roomId
*/
fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
/**
* @return true to unilaterally blacklist all unverified devices.
@@ -90,6 +98,20 @@ internal interface IMXCryptoStore {
fun isKeyGossipingEnabled(): Boolean
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun enableShareKeyOnInvite(enable: Boolean)
/**
* As per MSC3061.
* If true will make it possible to share part of e2ee room history
* on invite depending on the room visibility setting.
*/
fun isShareKeysOnInviteEnabled(): Boolean
/**
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
*
@@ -250,6 +272,17 @@ internal interface IMXCryptoStore {
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
fun shouldShareHistory(roomId: String): Boolean
/**
* Sets a boolean flag that will determine whether or not room history (existing inbound sessions)
* will be shared to new user invites.
*
* @param roomId the room id
* @param shouldShareHistory The boolean flag
*/
fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean)
/**
* Store a session between the logged-in user and another device.
*
@@ -290,7 +323,7 @@ internal interface IMXCryptoStore {
*
* @param sessions the inbound group sessions to store.
*/
fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>)
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
/**
* Retrieve an inbound group session.
@@ -299,7 +332,17 @@ internal interface IMXCryptoStore {
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return an inbound group session.
*/
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2?
fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper?
/**
* Retrieve an inbound group session, filtering shared history.
*
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @param sharedHistory filter inbound session with respect to shared history field
* @return an inbound group session.
*/
fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper?
/**
* Get the current outbound group session for this encrypted room.
@@ -333,7 +376,7 @@ internal interface IMXCryptoStore {
*
* @param olmInboundGroupSessionWrappers the sessions
*/
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>)
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
/**
* Retrieve inbound group sessions that are not yet backed up.
@@ -341,7 +384,7 @@ internal interface IMXCryptoStore {
* @param limit the maximum number of sessions to return.
* @return an array of non backed up inbound group sessions.
*/
fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2>
fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
/**
* Number of stored inbound group sessions.

View File

@@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldCo
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor(
?: false
}
override fun shouldShareHistory(roomId: String): Boolean {
if (!isShareKeysOnInviteEnabled()) return false
return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory
}
?: false
}
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
}
}
override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
Timber.tag(loggerTag.value)
.v("setShouldShareHistory for room $roomId is $shouldShareHistory")
doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
}
}
override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
var sessionIdentifier: String? = null
@@ -727,54 +743,55 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) {
override fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>) {
if (sessions.isEmpty()) {
return
}
doRealmTransaction(realmConfiguration) { realm ->
sessions.forEach { session ->
var sessionIdentifier: String? = null
sessions.forEach { wrapper ->
try {
sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier()
val sessionIdentifier = try {
wrapper.session.sessionIdentifier()
} catch (e: OlmException) {
Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
return@forEach
}
if (sessionIdentifier != null) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
// val shouldShareHistory = session.roomId?.let { roomId ->
// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory
// } ?: false
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
if (existing != null) {
// we want to keep the existing backup status
existing.putInboundGroupSession(session)
} else {
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
senderKey = session.senderKey
putInboundGroupSession(session)
}
realm.insertOrUpdate(realmOlmInboundGroupSession)
}
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
store(wrapper)
}
Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key")
realm.insertOrUpdate(realmOlmInboundGroupSession)
}
}
}
override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
?.getInboundGroupSession()
?.toModel()
}
}
override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory)
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
?.toModel()
}
}
@@ -786,7 +803,8 @@ internal class RealmCryptoStore @Inject constructor(
entity.getOutboundGroupSession()?.let {
OutboundGroupSessionWrapper(
it,
entity.creationTime ?: 0
entity.creationTime ?: 0,
entity.shouldShareHistory
)
}
}
@@ -806,6 +824,8 @@ internal class RealmCryptoStore @Inject constructor(
if (outboundGroupSession != null) {
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
creationTime = clock.epochMillis()
// Store the room history visibility on the outbound session creation
shouldShareHistory = entity.shouldShareHistory
putOutboundGroupSession(outboundGroupSession)
}
entity.outboundSessionInfo = info
@@ -814,17 +834,32 @@ internal class RealmCryptoStore @Inject constructor(
}
}
// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean {
// return doWithRealm(realmConfiguration) { realm ->
// CryptoRoomEntity.getById(realm, roomId)?.let { entity ->
// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory
// }
// } ?: false
// }
/**
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management.
*/
override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.findAll()
.mapNotNull { inboundGroupSessionEntity ->
inboundGroupSessionEntity.getInboundGroupSession()
}
.mapNotNull { it.toModel() }
}
}
override fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) { realm ->
realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId)
.findAll()
.mapNotNull { it.toModel() }
}
}
@@ -885,7 +920,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) {
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>) {
if (olmInboundGroupSessionWrappers.isEmpty()) {
return
}
@@ -893,10 +928,13 @@ internal class RealmCryptoStore @Inject constructor(
doRealmTransaction(realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try {
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
val sessionIdentifier =
tryOrNull("Failed to get session identifier") {
olmInboundGroupSessionWrapper.session.sessionIdentifier()
} ?: return@forEach
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
sessionIdentifier,
olmInboundGroupSessionWrapper.senderKey
olmInboundGroupSessionWrapper.sessionData.senderKey
)
val existing = realm.where<OlmInboundGroupSessionEntity>()
@@ -909,9 +947,7 @@ internal class RealmCryptoStore @Inject constructor(
// ... might be in cache but not yet persisted, create a record to persist backedup state
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
senderKey = olmInboundGroupSessionWrapper.senderKey
putInboundGroupSession(olmInboundGroupSessionWrapper)
store(olmInboundGroupSessionWrapper)
backedUp = true
}
@@ -924,15 +960,13 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2> {
override fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper> {
return doWithRealm(realmConfiguration) {
it.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
.limit(limit.toLong())
.findAll()
.mapNotNull { inboundGroupSession ->
inboundGroupSession.getInboundGroupSession()
}
.mapNotNull { it.toModel() }
}
}
@@ -973,6 +1007,18 @@ internal class RealmCryptoStore @Inject constructor(
} ?: false
}
override fun isShareKeysOnInviteEnabled(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite
} ?: false
}
override fun enableShareKeyOnInvite(enable: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable
}
}
override fun setDeviceKeysUploaded(uploaded: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded

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