Compare commits

..

294 Commits

Author SHA1 Message Date
df6080b1da Merge branch 'release/0.2.0' 2019-07-18 17:47:39 +02:00
d609c49b31 Prepare release 0.2.0 2019-07-18 17:47:24 +02:00
d87ee32422 Merge pull request #384 from vector-im/feature/edit_e2e
Feature/edit e2e
2019-07-18 16:44:44 +02:00
f0671b9e73 "Riot X" -> "RiotX" 2019-07-18 14:28:46 +02:00
e218691bf2 Import strings and translation from Riot 2019-07-18 14:25:34 +02:00
9c67036c08 Fix / keyboard won't show when using reply from long tap menu 2019-07-18 12:13:17 +02:00
62657538af Merge pull request #389 from vector-im/feature/cleanup
Do not show invitation in the filtered room list
2019-07-18 12:10:47 +02:00
5438207fba faster animation for quick reply 2019-07-18 12:01:23 +02:00
fe88aaffbd Inject RoomListNameFilter 2019-07-18 11:39:13 +02:00
21ba72e5e7 Do not show invitation in the filtered room list 2019-07-18 11:34:49 +02:00
d48ae967bd Remove dead code 2019-07-18 11:11:42 +02:00
0afde3b021 Rename class member for code clarity 2019-07-18 11:07:09 +02:00
49ae954183 Merge remote-tracking branch 'origin/develop' into develop 2019-07-18 10:58:40 +02:00
64bee91f7a Merge pull request #387 from vector-im/feature/fix_sync_state
Fix sync state progress bar
2019-07-18 10:57:46 +02:00
51fdccb393 cleaning 2019-07-18 09:29:27 +02:00
7e3b300130 Fix sync state progress bar 2019-07-17 19:45:35 +02:00
a98b324c89 Merge pull request #385 from vector-im/feature/invit_notif
Cancel invitation notification when handling the invitation in the application
2019-07-17 18:39:40 +02:00
977721881f Cancel invitation notification when handling the invitation in the application 2019-07-17 18:35:41 +02:00
7d41352918 Fix / edit reply was quoting wrong text
+ e2e reply of edit
2019-07-17 16:46:56 +02:00
077396a832 E2E replies
+ Edit History / support e2e and use original event
2019-07-17 16:20:12 +02:00
32b79bd50e Remove extra space around userId 2019-07-17 15:13:12 +02:00
844f6d16a4 Code quality 2019-07-17 15:05:29 +02:00
fc9ef579ca Merge pull request #381 from vector-im/feature/room_members_perf
Feature/room members perf
2019-07-17 15:01:06 +02:00
77fa5af1b8 Fix compilation issue after merge 2019-07-17 14:58:23 +02:00
2948018453 Clean code after review 2019-07-17 14:56:00 +02:00
90d25ff45e Code cleanup 2019-07-17 14:41:01 +02:00
173452d38c Merge pull request #367 from Dominaezzz/kotlinify-3
Some more kotlinification.
2019-07-17 14:38:16 +02:00
a9f9083745 Merge pull request #374 from vector-im/feature/quick_fix_long_click_link
WIP /  Fix Copying link from a message shouldn't open context menu
2019-07-17 14:37:11 +02:00
22dc2a6790 Fix Copying link from a message shouldn't open context menu 2019-07-17 14:36:47 +02:00
927cd7285d Merge pull request #378 from vector-im/feature/fix_sync_thread_wrong_autostart
Fix / SyncThread was started in background
2019-07-17 14:32:19 +02:00
4d5bdecec6 Merge pull request #382 from vector-im/feature/better_long_tap_menu
Feature/better long tap menu
2019-07-17 14:28:51 +02:00
0be987ac0d Merge branch 'develop' into feature/better_long_tap_menu 2019-07-17 14:28:36 +02:00
4bfaa00be4 Fix / clean bad method name 2019-07-17 14:27:02 +02:00
8e78d8a58d Merge pull request #380 from vector-im/feature/rs_crash_steve
Fix a crash in notificationwhen display name is empty
2019-07-17 14:22:45 +02:00
e3e86c0a41 Merge pull request #383 from vector-im/feature/filter_params
Pass filter to room directory screen or create room screen
2019-07-17 14:20:29 +02:00
8a5fddd952 Merge pull request #379 from vector-im/feature/small_fixes
Fix bad View used for searching in room directory.
2019-07-17 14:19:37 +02:00
208460850e Dagger: activate incremental build 2019-07-17 14:16:20 +02:00
0ddef67cc9 Migrate to rxbinding 3 and fix bad layout for room directory filter (Fixes #349) 2019-07-17 14:16:20 +02:00
896e582a9c Create style VectorSearchView 2019-07-17 14:16:20 +02:00
477920f411 Add some comment 2019-07-17 14:14:02 +02:00
c647648e79 Merge pull request #371 from vector-im/feature/composer_fix_edit_reply
Feature/composer fix edit reply
2019-07-17 14:03:10 +02:00
b654025a3b Fix alignment issue in toolbars 2019-07-17 12:38:35 +02:00
786a7d7560 Rename id 2019-07-17 12:20:11 +02:00
b935b9311e Scroll the list to top after each new filter 2019-07-17 12:18:45 +02:00
8e12f71535 Add top left back button 2019-07-17 12:16:10 +02:00
7eea2ccfb4 Fix infinite opening of room once the room is created 2019-07-17 12:09:09 +02:00
c32ef02a12 Pre fill the room directory filter and and the room name with the already entered string from the user 2019-07-17 12:04:19 +02:00
3651ec4870 Add some doc 2019-07-17 11:58:18 +02:00
87de7bd3e6 fix lint code quality 2019-07-17 11:41:14 +02:00
9494174c33 Swipe to reply in timeline (lab) 2019-07-17 10:54:15 +02:00
b7e0b400fb Timeline : set bigger initial load size 2019-07-16 17:48:32 +02:00
a8f06f609b Use latest retrofit version to properly cancel requests
Fix cancelation requests
2019-07-16 17:46:52 +02:00
d469299f42 RoomMembers: should fix state events issues 2019-07-16 17:46:52 +02:00
9bdea5b325 Change order of actions (and reply on top) 2019-07-16 16:35:57 +02:00
2f01ad99b3 Compact long tap menu 2019-07-16 16:35:36 +02:00
bb3b5788ba Update hint from design 2019-07-16 16:35:10 +02:00
45f7d3e9c4 Kotlin style 2019-07-16 15:59:08 +02:00
0f7a56d005 Use Session.myUserId whereas it's possible 2019-07-16 15:54:00 +02:00
63d2861bc8 Fix / SyncThread was started in background
Upon reception of a push, is the session is instantiated the sync thread was starting to loop
2019-07-16 15:44:08 +02:00
6bbc784c29 Fix crash (from Steve's rageshake) 2019-07-16 15:42:02 +02:00
c6fd625761 code review 2019-07-16 14:56:16 +02:00
d8092abc4e fix / strip reply prefix on history 2019-07-16 14:39:46 +02:00
6effb90361 Fix / edit of reply and edit of edit of reply 2019-07-16 14:39:05 +02:00
42584fc55a Merge pull request #372 from vector-im/feature/room_filtering
Room filtering
2019-07-16 11:41:08 +02:00
30d9ddb3e8 Merge pull request #373 from vector-im/feature/fix_composer_separator_dark
Fix / composer separator color was using a clear theme color
2019-07-15 18:16:06 +02:00
020c32bb1a Fix / composer separator color was using a clear theme color 2019-07-15 17:46:24 +02:00
efd973208f Green close icon 2019-07-15 17:35:51 +02:00
30a6c98c08 Room name in bold 2019-07-15 17:29:37 +02:00
1440080d04 Changes 2019-07-15 17:27:49 +02:00
61bb4c0427 Introduce CreateRoomActivity, a simple container for [CreateRoomFragment] 2019-07-15 17:26:48 +02:00
3c25088243 Filter rooms 2019-07-15 17:26:48 +02:00
fc1c0caea3 Avoid displaying two loaders if there is no elements between them 2019-07-15 17:25:59 +02:00
8901a5e09a Merge pull request #342 from vector-im/feature/edit_history
Feature/edit history
2019-07-15 15:15:45 +02:00
25f1d21bc7 Edit history
Get history from API


cleaning


Updated change log


Missing copyrights


Code review


cleaning
2019-07-15 14:57:12 +02:00
4d2ab9fa31 Merge pull request #344 from vector-im/feature/play_store_crash
Feature/play store crash
2019-07-15 10:49:20 +02:00
0289d2ee87 Simpler code 2019-07-15 10:48:44 +02:00
222201cc64 Fix crash observe on the PlayStore (#341) 2019-07-15 10:48:44 +02:00
b15dea6de3 Merge pull request #338 from vector-im/feature/green_encrypt
Text in green when encrypting
2019-07-15 10:46:44 +02:00
2ba83e456d Merge pull request #343 from vector-im/feature/click_on_redacted_event
Handle click on redacted event
2019-07-15 10:46:06 +02:00
1822fc4fbb Some more kotlinification
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-13 15:35:10 +01:00
e6dd1fbfec Use GlobalScope instead of temp scope
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-13 15:18:16 +01:00
e2ea76f871 Fix crash reported by PlayStore 2019-07-12 16:48:35 +02:00
9182f2ce4e RoomMembers/User : get a better and faster handling (still need to fix one small issue) 2019-07-12 13:59:37 +02:00
34d14eb304 Fix regression on permalink click 2019-07-12 13:51:37 +02:00
3625c462f0 Click on redacted event 2019-07-12 13:51:37 +02:00
fe69206340 Prepare next release 2019-07-12 11:39:26 +02:00
f9885fd04c Update CHANGES.md 2019-07-12 11:38:55 +02:00
316c8ec27e Merge pull request #265 from vector-im/readme_update_for_beta
README: Update it for the beta launch
2019-07-12 11:36:49 +02:00
41465450d8 Code cleanup 2019-07-12 10:45:08 +02:00
bd009caaf1 Code cleanup 2019-07-12 10:22:58 +02:00
33252c3b65 Green text color during encrypting 2019-07-12 10:16:43 +02:00
10e4d0190f Try to insert users directly to see if perfs are better [WIP] 2019-07-11 18:55:13 +02:00
b77310fe92 Merge pull request #337 from vector-im/feature/debug_suffix
Add ".debug" to the applicationId to be able to install the app along with the prod version
2019-07-11 18:33:43 +02:00
919dec4a56 Add ".debug" to the applicationId to be able to install the app along with the prod version 2019-07-11 17:59:07 +02:00
43b3680774 Prepare next release 2019-07-11 17:44:58 +02:00
bfb5fce809 Update CHANGES.md 2019-07-11 17:43:56 +02:00
1f3731aae7 Merge branch 'master' into develop 2019-07-11 17:42:11 +02:00
52dced43ff Fix version code issue 2019-07-11 16:49:06 +02:00
ff80c3c8d5 Add script to sign the APK. 2019-07-11 16:41:45 +02:00
34e4d27573 Add missing space in pipeline 2019-07-11 16:00:45 +02:00
6522148e63 Merge branch 'release/0.1.0' 2019-07-11 15:54:48 +02:00
252b2ea30a Merge pull request #334 from vector-im/feature/general_perf
Feature/general perf
2019-07-11 15:52:00 +02:00
f493ce44f2 RealmLiveEntity: passes the results and changeSet instead of filtering as it's more efficient 2019-07-11 15:30:01 +02:00
c4c5069ee5 Merge pull request #332 from vector-im/feature/login_warning
Improve login screen
2019-07-11 15:25:11 +02:00
423125b5d9 Merge pull request #333 from vector-im/feature/timeout
Create a TimeOutInterceptor to set specific timeout on some request
2019-07-11 15:24:54 +02:00
9e3d29b7d7 Create a TimeOutInterceptor to set specific timeout on some request: login and sync (Fixes #170) 2019-07-11 15:16:25 +02:00
f65becf7c0 Rework login screen before release 2019-07-11 14:38:30 +02:00
80a61cf6b5 Improve dependency download safe path 2019-07-11 14:03:20 +02:00
77056aff94 Merge pull request #330 from vector-im/feature/edit_emote
Edit emote
2019-07-11 13:34:46 +02:00
65e123d87f Split long lines 2019-07-11 13:32:28 +02:00
d0b145d031 Edit emote 2019-07-11 12:29:02 +02:00
98306e223b Merge pull request #322 from vector-im/feature/clean
Improve reply feature
2019-07-11 11:46:00 +02:00
c9fe1adb77 Add a debug button to test crash of the app 2019-07-11 10:36:59 +02:00
1b95336ad3 EventEntity|TimelineEventEntity : remove UUID as primary key and use auto-incremented Long 2019-07-11 10:25:30 +02:00
f007fb04b8 Timeline: clean listeners 2019-07-11 10:25:30 +02:00
141434e8f8 Try getting things off the main thread 2019-07-11 10:25:30 +02:00
b8669d5ed2 Sync: use a single threaded executor to ensure we have only one sync at a time 2019-07-11 10:23:24 +02:00
7a08a11b19 Fix compilation of test 2019-07-10 18:17:03 +02:00
54b1d18812 Merge remote-tracking branch 'origin/feature/clean' into feature/clean 2019-07-10 18:07:03 +02:00
3aa30e5f15 Fix reply of reply 2019-07-10 18:06:44 +02:00
ddf4a81905 Do not display the banner when keys backup is sending keys 2019-07-10 18:04:27 +02:00
794fd650a4 Mutualize code, and also, when replying to an edited event, use the last text in the reply prefix content 2019-07-10 17:37:22 +02:00
9a57a02996 Cleaner code: add TimelineEvent to special modes 2019-07-10 17:05:32 +02:00
7e8cd07e1e Do not send edition if text is identical 2019-07-10 16:32:44 +02:00
d613abf4b4 i18n edited_suffix 2019-07-10 15:29:52 +02:00
06699eaefc Cleaner code 2019-07-10 14:40:08 +02:00
e5082f662c Fix actually done TODO 2019-07-10 14:19:59 +02:00
c8ab53e39c Video visibility fix 2019-07-10 14:11:49 +02:00
d424a135a9 Merge pull request #324 from vector-im/feature/quick_react_e2e
Quick react on e2e was not displayed
2019-07-10 14:08:46 +02:00
e6409d4c60 Create a common canReact() method 2019-07-10 12:10:55 +02:00
19c7de687e We can react on e2e room text event 2019-07-10 11:51:09 +02:00
1918302297 Reply with formatted content 2019-07-10 11:29:47 +02:00
92e3a02389 Create data class instead of Pair 2019-07-10 10:34:32 +02:00
0a54801fcc Code clarity 2019-07-10 10:16:21 +02:00
228ee52563 Remove extra space in <mx-reply> 2019-07-10 10:07:45 +02:00
e6c74dc1fe Convert a Task to a ConfigurableTask without parameter 2019-07-09 18:41:08 +02:00
fe82ad2002 Format 2019-07-09 18:31:04 +02:00
f66739491a Merge pull request #321 from vector-im/feature/workManager_clean
Fix bug on WorkManager: clean by tag
2019-07-09 18:30:07 +02:00
c5dc9d4a9a Fix test 2019-07-09 18:29:32 +02:00
8f858f8119 Fix / line too long 2019-07-09 18:20:00 +02:00
6e036c24b8 Make the test be runnable 2019-07-09 18:14:58 +02:00
5e832e07cd Code cleanup 2019-07-09 18:04:19 +02:00
e9700e04d8 Move method to JsonCanonicalizer and fix test compilation 2019-07-09 18:04:19 +02:00
c19b1f917f Javadoc 2019-07-09 18:04:19 +02:00
4281b5967a Create object for work constraint 2019-07-09 18:04:19 +02:00
aa743d8469 Ensure we do not cancel Work from other lib or SDK client 2019-07-09 18:04:19 +02:00
a09850b16c Merge pull request #316 from vector-im/feature/initial_sync_progress
Feature/initial sync progress
2019-07-09 17:58:24 +02:00
6cb94dd4d6 Fine tune task weights + more measure 2019-07-09 17:42:53 +02:00
c9931e3ba3 Block interaction on initial sync 2019-07-09 17:36:08 +02:00
fc302c1b5a FIx / crash notification drawer empty nam 2019-07-09 17:35:50 +02:00
34ac987494 Cleanup 2019-07-09 16:36:46 +02:00
24b2387703 Merge pull request #319 from vector-im/feature/code_quality
Feature/code quality
2019-07-09 16:29:44 +02:00
8a0c9ae9b0 Rename PreferencesManager to VectorPreferences for code clarity 2019-07-09 16:29:24 +02:00
a79227424f Convert PreferencesManager file to Kotlin 2019-07-09 16:07:16 +02:00
ffe0b9712c Convert file to Kotlin 2019-07-09 15:50:15 +02:00
d92c090c30 Code quality: HashMap / HashSet 2019-07-09 15:40:49 +02:00
1a4157a663 review 2019-07-09 15:38:44 +02:00
fa81d1a9c7 Fix / revert bad refactor rename 2019-07-09 15:38:44 +02:00
dba4df6836 clean 2019-07-09 15:38:44 +02:00
4aae1f78d8 moved new strings + @StringRes annotation 2019-07-09 15:38:44 +02:00
8159a52bd7 cleaning 2019-07-09 15:38:44 +02:00
95d83db90c WIP 2019-07-09 15:38:44 +02:00
ac5b0af63e Code quality: remove rule for map() 2019-07-09 15:37:20 +02:00
e80473903e Code quality: import static 2019-07-09 15:35:27 +02:00
d08778c674 Code quality: equalTo 2019-07-09 15:33:31 +02:00
0919b9460d Code quality: split long lines 2019-07-09 15:26:32 +02:00
66a018c79e Code quality: trim() 2019-07-09 15:11:20 +02:00
dcd64de4b8 Check sdk modules 2019-07-09 15:07:11 +02:00
a0bd206308 Merge pull request #318 from vector-im/feature/send_state
Fix some bugs on e2e rooms
2019-07-09 15:03:39 +02:00
ba589e7961 Add missing permission request 2019-07-09 15:03:21 +02:00
5dc83d64c1 Fix compilation issue 2019-07-09 15:03:21 +02:00
9a4eb8e9a4 add getFileUrl extension 2019-07-09 15:03:21 +02:00
058e7153a1 Fix bug 2019-07-09 15:03:21 +02:00
d7b2371854 Add long click listener to file items 2019-07-09 15:03:21 +02:00
b0c939866f Download file - typo 2019-07-09 15:03:21 +02:00
a07f8b615e Download file - WIP 2019-07-09 15:03:21 +02:00
12bd85e0a9 Decrypt video file 2019-07-09 15:02:31 +02:00
1b82ed5abb Fix regression 2019-07-09 15:02:31 +02:00
c13ab62187 Fix issue when sending video in encrypted room 2019-07-09 15:02:31 +02:00
ea77686746 Send file: cleanup 2019-07-09 15:02:31 +02:00
8a5612be3d Send file: improve UI feedback 2019-07-09 15:02:31 +02:00
d24ce27903 Add missing call to contentUploadStateTracker.setFailure 2019-07-09 15:02:31 +02:00
2099965508 Avoid returning Result.failure() from appendable worker. 2019-07-09 15:02:31 +02:00
829e8da8dc lastFailureMessage is val, not var 2019-07-09 15:02:31 +02:00
e149ee53de Fix bad mime type for encrypted thumbnail 2019-07-09 15:02:31 +02:00
b73d3b15f8 Merge pull request #317 from vector-im/feature/realm_entity_rework
Feature/realm entity rework
2019-07-09 15:01:05 +02:00
61d7f23870 remove dead code 2019-07-09 15:00:37 +02:00
b5650b2b8f Pagination : avoid breaking timeline when paginating twice from same token (race condition) 2019-07-09 14:44:59 +02:00
8777d13d8b Fix / view source, decrypted source was not correct 2019-07-09 14:22:40 +02:00
d52613d723 Trick / Remove home progress blank paddings 2019-07-09 11:17:36 +02:00
7ce476f858 Merge pull request #313 from vector-im/feature/notif_optim
Improve notification drawer manager: Dagger, throttle, and icon for API 9
2019-07-08 17:44:10 +02:00
dd07f5c2a6 TimelineEvent : update sender data when loading room members and prune event (+ remove RoomSummaryMapper param) 2019-07-08 15:32:24 +02:00
7e6e09bc19 fix / compilation 2019-07-08 15:30:11 +02:00
1d11a163af Notification resolver try to decrypt 2019-07-08 15:08:49 +02:00
57bd103de8 Fix / decrypt room summary latest event 2019-07-08 14:58:49 +02:00
25bc5001f9 RoomSummary / Use encrypted message screen 2019-07-08 14:57:37 +02:00
e4c52484b1 Fix / ensure equals check for encryption result 2019-07-08 14:57:02 +02:00
a30da07fd1 Fix / timeline auto refresh on new session 2019-07-08 14:12:46 +02:00
ee27d3e047 Fix / clear unknown session map before re-request decrypt 2019-07-08 12:49:22 +02:00
7096094224 wip crypto 2019-07-08 12:05:41 +02:00
443fb41d18 Cleanup 2019-07-08 11:21:26 +02:00
94b4351e19 wip async crypto + persist 2019-07-08 11:18:27 +02:00
e90aeff417 ThrottleLast the notification drawer manager 2019-07-08 11:08:23 +02:00
e50dd265d4 merge develop 2019-07-08 10:58:41 +02:00
4521ea14ee Merge branch 'develop' into feature/realm_entity_rework 2019-07-08 10:55:20 +02:00
535b41d818 Rename Debouncer to FirstThrottler 2019-07-08 10:49:32 +02:00
21357a1ec7 private fun 2019-07-08 10:32:38 +02:00
8c872caf78 Inject IconLoader and BitmapLoader 2019-07-08 10:30:45 +02:00
62a81a556e Refresh notification drawer in a background thread. It also fixes the person and room avatar display 2019-07-08 10:26:22 +02:00
568e8c8bc0 Do not load user icon before Android Pie 2019-07-08 10:10:39 +02:00
98a7652403 Put back local echo 2019-07-05 19:13:34 +02:00
78951b9155 Timeline event: handle displayName/avatar [WIP] 2019-07-05 19:07:33 +02:00
8c86a653b2 Merge pull request #309 from vector-im/feature/crypto_cleanup
Rework Crypto using Try
2019-07-05 19:03:59 +02:00
ea0526821e Top left Back does not go to previous Activity anymore (Fixes #275) 2019-07-05 18:44:09 +02:00
c503445092 Branch back relation summaries 2019-07-05 18:38:20 +02:00
205af8b122 Merge pull request #280 from Dominaezzz/kotlinify-1
Enhance CancelableBag
2019-07-05 18:34:28 +02:00
3abb7c8de6 Merge pull request #308 from Dominaezzz/kotlinify-2
Some "Kotlinification"
2019-07-05 18:11:18 +02:00
a40510da3b Merge pull request #310 from vector-im/feature/buildkite_pr
Build every branch which is not master, to be able to build PR from external repository
2019-07-05 18:06:14 +02:00
a6ab4a349d Build every branch which is not master, to be able to build PR from external repository 2019-07-05 18:02:13 +02:00
79a704d240 Timeline : Uncomment liveChunk to make pagination working 2019-07-05 17:27:24 +02:00
e5adf174a8 Fix crash when invalid urls for image 2019-07-05 17:00:57 +02:00
f01e796271 Timeline is back 2019-07-05 17:00:13 +02:00
302d23ba96 Create a realm locker to fast up next Realm.getInstance calls 2019-07-05 16:28:15 +02:00
03050c3f25 Cleanup 2019-07-05 16:11:54 +02:00
cbfd2af74b Start branching TimelineEventEntity 2019-07-05 16:07:12 +02:00
f3fab0dc08 Rename ErrorTypes 2019-07-05 15:52:37 +02:00
4a512d2425 Create enum for errorType and fix a few issues 2019-07-05 15:43:28 +02:00
07f80f43bd Display clear type 2019-07-05 15:15:55 +02:00
87dec337d8 Rework Crypto using Try 2019-07-05 14:41:32 +02:00
b37877746a Introduce TimelineEventEntity to begin with the rework 2019-07-05 14:39:15 +02:00
b0e5612bdc Convert java-esque code to Kotlin
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-05 12:32:21 +01:00
25b0cd0e4b Remove some work from UI thread 2019-07-04 19:02:37 +02:00
2800d86a57 Merge pull request #302 from vector-im/feature/invitation
Quick action in invitation and composer input type
2019-07-04 18:47:44 +02:00
01bc0de2c2 Set again input type for composer, lost after merge 2019-07-04 18:43:36 +02:00
857a4c5a26 Quick implementation of accept/reject invitation from notification 2019-07-04 18:14:39 +02:00
063c35380a Fix regression on invitation full screen display 2019-07-04 16:44:37 +02:00
5322251bc0 Fix wording for direct message tab 2019-07-04 15:37:19 +02:00
c21b9df9a5 Fix issue with notification from previous account displayed after logout 2019-07-04 15:23:59 +02:00
f2a52f0253 Merge pull request #297 from vector-im/feature/crypto_stabilization
Safely remove all usage of `!![`
2019-07-04 15:17:26 +02:00
baaf493cb4 Merge pull request #299 from vector-im/feature/dix_concurrent_sync
Fix / Push worker could launch concurrent syncs
2019-07-04 15:10:18 +02:00
6cbd6d3a33 Valere's review 2019-07-04 14:59:29 +02:00
72e5aa981a Merge pull request #298 from vector-im/feature/quote
Fix issue when quoting event in e2e rooms (Fixes #295)
2019-07-04 14:49:53 +02:00
c0f085cdf8 SyncTask now handles by itself the sync token 2019-07-04 14:46:59 +02:00
10bc2297d4 Fix / Push worker could launch concurrent syncs 2019-07-04 14:04:36 +02:00
8fa5e63b07 Fix issue: reply to e2e event does not contain the base message 2019-07-04 12:52:43 +02:00
9d0c50907c Fix issue when quoting event in e2e rooms (Fixes #295) 2019-07-04 12:39:59 +02:00
e5958983d8 Safely remove all usage of !![ 2019-07-04 11:44:09 +02:00
ab23ec3f35 Fix https://github.com/matrix-org/riot-android-rageshakes/issues/5851 (DI) 2019-07-04 10:20:50 +02:00
a79a6443e7 Realm: update realm dependencie 2019-07-03 20:08:27 +02:00
9ff24cbf2a Merge branch 'feature/fix_issues' into develop 2019-07-03 19:46:34 +02:00
2eee25bbc1 Fix / crash not called on UI Thread 2019-07-03 19:36:25 +02:00
2a2431e490 Merge pull request #290 from vector-im/feature/fix_crash_npe_cryptomanager
Fix / Rageshake crashes + cleaning
2019-07-03 18:47:45 +02:00
4041e2e8ca code review 2019-07-03 18:40:42 +02:00
031c4e5746 Crash on loggout
https://github.com/matrix-org/riot-android-rageshakes/issues/5881
2019-07-03 18:40:04 +02:00
b4ea85fc76 Fix / Rageshake crashes + cleaning !!
https://github.com/matrix-org/riot-android-rageshakes/issues/5880
https://github.com/matrix-org/riot-android-rageshakes/issues/5877
https://github.com/matrix-org/riot-android-rageshakes/issues/5873
https://github.com/matrix-org/riot-android-rageshakes/issues/5871
2019-07-03 18:40:04 +02:00
480f14902d Rx: observe on computation by default 2019-07-03 18:28:56 +02:00
20c8e8d922 Change the test to apply Google Service plugin to be able to run sonar 2019-07-03 18:18:07 +02:00
9cdecced57 Merge pull request #291 from vector-im/feature/start_crypto_earlier
Start crypto manager before handling first sync events
2019-07-03 18:05:44 +02:00
60d46538de Merge pull request #292 from vector-im/feature/sonar_fix
Feature sonar fix and convert remaining Java files to Kotlin
2019-07-03 18:03:23 +02:00
223295c2f1 Convert MXUsersDevicesMap to kotlin - Fix issue 2019-07-03 18:01:28 +02:00
f789fb275d Convert MXUsersDevicesMap to kotlin 2019-07-03 17:34:22 +02:00
a7c12aeb93 Start crypto manager before handling first sync events 2019-07-03 17:17:58 +02:00
0ca9a5f68b Convert MXKey to kotlin 2019-07-03 16:45:08 +02:00
842345df9b Merge pull request #284 from vector-im/feature/better_incoming_key_verif_mgmt
Moved incoming key/verif to active session holder
2019-07-03 15:54:15 +02:00
7d5c31c510 Fix Javadoc issues 2019-07-03 15:52:53 +02:00
1ee1c31b9c Fix bugs detected by Sonar 2019-07-03 15:42:35 +02:00
e9eada77f9 Add comment to run sonar analysis and fix compilation issue 2019-07-03 15:42:35 +02:00
93ce0cc5e9 Realm: avoid using monarchy thread for custom work 2019-07-03 14:48:45 +02:00
eefd09d022 Dagger: don't create MatrixCoroutineDispatchers multiple time!! 2019-07-03 14:48:03 +02:00
ef597cc67a RoomSummary: set unreadNotification to 0 by default 2019-07-03 14:47:33 +02:00
5d171e0240 Moved incoming key/verif to active session holder 2019-07-03 12:56:08 +02:00
39070820be Merge pull request #283 from vector-im/feature/check_pushrule_on_sync_only
Check Push rule on sync only + fix bad room name in notif
2019-07-03 12:37:49 +02:00
1fdad38b9d Check Push rule on sync only + fix bad room name in notif 2019-07-03 11:59:45 +02:00
f41c0311fa Fix done TODO 2019-07-03 11:58:50 +02:00
a476ac71da Import translations from Riot 2019-07-03 10:20:07 +02:00
4b971a9e67 README: Fix develop build links 2019-07-03 10:04:35 +02:00
bc2d321a84 Merge branch 'feature/Perf' into develop 2019-07-02 23:07:16 +02:00
9adeab6bae Perf: revert constraintLayout version as it breaks at the moment 2019-07-02 23:06:40 +02:00
0f3a63e366 Enhance CancelableBag
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-02 21:46:44 +01:00
d90698fe92 Merge pull request #279 from vector-im/feature/clear_glide_cache
Clear media cache from the settings and clear cache when signing out
2019-07-02 21:56:31 +02:00
af0af6e260 Fix bad background color in dark theme 2019-07-02 21:49:52 +02:00
6e71fb565a Fix bad layout for button when keyboard is opened (Fixes #268) 2019-07-02 21:23:57 +02:00
6c66ab1568 Fix code quality 2019-07-02 21:17:41 +02:00
0d329f0338 Clear media cache from the settings and clear cache when signing out 2019-07-02 20:21:40 +02:00
2f66321c2a RoomSummary: don't fetch last event by default as it takes some time 2019-07-02 19:59:01 +02:00
5b102485bc Perf: timeline should reuse one background looper thread 2019-07-02 19:12:20 +02:00
698fc35704 README: Put back link to #riotx:matrix.org 2019-07-02 18:41:28 +02:00
37199da52f Merge branch 'develop' into feature/Perf 2019-07-02 18:29:59 +02:00
1c69d8e425 README: Update it for the beta launch 2019-07-02 18:06:43 +02:00
11bf00030d Merge branch 'develop' into feature/Perf 2019-07-02 17:00:09 +02:00
9378d30601 Merge branch 'develop' into feature/Perf 2019-07-02 11:25:39 +02:00
41ed4b23d8 Update dependencies (tested ok) 2019-07-02 09:39:45 +02:00
de9a5a3d12 Perf: eventHtmlRenderer is slow to build, get only one instance 2019-07-01 20:19:50 +02:00
19202cfca6 Perf: try to get better 2019-07-01 20:05:48 +02:00
409 changed files with 9359 additions and 5397 deletions

View File

@ -14,7 +14,7 @@ steps:
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/gplay/debug/*.apk"
branches: "develop feature/*"
branches: "!master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
@ -28,7 +28,7 @@ steps:
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/fdroid/debug/*.apk"
branches: "develop feature/*"
branches: "!master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"

View File

@ -1,24 +1,34 @@
Changes in RiotX 0.XX (2019-XX-XX)
Changes in RiotX 0.2.0 (2019-07-18)
===================================================
Features:
- Contextual action menu for messages in room
- Message Editing: View edit history (#121)
- Rooms filtering (#304)
- Edit in encrypted room
Improvements:
-
- Handle click on redacted events: view source and create permalink
- Improve long tap menu: reply on top, more compact (#368)
- Quick reply in timeline with swipe gesture (#167)
- Improve edit of replies
- Improve performance on Room Members and Users management (#381)
Other changes:
-
- migrate from rxbinding 2 to rxbinding 3
Bugfix:
-
- Fix regression on permalink click
- Fix crash reported by the PlayStore (#341)
- Fix Chat composer separator color in dark/black theme
- Fix bad layout for room directory filter (#349)
- Fix Copying link from a message shouldn't open context menu (#364)
Translations:
-
Changes in RiotX 0.1.0 (2019-07-11)
===================================================
Build:
-
First release!
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771
=======================================================

View File

@ -1,4 +1,4 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android)
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
@ -7,14 +7,31 @@
# RiotX Android
RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.
RiotX is an Android Matrix Client currently in beta but in active development.
It's based on a new Matrix SDK, written in Kotlin.
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
Download nightly build here: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
# New Android SDK
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.
# Roadmap
The current target is to release an application out of beta with the same level of features (and even more) as Riot.
The roadmap has 3 phases:
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
Matrix Room: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
## Contributing
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).

View File

@ -1,9 +1,9 @@
import javax.tools.JavaCompiler
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.21'
ext.koin_version = '1.0.2'
// TODO ext.koin_version = '2.0.0-GA'
repositories {
google()
jcenter()
@ -26,16 +26,47 @@ buildscript {
allprojects {
repositories {
maven { url "http://dl.bintray.com/piasy/maven" }
maven { url 'https://jitpack.io' }
// For olm library. This has to be declared first, to ensure that Olm library is not downloaded from another repo
maven {
url 'https://jitpack.io'
content {
// Use this repo only for olm library
includeGroupByRegex "org\\.matrix\\.gitlab\\.matrix-org"
// And also for FilePicker
includeGroupByRegex "com\\.github\\.jaiselrahman"
// And monarchy
includeGroupByRegex "com\\.github\\.Zhuinden"
}
}
maven {
url "http://dl.bintray.com/piasy/maven"
content {
includeGroupByRegex "com\\.github\\.piasy"
}
}
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
jcenter()
// For Olm SDK
maven {
url 'https://jitpack.io'
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
}
tasks.withType(JavaCompile).all {
options.compilerArgs += [
'-Adagger.gradle.incremental=enabled'
]
}
afterEvaluate {
extensions.findByName("kapt")?.arguments {
arg("dagger.gradle.incremental", "enabled")
}
}
}
task clean(type: Delete) {
@ -44,6 +75,10 @@ task clean(type: Delete) {
apply plugin: 'org.sonarqube'
// To run a sonar analysis:
// Run './gradlew sonarqube -Dsonar.login=<REPLACE_WITH_SONAR_KEY>'
// The SONAR_KEY is stored in passbolt
sonarqube {
properties {
property "sonar.projectName", "RiotX-Android"
@ -69,3 +104,23 @@ project(":vector") {
}
}
}
//project(":matrix-sdk-android") {
// sonarqube {
// properties {
// property "sonar.sources", project(":matrix-sdk-android").android.sourceSets.main.java.srcDirs
// // exclude source code from analyses separated by a colon (:)
// // property "sonar.exclusions", "**/*.*"
// }
// }
//}
//
//project(":matrix-sdk-android-rx") {
// sonarqube {
// properties {
// property "sonar.sources", project(":matrix-sdk-android-rx").android.sourceSets.main.java.srcDirs
// // exclude source code from analyses separated by a colon (:)
// // property "sonar.exclusions", "**/*.*"
// }
// }
//}

View File

@ -1,4 +1,4 @@
This document aims to describe how Riot X android displays notifications to the end user. It also clarifies notifications and background settings in the app.
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app.
# Table of Contents
1. [Prerequisites Knowledge](#prerequisites-knowledge)
@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
When the Riot X Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
## How does a mobile app receives push notification
@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
This server is called a **Push Gateway** in the matrix world
That means that Riot X Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
@ -223,7 +223,7 @@ Upon reception of the FCM push, RiotX will perform a sync call to the Home Serve
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
Riot X implements several strategies in these cases (TODO document)
RiotX implements several strategies in these cases (TODO document)
## FCM Fallback mode

View File

@ -35,11 +35,11 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":matrix-sdk-android")
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

View File

@ -56,6 +56,6 @@ private class LiveDataObservable<T>(
}
}
fun <T> LiveData<T>.asObservable(defaultValue: T? = null): Observable<T> {
return LiveDataObservable(this, defaultValue)
fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this)
}

View File

@ -21,23 +21,24 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<RoomSummary> {
return room.liveRoomSummary.asObservable()
return room.liveRoomSummary().asObservable().observeOn(Schedulers.computation())
}
fun liveRoomMemberIds(): Observable<List<String>> {
return room.getRoomMemberIdsLive().asObservable()
return room.getRoomMemberIdsLive().asObservable().observeOn(Schedulers.computation())
}
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
return room.getEventSummaryLive(eventId).asObservable()
return room.getEventSummaryLive(eventId).asObservable().observeOn(Schedulers.computation())
}
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
return room.liveTimeLineEvent(eventId).asObservable()
return room.liveTimeLineEvent(eventId).asObservable().observeOn(Schedulers.computation())
}
}

View File

@ -21,29 +21,25 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
class RxSession(private val session: Session) {
fun liveRoomSummaries(): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable()
return session.liveRoomSummaries().asObservable().observeOn(Schedulers.computation())
}
fun liveGroupSummaries(): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable()
return session.liveGroupSummaries().asObservable().observeOn(Schedulers.computation())
}
fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable()
return session.syncState().asObservable().observeOn(Schedulers.computation())
}
fun livePushers(): Observable<List<Pusher>> {
return session.livePushers().asObservable()
}
fun liveUser(userId: String): Observable<User?> {
return session.observeUser(userId).asObservable(User(userId))
return session.livePushers().asObservable().observeOn(Schedulers.computation())
}
}

View File

@ -6,20 +6,14 @@ apply plugin: 'realm-android'
apply plugin: 'okreplay'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:5.9.0"
classpath "io.realm:realm-gradle-plugin:5.12.0"
}
}
repositories {
google()
jcenter()
}
androidExtensions {
experimental = true
}
@ -33,6 +27,8 @@ android {
targetSdkVersion 28
versionCode 1
versionName "0.0.1"
// Multidex is useful for tests
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
@ -66,6 +62,11 @@ android {
lintOptions {
lintConfig file("lint.xml")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
static def gitRevision() {
@ -86,7 +87,7 @@ static def gitRevisionDate() {
dependencies {
def arrow_version = "0.8.0"
def support_version = '1.1.0-alpha03'
def support_version = '1.1.0-beta01'
def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1"
@ -98,16 +99,16 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:$support_version"
implementation "androidx.recyclerview:recyclerview:$support_version"
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta01"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// Network
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.novoda:merlin:1.1.6'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
@ -120,7 +121,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work
implementation "androidx.work:work-runtime-ktx:2.1.0-beta01"
implementation "androidx.work:work-runtime-ktx:2.1.0-rc01"
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -148,18 +149,16 @@ dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.0.2'
testImplementation "org.koin:koin-test:$koin_version"
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
androidTestImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"

View File

@ -19,4 +19,4 @@ package im.vector.matrix.android
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers.Main
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main)
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main)

View File

@ -19,25 +19,18 @@ package im.vector.matrix.android.auth
import androidx.test.annotation.UiThreadTest
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.di.MatrixModule
import im.vector.matrix.android.internal.di.NetworkModule
import okreplay.*
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.standalone.StandAloneContext.loadKoinModules
import org.koin.standalone.inject
import org.koin.test.KoinTest
@RunWith(AndroidJUnit4::class)
internal class AuthenticatorTest : InstrumentedTest, KoinTest {
internal class AuthenticatorTest : InstrumentedTest {
lateinit var authenticator: Authenticator
lateinit var okReplayInterceptor: OkReplayInterceptor

View File

@ -52,7 +52,7 @@ internal class JsonCanonicalizerTest : InstrumentedTest {
@Test
fun realSampleTest() {
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}"""))
}

View File

@ -59,7 +59,7 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
chunk.timelineEvents.size shouldEqual 1
}
}
@ -70,7 +70,7 @@ internal class ChunkEntityTest : InstrumentedTest {
val fakeEvent = createFakeMessageEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1
chunk.timelineEvents.size shouldEqual 1
}
}
@ -126,7 +126,7 @@ internal class ChunkEntityTest : InstrumentedTest {
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 60
chunk1.timelineEvents.size shouldEqual 60
}
}
@ -142,7 +142,7 @@ internal class ChunkEntityTest : InstrumentedTest {
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 40
chunk1.timelineEvents.size shouldEqual 40
chunk1.isLastForward.shouldBeTrue()
}
}

View File

@ -18,25 +18,6 @@ package im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm
import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before
import org.junit.Test
import timber.log.Timber
import java.util.concurrent.CountDownLatch
internal class TimelineTest : InstrumentedTest {

View File

@ -87,7 +87,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
instance = Matrix(appContext, matrixConfiguration)
} else {
throw IllegalStateException("Matrix is not initialized properly. You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
throw IllegalStateException("Matrix is not initialized properly." +
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
}
}
return instance

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.api
import java.util.regex.Pattern
/**
* This class contains pattern to match the different Matrix ids
@ -29,31 +28,31 @@ object MatrixPatterns {
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room ids in a string.
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room aliases in a string.
private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE)
// Ref: https://matrix.org/docs/spec/rooms/v4#event-ids
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V4_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find group ids in a string.
private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find permalink with message id.
// Android does not support in URL so extract it.
@ -62,16 +61,16 @@ object MatrixPatterns {
const val SEP_REGEX = "/"
private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE)
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
// list of patterns to find some matrix item.
val MATRIX_PATTERNS = listOf(
@ -93,7 +92,7 @@ object MatrixPatterns {
* @return true if the string is a valid user id
*/
fun isUserId(str: String?): Boolean {
return str != null && PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.matcher(str).matches()
return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
}
/**
@ -103,7 +102,7 @@ object MatrixPatterns {
* @return true if the string is a valid room Id
*/
fun isRoomId(str: String?): Boolean {
return str != null && PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER.matcher(str).matches()
return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER
}
/**
@ -113,7 +112,7 @@ object MatrixPatterns {
* @return true if the string is a valid room alias.
*/
fun isRoomAlias(str: String?): Boolean {
return str != null && PATTERN_CONTAIN_MATRIX_ALIAS.matcher(str).matches()
return str != null && str matches PATTERN_CONTAIN_MATRIX_ALIAS
}
/**
@ -124,9 +123,9 @@ object MatrixPatterns {
*/
fun isEventId(str: String?): Boolean {
return str != null
&& (PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches()
|| PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3.matcher(str).matches()
|| PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4.matcher(str).matches())
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
}
/**
@ -136,6 +135,6 @@ object MatrixPatterns {
* @return true if the string is a valid group id.
*/
fun isGroupId(str: String?): Boolean {
return str != null && PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER.matcher(str).matches()
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
}
}

View File

@ -36,17 +36,15 @@ interface Authenticator {
*/
fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback<Session>): Cancelable
//TODO remove this method. Shouldn't be managed like that.
/**
* Check if there is an active [Session].
* Check if there is an authenticated [Session].
* @return true if there is at least one active session.
*/
fun hasAuthenticatedSessions(): Boolean
//TODO remove this method. Shouldn't be managed like that.
/**
* Get the last active [Session], if there is an active session.
* @return the lastActive session if any, or null
* Get the last authenticated [Session], if there is an active session.
* @return the last active session if any, or null
*/
fun getLastAuthenticatedSession(): Session?
@ -58,7 +56,4 @@ interface Authenticator {
* @return the associated session if any, or null
*/
fun getSession(sessionParams: SessionParams): Session?
}

View File

@ -38,7 +38,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
data class CryptoError(val error: MXCryptoError) : Failure(error)
abstract class FeatureFailure : Failure()

View File

@ -37,15 +37,13 @@ object MatrixLinkify {
}
val text = spannable.toString()
var hasMatch = false
for (index in MatrixPatterns.MATRIX_PATTERNS.indices) {
val pattern = MatrixPatterns.MATRIX_PATTERNS[index]
val matcher = pattern.matcher(spannable)
while (matcher.find()) {
for (pattern in MatrixPatterns.MATRIX_PATTERNS) {
for (match in pattern.findAll(spannable)) {
hasMatch = true
val startPos = matcher.start(0)
val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = matcher.end(0)
val url = text.substring(matcher.start(0), matcher.end(0))
val endPos = match.range.last + 1
val url = text.substring(match.range)
val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}

View File

@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import java.util.regex.Pattern
@ -35,15 +34,15 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
}
fun isSatisfied(event: Event, displayName: String): Boolean {
//TODO the spec says:
// Matches any message whose content is unencrypted and contains the user's current display name
var message = when (event.type) {
EventType.MESSAGE -> {
event.content.toModel<MessageContent>()
}
// EventType.ENCRYPTED -> {
// event.root.getClearContent()?.toModel<MessageContent>()
// }
//TODO the spec says:
// Matches any message whose content is unencrypted and contains the user's current display name
// EventType.ENCRYPTED -> {
// event.root.getClearContent()?.toModel<MessageContent>()
// }
else -> null
} ?: return false

View File

@ -40,7 +40,8 @@ data class PushCondition(
/**
* Required for room_member_count conditions.
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth. If no prefix is present, this parameter defaults to ==.
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth.
* If no prefix is present, this parameter defaults to ==.
*/
@Json(name = "is") val iz: String? = null
) {

View File

@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session
package im.vector.matrix.android.internal.session;
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
interface InitialSyncProgressService {
import javax.inject.Scope;
fun getLiveStatus() : LiveData<Status?>
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Scope
@Documented
@Retention(RUNTIME)
public @interface SessionScope {}
data class Status(
@StringRes val statusText: Int?,
val percentProgress: Int = 0
)
}

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -46,15 +47,20 @@ interface Session :
CacheService,
SignOutService,
FilterService,
FileService,
PushRuleService,
PushersService {
PushersService,
InitialSyncProgressService {
/**
* The params associated to the session
*/
val sessionParams: SessionParams
val myUserId : String
/**
* Useful shortcut to get access to the userId
*/
val myUserId: String
get() = sessionParams.credentials.userId
@ -81,7 +87,7 @@ interface Session :
/**
* This method start the sync thread.
*/
fun startSync()
fun startSync(fromForeground : Boolean)
/**
* This method stop the sync thread.

View File

@ -28,10 +28,11 @@ interface ContentUploadStateTracker {
sealed class State {
object Idle : State()
data class ProgressData(val current: Long, val total: Long) : State()
object EncryptingThumbnail : State()
data class UploadingThumbnail(val current: Long, val total: Long) : State()
object Encrypting : State()
data class Uploading(val current: Long, val total: Long) : State()
object Success : State()
object Failure : State()
data class Failure(val throwable: Throwable) : State()
}
}

View File

@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import java.io.File
interface CryptoService {
@ -85,6 +87,8 @@ interface CryptoService {
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener)
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
@ -96,9 +100,10 @@ interface CryptoService {
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
@Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult?>)
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
fun getEncryptionAlgorithm(roomId: String): String?

View File

@ -18,106 +18,65 @@
package im.vector.matrix.android.api.session.crypto
import android.text.TextUtils
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import org.matrix.olm.OlmException
/**
* Represents a crypto error response.
*/
class MXCryptoError(var code: String,
var message: String) {
sealed class MXCryptoError : Throwable() {
data class Base(val errorType: ErrorType,
val technicalMessage: String,
/**
* Describe the error with more details
*/
private var mDetailedErrorDescription: String? = null
val detailedErrorDescription: String? = null) : MXCryptoError()
/**
* Data exception.
* Some exceptions provide some data to describe the exception
*/
var mExceptionData: Any? = null
data class OlmError(val olmException: OlmException) : MXCryptoError()
/**
* @return true if the current error is an olm one.
*/
val isOlmError: Boolean
get() = OLM_ERROR_CODE == code
data class UnknownDevice(val deviceList: MXUsersDevicesMap<MXDeviceInfo>) : MXCryptoError()
/**
* @return the detailed error description
*/
val detailedErrorDescription: String?
get() = if (TextUtils.isEmpty(mDetailedErrorDescription)) {
message
} else mDetailedErrorDescription
/**
* Create a crypto error
*
* @param code the error code (see XX_ERROR_CODE)
* @param shortErrorDescription the short error description
* @param detailedErrorDescription the detailed error description
*/
constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?) : this(code, shortErrorDescription) {
mDetailedErrorDescription = detailedErrorDescription
}
/**
* Create a crypto error
*
* @param code the error code (see XX_ERROR_CODE)
* @param shortErrorDescription the short error description
* @param detailedErrorDescription the detailed error description
* @param exceptionData the exception data
*/
constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?, exceptionData: Any) : this(code, shortErrorDescription) {
mDetailedErrorDescription = detailedErrorDescription
mExceptionData = exceptionData
enum class ErrorType {
ENCRYPTING_NOT_ENABLED,
UNABLE_TO_ENCRYPT,
UNABLE_TO_DECRYPT,
UNKNOWN_INBOUND_SESSION_ID,
INBOUND_SESSION_MISMATCH_ROOM_ID,
MISSING_FIELDS,
BAD_EVENT_FORMAT,
MISSING_SENDER_KEY,
MISSING_CIPHER_TEXT,
BAD_DECRYPTED_FORMAT,
NOT_INCLUDE_IN_RECIPIENTS,
BAD_RECIPIENT,
BAD_RECIPIENT_KEY,
FORWARDED_MESSAGE,
BAD_ROOM,
BAD_ENCRYPTED_MESSAGE,
DUPLICATED_MESSAGE_INDEX,
MISSING_PROPERTY,
OLM,
UNKNOWN_DEVICES,
UNKNOWN_MESSAGE_INDEX
}
companion object {
/**
* Error codes
* Resource for technicalMessage
*/
const val ENCRYPTING_NOT_ENABLED_ERROR_CODE = "ENCRYPTING_NOT_ENABLED"
const val UNABLE_TO_ENCRYPT_ERROR_CODE = "UNABLE_TO_ENCRYPT"
const val UNABLE_TO_DECRYPT_ERROR_CODE = "UNABLE_TO_DECRYPT"
const val UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE = "UNKNOWN_INBOUND_SESSION_ID"
const val INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE = "INBOUND_SESSION_MISMATCH_ROOM_ID"
const val MISSING_FIELDS_ERROR_CODE = "MISSING_FIELDS"
const val MISSING_CIPHER_TEXT_ERROR_CODE = "MISSING_CIPHER_TEXT"
const val NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE = "NOT_INCLUDE_IN_RECIPIENTS"
const val BAD_RECIPIENT_ERROR_CODE = "BAD_RECIPIENT"
const val BAD_RECIPIENT_KEY_ERROR_CODE = "BAD_RECIPIENT_KEY"
const val FORWARDED_MESSAGE_ERROR_CODE = "FORWARDED_MESSAGE"
const val BAD_ROOM_ERROR_CODE = "BAD_ROOM"
const val BAD_ENCRYPTED_MESSAGE_ERROR_CODE = "BAD_ENCRYPTED_MESSAGE"
const val DUPLICATED_MESSAGE_INDEX_ERROR_CODE = "DUPLICATED_MESSAGE_INDEX"
const val MISSING_PROPERTY_ERROR_CODE = "MISSING_PROPERTY"
const val OLM_ERROR_CODE = "OLM_ERROR_CODE"
const val UNKNOWN_DEVICES_CODE = "UNKNOWN_DEVICES_CODE"
const val UNKNOWN_MESSAGE_INDEX = "UNKNOWN_MESSAGE_INDEX"
/**
* short error reasons
*/
const val UNABLE_TO_DECRYPT = "Unable to decrypt"
const val UNABLE_TO_ENCRYPT = "Unable to encrypt"
/**
* Detailed error reasons
*/
const val ENCRYPTING_NOT_ENABLED_REASON = "Encryption not enabled"
const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s"
const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s"
const val OLM_REASON = "OLM error: %1\$s"
const val DETAILLED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s"
const val DETAILED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s"
const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id"
const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)"
const val MISSING_FIELDS_REASON = "Missing fields in input"
const val BAD_EVENT_FORMAT_TEXT_REASON = "Bad event format"
const val MISSING_SENDER_KEY_TEXT_REASON = "Missing senderKey"
const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext"
const val BAD_DECRYPTED_FORMAT_TEXT_REASON = "Bad decrypted event format"
const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients"
const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s"
const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device"
@ -126,7 +85,9 @@ class MXCryptoError(var code: String,
const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message"
const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s"
const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack"
const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + "We strongly recommend you verify them before continuing."
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + " Perhaps the homeserver is hiding the configuration event."
const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" +
"We strongly recommend you verify them before continuing."
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." +
" Perhaps the homeserver is hiding the configuration event."
}
}

View File

@ -40,7 +40,8 @@ interface KeysBackupService {
* @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion].
* @param callback Asynchronous callback
*/
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback<KeysVersion>)
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
callback: MatrixCallback<KeysVersion>)
/**
* Facility method to get the total number of locally stored keys
@ -58,7 +59,8 @@ interface KeysBackupService {
* @param progressListener the callback to follow the progress
* @param callback the main callback
*/
fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback<Unit>?)
fun backupAllGroupSessions(progressListener: ProgressListener?,
callback: MatrixCallback<Unit>?)
/**
* Check trust on a key backup version.
@ -66,7 +68,8 @@ interface KeysBackupService {
* @param keysBackupVersion the backup version to check.
* @param callback block called when the operations completes.
*/
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, callback: MatrixCallback<KeysBackupVersionTrust>)
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
callback: MatrixCallback<KeysBackupVersionTrust>)
/**
* Return the current progress of the backup
@ -80,7 +83,8 @@ interface KeysBackupService {
* @param version the backup version
* @param callback
*/
fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>)
fun getVersion(version: String,
callback: MatrixCallback<KeysVersionResult?>)
/**
* This method fetches the last backup version on the server, then compare to the currently backup version use.
@ -114,7 +118,9 @@ interface KeysBackupService {
* @param progressListener a progress listener, as generating private key from password may take a while
* @param callback Asynchronous callback
*/
fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>)
fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>)
/**
* Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself.
@ -123,7 +129,8 @@ interface KeysBackupService {
* @param version the backup version to delete.
* @param callback Asynchronous callback
*/
fun deleteBackup(version: String, callback: MatrixCallback<Unit>?)
fun deleteBackup(version: String,
callback: MatrixCallback<Unit>?)
/**
* Ask if the backup on the server contains keys that we may do not have locally.
@ -139,7 +146,9 @@ interface KeysBackupService {
* @param trust the trust to set to the keys backup.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean, callback: MatrixCallback<Unit>)
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
trust: Boolean,
callback: MatrixCallback<Unit>)
/**
* Set trust on a keys backup version.
@ -148,7 +157,9 @@ interface KeysBackupService {
* @param recoveryKey the recovery key to challenge with the key backup public key.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String, callback: MatrixCallback<Unit>)
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String,
callback: MatrixCallback<Unit>)
/**
* Set trust on a keys backup version.
@ -157,7 +168,9 @@ interface KeysBackupService {
* @param password the pass phrase to challenge with the keyBackupVersion public key.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String, callback: MatrixCallback<Unit>)
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String,
callback: MatrixCallback<Unit>)
/**
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
@ -169,7 +182,11 @@ interface KeysBackupService {
* @param stepProgressListener the step progress listener
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
*/
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
recoveryKey: String, roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>)
/**
* Restore a backup with a password from a given backup version stored on the homeserver.
@ -181,7 +198,12 @@ interface KeysBackupService {
* @param stepProgressListener the step progress listener
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
*/
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
password: String,
roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>)
val keysBackupVersion: KeysVersionResult?
val currentBackupVersion: String?

View File

@ -21,11 +21,10 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.MoshiProvider
import org.json.JSONObject
import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap
typealias Content = JsonDict
@ -79,6 +78,11 @@ data class Event(
@Json(name = "redacts") val redacts: String? = null
) {
var mxDecryptionResult: OlmDecryptionResult? = null
var mCryptoError: MXCryptoError.ErrorType? = null
/**
* Check if event is a state event.
* @return true if event is state event.
@ -91,41 +95,41 @@ data class Event(
// Crypto
//==============================================================================================================
/**
* For encrypted events, the plaintext payload for the event.
* This is a small MXEvent instance with typically value for `type` and 'content' fields.
*/
@Transient
var mClearEvent: Event? = null
private set
/**
* Curve25519 key which we believe belongs to the sender of the event.
* See `senderKey` property.
*/
@Transient
private var mSenderCurve25519Key: String? = null
/**
* Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
* See `claimedEd25519Key` property.
*/
@Transient
private var mClaimedEd25519Key: String? = null
/**
* Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
* See `forwardingCurve25519KeyChain` property.
*/
@Transient
private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
/**
* Decryption error
*/
@Transient
var mCryptoError: MXCryptoError? = null
private set
// /**
// * For encrypted events, the plaintext payload for the event.
// * This is a small MXEvent instance with typically value for `type` and 'content' fields.
// */
// @Transient
// var mClearEvent: Event? = null
// private set
//
// /**
// * Curve25519 key which we believe belongs to the sender of the event.
// * See `senderKey` property.
// */
// @Transient
// private var mSenderCurve25519Key: String? = null
//
// /**
// * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
// * See `claimedEd25519Key` property.
// */
// @Transient
// private var mClaimedEd25519Key: String? = null
//
// /**
// * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
// * See `forwardingCurve25519KeyChain` property.
// */
// @Transient
// private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
//
// /**
// * Decryption error
// */
// @Transient
// var mCryptoError: MXCryptoError? = null
// private set
/**
* @return true if this event is encrypted.
@ -140,96 +144,151 @@ data class Event(
*
* @param decryptionResult the decryption result, including the plaintext and some key info.
*/
internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
mClearEvent = null
if (decryptionResult != null) {
if (decryptionResult.clearEvent != null) {
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
if (mClearEvent != null) {
mSenderCurve25519Key = decryptionResult.senderCurve25519Key
mClaimedEd25519Key = decryptionResult.claimedEd25519Key
mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
// For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
// in the clear event
try {
content?.get("m.relates_to")?.let { clearRelates ->
mClearEvent = mClearEvent?.copy(
content = HashMap(mClearEvent!!.content).apply {
this["m.relates_to"] = clearRelates
}
)
}
} catch (e: Exception) {
Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
}
}
}
}
mCryptoError = null
}
// internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
// mClearEvent = null
// if (decryptionResult != null) {
// if (decryptionResult.clearEvent != null) {
// val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
// mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
//
// if (mClearEvent != null) {
// mSenderCurve25519Key = decryptionResult.senderCurve25519Key
// mClaimedEd25519Key = decryptionResult.claimedEd25519Key
// mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
//
// // For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
// // in the clear event
// try {
// content?.get("m.relates_to")?.let { clearRelates ->
// mClearEvent = mClearEvent?.copy(
// content = HashMap(mClearEvent!!.content).apply {
// this["m.relates_to"] = clearRelates
// }
// )
// }
// } catch (e: Exception) {
// Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
// }
// }
//
//
// }
// }
// mCryptoError = null
// }
/**
* @return The curve25519 key that sent this event.
*/
fun getSenderKey(): String? {
return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
return mxDecryptionResult?.senderKey
// return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
}
/**
* @return The additional keys the sender of this encrypted event claims to possess.
*/
fun getKeysClaimed(): Map<String, String> {
val res = HashMap<String, String>()
val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
if (null != claimedEd25519Key) {
res["ed25519"] = claimedEd25519Key
return mxDecryptionResult?.keysClaimed ?: HashMap()
// val res = HashMap<String, String>()
//
// val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
//
// if (null != claimedEd25519Key) {
// res["ed25519"] = claimedEd25519Key
// }
//
// return res
}
return res
}
//
/**
* @return the event type
*/
fun getClearType(): String {
return mClearEvent?.type ?: type
return mxDecryptionResult?.payload?.get("type")?.toString()
?: type//get("type")?.toString() ?: type
}
/**
* @return the event content
*/
fun getClearContent(): Content? {
return mClearEvent?.content ?: content
return mxDecryptionResult?.payload?.get("content") as? Content ?: content
}
/**
* @return the linked crypto error
*/
fun getCryptoError(): MXCryptoError? {
return mCryptoError
// /**
// * @return the linked crypto error
// */
// fun getCryptoError(): MXCryptoError? {
// return mCryptoError
// }
//
// /**
// * Update the linked crypto error
// *
// * @param error the new crypto error.
// */
// fun setCryptoError(error: MXCryptoError?) {
// mCryptoError = error
// if (null != error) {
// mClearEvent = null
// }
// }
fun toContentStringWithIndent(): String {
val contentMap = this.toContent()?.toMutableMap() ?: HashMap()
contentMap.remove("mxDecryptionResult")
contentMap.remove("mCryptoError")
return JSONObject(contentMap).toString(4)
}
/**
* Update the linked crypto error
*
* @param error the new crypto error.
*/
fun setCryptoError(error: MXCryptoError?) {
mCryptoError = error
if (null != error) {
mClearEvent = null
}
fun toClearContentStringWithIndent(): String? {
val contentMap = this.mxDecryptionResult?.payload?.toMutableMap()
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
}
/**
* Tells if the event is redacted
*/
fun isRedacted() = unsignedData?.redactedEvent != null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Event
if (type != other.type) return false
if (eventId != other.eventId) return false
if (content != other.content) return false
if (prevContent != other.prevContent) return false
if (originServerTs != other.originServerTs) return false
if (senderId != other.senderId) return false
if (stateKey != other.stateKey) return false
if (roomId != other.roomId) return false
if (unsignedData != other.unsignedData) return false
if (redacts != other.redacts) return false
if (mxDecryptionResult != other.mxDecryptionResult) return false
if (mCryptoError != other.mCryptoError) return false
return true
}
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + (eventId?.hashCode() ?: 0)
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + (prevContent?.hashCode() ?: 0)
result = 31 * result + (originServerTs?.hashCode() ?: 0)
result = 31 * result + (senderId?.hashCode() ?: 0)
result = 31 * result + (stateKey?.hashCode() ?: 0)
result = 31 * result + (roomId?.hashCode() ?: 0)
result = 31 * result + (unsignedData?.hashCode() ?: 0)
result = 31 * result + (redacts?.hashCode() ?: 0)
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
return result
}
}

View File

@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.events.model
/**
* Constants defining known event relation types from Matrix specifications.
* Constants defining known event relation types from Matrix specifications
*/
object RelationType {
@ -25,7 +25,7 @@ object RelationType {
const val ANNOTATION = "m.annotation"
/** Lets you define an event which replaces an existing event.*/
const val REPLACE = "m.replace"
/** ets you define an event which references an existing event.*/
/** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference"
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.file
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import java.io.File
/**
* This interface defines methods to get files.
*/
interface FileService {
enum class DownloadMode {
/**
* Download file in external storage
*/
TO_EXPORT,
/**
* Download file in cache
*/
FOR_INTERNAL_USE
}
/**
* Download a file.
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
* You can pass the eventId
*/
fun downloadFile(
downloadMode: DownloadMode,
id: String,
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>)
}

View File

@ -47,8 +47,8 @@ interface Room :
* A live [RoomSummary] associated with the room
* You can observe this summary to get dynamic data from this room.
*/
val liveRoomSummary: LiveData<RoomSummary>
fun liveRoomSummary(): LiveData<RoomSummary>
val roomSummary: RoomSummary?
fun roomSummary(): RoomSummary?
}

View File

@ -210,13 +210,7 @@ class CreateRoomParams {
* @return the first invited user id
*/
fun getFirstInvitedUserId(): String? {
if (0 != getInviteCount()) {
return invitedUserIds!![0]
}
return if (0 != getInvite3PidCount()) {
invite3pids!![0].address
} else null
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
}
/**

View File

@ -42,7 +42,7 @@ data class MessageAudioContent(
/**
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -51,4 +51,4 @@ data class MessageAudioContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -26,3 +26,8 @@ interface MessageContent {
val relatesTo: RelationDefaultContent?
val newContent: Content?
}
fun MessageContent?.isReply(): Boolean {
return this?.relatesTo?.inReplyTo != null
}

View File

@ -20,8 +20,18 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
/**
* Interface for message which can contains encrypted data
* Interface for message which can contains an encrypted file
*/
interface MessageEncyptedContent : MessageContent {
interface MessageEncryptedContent : MessageContent {
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
val url: String?
val encryptedFileInfo: EncryptedFileInfo?
}
/**
* Get the url of the encrypted file or of the file
*/
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.api.session.room.model.message
import android.content.ClipDescription
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content
@ -47,10 +48,22 @@ data class MessageFileContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent {
fun getMimeType(): String {
// Mimetype default to plain text, should not be used
return encryptedFileInfo?.mimetype
?: info?.mimeType
?: ClipDescription.MIMETYPE_TEXT_PLAIN
}
fun getFileName(): String {
return filename ?: body
}
}

View File

@ -43,7 +43,7 @@ data class MessageImageContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -52,4 +52,4 @@ data class MessageImageContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -42,7 +42,7 @@ data class MessageVideoContent(
/**
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
*/
@Json(name = "url") val url: String? = null,
@Json(name = "url") override val url: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null,
@ -51,4 +51,4 @@ data class MessageVideoContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncyptedContent
) : MessageEncryptedContent

View File

@ -16,7 +16,10 @@
package im.vector.matrix.android.api.session.room.model.relation
import im.vector.matrix.android.api.session.events.model.RelationType
interface RelationContent {
/** See [RelationType] for known possible values */
val type: String?
val eventId: String?
val inReplyTo: ReplyToContent?

View File

@ -16,8 +16,10 @@
package im.vector.matrix.android.api.session.room.model.relation
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
/**
@ -51,7 +53,8 @@ interface RelationService {
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted
*/
fun sendReaction(reaction: String, targetEventId: String): Cancelable
fun sendReaction(reaction: String,
targetEventId: String): Cancelable
/**
@ -60,7 +63,9 @@ interface RelationService {
* @param targetEventId the id of the event being reacted
* @param myUserId used to know if a reaction event was made by the user
*/
fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable
fun undoReaction(reaction: String,
targetEventId: String,
myUserId: String)//: Cancelable
/**
@ -69,7 +74,30 @@ interface RelationService {
* @param newBodyText The edited body
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable
fun editTextMessage(targetEventId: String,
msgType: String,
newBodyText: String,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable
/**
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit
* @param originalTimelineEvent the message that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editReply(replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
compatibilityBodyText: String = "* $newBodyText"): Cancelable
/**
* Get's the edit history of the given event
*/
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)
/**
@ -77,8 +105,13 @@ interface RelationService {
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?
fun replyToMessage(eventReplied: TimelineEvent,
replyText: String,
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
}

View File

@ -67,7 +67,8 @@ data class PublicRoom(
var worldReadable: Boolean = false,
/**
* Required. Whether guest users may join the room and participate in it. If they can, they will be subject to ordinary power level rules like any other user.
* Required. Whether guest users may join the room and participate in it. If they can,
* they will be subject to ordinary power level rules like any other user.
*/
@Json(name = "guest_can_join")
var guestCanJoin: Boolean = false,

View File

@ -42,7 +42,7 @@ interface SendService {
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @return a [Cancelable]
*/
fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable
fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable
/**
* Method to send a media asynchronously.

View File

@ -18,8 +18,13 @@ package im.vector.matrix.android.api.session.room.timeline
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
/**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
@ -28,13 +33,12 @@ import im.vector.matrix.android.api.session.room.send.SendState
*/
data class TimelineEvent(
val root: Event,
val localId: String,
val localId: Long,
val displayIndex: Int,
val senderName: String?,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val sendState: SendState,
val hasClearEventFlag: Boolean = false,
val annotations: EventAnnotationsSummary? = null
) {
@ -81,3 +85,21 @@ data class TimelineEvent(
return EventType.ENCRYPTED == root.type
}
}
/**
* Get last MessageContent, after a possible edition
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
val lastContent = getLastMessageContent()
return if (isReply) {
return extractUsefulTextFromReply(lastContent?.body ?: "")
} else {
lastContent?.body ?: ""
}
}

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.sync
sealed class SyncState {
object IDLE : SyncState()
data class RUNNING(val catchingUp: Boolean) : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()

View File

@ -16,20 +16,8 @@
package im.vector.matrix.android.api.util
class CancelableBag : Cancelable {
private val cancelableList = ArrayList<Cancelable>()
fun add(cancelable: Cancelable) {
cancelableList.add(cancelable)
}
class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() {
override fun cancel() {
cancelableList.forEach { it.cancel() }
forEach { it.cancel() }
}
}
fun Cancelable.addTo(cancelables: CancelableBag) {
cancelables.add(this)
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.api.util
object ContentUtils {
fun extractUsefulTextFromReply(repliedBody: String): String {
val lines = repliedBody.lines()
var wellFormed = repliedBody.startsWith(">")
var endOfPreviousFound = false
val usefullines = ArrayList<String>()
lines.forEach {
if (it == "") {
endOfPreviousFound = true
return@forEach
}
if (!endOfPreviousFound) {
wellFormed = wellFormed && it.startsWith(">")
} else {
usefullines.add(it)
}
}
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
}
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
if (repliedBody.startsWith("<mx-reply>")) {
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
if (closingTagIndex != -1)
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
}
return repliedBody
}
}

View File

@ -21,13 +21,4 @@ import im.vector.matrix.android.api.MatrixCallback
/**
* Simple MatrixCallback implementation which delegate its calls to another callback
*/
open class MatrixCallbackDelegate<T>(private val callback: MatrixCallback<T>) : MatrixCallback<T> {
override fun onSuccess(data: T) {
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
open class MatrixCallbackDelegate<T>(private val callback: MatrixCallback<T>) : MatrixCallback<T> by callback

View File

@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
}
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
val userId = sessionParams.credentials.userId
if (sessionComponents.containsKey(userId)) {
return sessionComponents[userId]!!
}
return DaggerSessionComponent
return sessionComponents.getOrPut(sessionParams.credentials.userId) {
DaggerSessionComponent
.factory()
.create(matrixComponent, sessionParams)
.also {
sessionComponents[sessionParams.credentials.userId] = it
}
}
}

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
/**
@ -30,9 +31,11 @@ internal interface AuthAPI {
/**
* Pass params to the server for the current login phase.
* Set all the timeouts to 1 minute
*
* @param loginParams the login parameters
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>

View File

@ -21,10 +21,10 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import arrow.core.Try
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure
@ -72,15 +72,16 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.max
/**
* A `CryptoService` class instance manages the end-to-end crypto for a session.
@ -98,7 +99,7 @@ internal class CryptoManager @Inject constructor(
private val olmManager: OlmManager,
// The credentials,
private val credentials: Credentials,
private val myDeviceInfoHolder: MyDeviceInfoHolder,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
// the crypto store
private val cryptoStore: IMXCryptoStore,
// Olm device
@ -190,12 +191,12 @@ internal class CryptoManager @Inject constructor(
}
override fun getMyDevice(): MXDeviceInfo {
return myDeviceInfoHolder.myDevice
return myDeviceInfoHolder.get().myDevice
}
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask
.configureWith(Unit)
.toConfigurableTask()
.dispatchTo(callback)
.executeBy(taskExecutor)
}
@ -241,16 +242,16 @@ internal class CryptoManager @Inject constructor(
* @param isInitialSync true if it starts from an initial sync
*/
fun start(isInitialSync: Boolean) {
CoroutineScope(coroutineDispatchers.crypto).launch {
if (isStarted.get() || isStarting.get()) {
return
}
isStarting.set(true)
GlobalScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync)
}
}
private suspend fun internalStart(isInitialSync: Boolean) {
if (isStarted.get() || isStarting.get()) {
return
}
isStarting.set(true)
// Open the store
cryptoStore.open()
uploadDeviceKeys()
@ -312,7 +313,7 @@ internal class CryptoManager @Inject constructor(
* @param syncResponse the syncResponse
*/
fun onSyncCompleted(syncResponse: SyncResponse) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
}
@ -337,7 +338,7 @@ internal class CryptoManager @Inject constructor(
* @return the device info, or null if not found / unsupported algorithm / crypto released
*/
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
// We only deal in olm keys
null
} else cryptoStore.deviceWithIdentityKey(senderKey)
@ -350,8 +351,8 @@ internal class CryptoManager @Inject constructor(
* @param deviceId the device id
*/
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
cryptoStore.getUserDevice(deviceId!!, userId)
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(deviceId, userId)
} else {
null
}
@ -387,12 +388,12 @@ internal class CryptoManager @Inject constructor(
var isUpdated = false
val deviceIds = devicesIdListByUserId[userId]
for (deviceId in deviceIds!!) {
deviceIds?.forEach { deviceId ->
val device = storedDeviceIDs[deviceId]
// assume if the device is either verified or blocked
// it means that the device is known
if (null != device && device.isUnknown) {
if (device?.isUnknown == true) {
device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
isUpdated = true
}
@ -436,7 +437,7 @@ internal class CryptoManager @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false
}
@ -444,7 +445,7 @@ internal class CryptoManager @Inject constructor(
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
if (!encryptingClass) {
Timber.e("## setEncryptionInRoom() : Unable to encrypt with " + algorithm!!)
Timber.e("## setEncryptionInRoom() : Unable to encrypt room ${roomId} with $algorithm")
return false
}
@ -532,7 +533,7 @@ internal class CryptoManager @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false)
@ -559,7 +560,7 @@ internal class CryptoManager @Inject constructor(
.fold(
{ callback.onFailure(it) },
{
Timber.v("## encryptEventContent() : succeeds after " + (System.currentTimeMillis() - t0) + " ms")
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
}
)
@ -568,8 +569,7 @@ internal class CryptoManager @Inject constructor(
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## encryptEventContent() : $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, reason)))
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
}
}
}
@ -579,10 +579,10 @@ internal class CryptoManager @Inject constructor(
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error
* @return the MXEventDecryptionResult data, or throw in case of error
*/
@Throws(MXDecryptionException::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? {
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return runBlocking {
internalDecryptEvent(event, timeline).fold(
{ throw it },
@ -598,8 +598,8 @@ internal class CryptoManager @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult?>) {
GlobalScope.launch(EmptyCoroutineContext) {
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
GlobalScope.launch {
val result = withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
}
@ -614,18 +614,18 @@ internal class CryptoManager @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try]
*/
private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult?> {
return Try {
private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
val eventContent = event.content
if (eventContent == null) {
return if (eventContent == null) {
Timber.e("## decryptEvent : empty event content")
return@Try null
}
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, eventContent["algorithm"] as String)
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, eventContent["algorithm"] as String)
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.e("## decryptEvent() : $reason")
throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, MXCryptoError.UNABLE_TO_DECRYPT, reason))
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason))
} else {
alg.decryptEvent(event, timeline)
}
@ -647,7 +647,7 @@ internal class CryptoManager @Inject constructor(
* @param event the event
*/
fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event)
@ -669,13 +669,13 @@ internal class CryptoManager @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : missing fields")
return
}
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
if (alg == null) {
Timber.e("## onRoomKeyEvent() : Unable to handle keys for " + roomKeyContent.algorithm)
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return
}
alg.onRoomKeyEvent(event, keysBackup)
@ -687,13 +687,13 @@ internal class CryptoManager @Inject constructor(
* @param event the encryption event.
*/
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId)
loadRoomMembersTask
.execute(params)
.map { allLoaded ->
.map { _ ->
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content!!["algorithm"] as String, true, userIds)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
}
}
}
@ -736,7 +736,7 @@ internal class CryptoManager @Inject constructor(
val membership = roomMember?.membership
if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId)
&& cryptoConfig.enableEncryptionForInvitedMembers) {
@ -745,7 +745,7 @@ internal class CryptoManager @Inject constructor(
// know what other servers are in the room at the time they've been invited.
// They therefore will not send device updates if a user logs in whilst
// their state is invite.
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
deviceListManager.startTrackingDeviceList(listOf(userId))
}
}
}
@ -764,7 +764,7 @@ internal class CryptoManager @Inject constructor(
private suspend fun uploadDeviceKeys(): Try<KeysUploadResponse> {
// Prepare the device keys data to send
// Sign it
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
getMyDevice().signatures = objectSigner.signObject(canonicalJson)
// For now, we set the device id explicitly, as we may not be using the
@ -780,7 +780,11 @@ internal class CryptoManager @Inject constructor(
* @param callback the exported keys
*/
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
GlobalScope.launch(coroutineDispatchers.main) {
runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.fold(callback::onSuccess, callback::onFailure)
}
}
/**
@ -790,31 +794,17 @@ internal class CryptoManager @Inject constructor(
* @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys
*/
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
val iterationCount = Math.max(0, anIterationCount)
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
return withContext(coroutineDispatchers.crypto) {
val iterationCount = max(0, anIterationCount)
val exportedSessions = ArrayList<MegolmSessionData>()
val inboundGroupSessions = cryptoStore.getInboundGroupSessions()
for (session in inboundGroupSessions) {
val megolmSessionData = session.exportKeys()
if (null != megolmSessionData) {
exportedSessions.add(megolmSessionData)
}
}
val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }
val adapter = MoshiProvider.providesMoshi()
.adapter(List::class.java)
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
}
}.foldToCallback(callback)
}
}
/**
@ -838,7 +828,7 @@ internal class CryptoManager @Inject constructor(
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
val t1 = System.currentTimeMillis()
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
val importedSessions = MoshiProvider.providesMoshi()
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
@ -846,7 +836,7 @@ internal class CryptoManager @Inject constructor(
val t2 = System.currentTimeMillis()
Timber.v("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms")
Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms")
if (importedSessions == null) {
throw Exception("Error")
@ -870,14 +860,14 @@ internal class CryptoManager @Inject constructor(
/**
* Check if the user ids list have some unknown devices.
* A success means there is no unknown devices.
* If there are some unknown devices, a MXCryptoError.UNKNOWN_DEVICES_CODE exception is triggered.
* If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered.
*
* @param userIds the user ids list
* @param callback the asynchronous callback.
*/
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(userIds, true)
.fold(
@ -888,9 +878,7 @@ internal class CryptoManager @Inject constructor(
callback.onSuccess(Unit)
} else {
// trigger an an unknown devices exception
callback.onFailure(
Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)))
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
}
}
)
@ -930,11 +918,8 @@ internal class CryptoManager @Inject constructor(
*/
// TODO add this info in CryptoRoomEntity?
override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
return if (null != roomId) {
cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
} else {
false
}
return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
?: false
}
/**
@ -947,7 +932,7 @@ internal class CryptoManager @Inject constructor(
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
if (add) {
if (!roomIds.contains(roomId)) {
if (roomId !in roomIds) {
roomIds.add(roomId)
}
} else {
@ -992,18 +977,18 @@ internal class CryptoManager @Inject constructor(
* @param event the event to decrypt again.
*/
override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content!!
val algorithm = wireContent["algorithm"].toString()
val senderKey = wireContent["sender_key"].toString()
val sessionId = wireContent["session_id"].toString()
val wireContent = event.content
if (wireContent == null) {
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
return
}
val requestBody = RoomKeyRequestBody()
requestBody.roomId = event.roomId
requestBody.algorithm = algorithm
requestBody.senderKey = senderKey
requestBody.sessionId = sessionId
requestBody.algorithm = wireContent["algorithm"]?.toString()
requestBody.senderKey = wireContent["sender_key"]?.toString()
requestBody.sessionId = wireContent["session_id"]?.toString()
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
}
@ -1022,7 +1007,7 @@ internal class CryptoManager @Inject constructor(
*
* @param listener listener
*/
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
}
@ -1036,12 +1021,11 @@ internal class CryptoManager @Inject constructor(
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
val userIds = devicesInRoom.userIds
for (userId in userIds) {
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(deviceId, userId)
if (deviceInfo!!.isUnknown) {
unknownDevices.setObject(deviceInfo, userId, deviceId)
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
devicesInRoom.getObject(userId, deviceId)
?.takeIf { it.isUnknown }
?.let {
unknownDevices.setObject(userId, deviceId, it)
}
}
}
@ -1050,7 +1034,7 @@ internal class CryptoManager @Inject constructor(
}
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(userIds, forceDownload)
.foldToCallback(callback)
@ -1058,7 +1042,8 @@ internal class CryptoManager @Inject constructor(
}
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
clearCryptoDataTask.configureWith(Unit)
clearCryptoDataTask
.toConfigurableTask()
.dispatchTo(callback)
.executeBy(taskExecutor)
}

View File

@ -76,7 +76,7 @@ internal abstract class CryptoModule {
@Provides
fun providesCryptoStore(@CryptoDatabase
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
return RealmCryptoStore(false /* TODO*/,
return RealmCryptoStore(
realmConfiguration,
credentials)
}
@ -168,8 +168,10 @@ internal abstract class CryptoModule {
abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask
@Binds
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
: ClaimOneTimeKeysForUsersDeviceTask
@Binds
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask)
: DeleteDeviceWithUserPasswordTask
}

View File

@ -21,11 +21,11 @@ import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.extensions.onError
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.extensions.onError
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import timber.log.Timber
@ -221,7 +221,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
Timber.v("Device list for $userId now up to date")
}
// And the response result
usersDevicesInfoMap.setObjects(devices, userId)
usersDevicesInfoMap.setObjects(userId, devices)
}
}
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
@ -239,7 +239,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): Try<MXUsersDevicesMap<MXDeviceInfo>> {
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
// Map from userid -> deviceid -> DeviceInfo
// Map from userId -> deviceId -> MXDeviceInfo
val stored = MXUsersDevicesMap<MXDeviceInfo>()
// List of user ids we need to download keys for
@ -258,7 +258,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val devices = cryptoStore.getUserDevices(userId)
// should always be true
if (devices != null) {
stored.setObjects(devices, userId)
stored.setObjects(userId, devices)
} else {
downloadUsers.add(userId)
}
@ -380,14 +380,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
val signKeyId = "ed25519:" + deviceKeys.deviceId
val signKey = deviceKeys.keys!![signKeyId]
val signKey = deviceKeys.keys?.get(signKeyId)
if (null == signKey) {
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key")
return false
}
val signatureMap = deviceKeys.signatures!![userId]
val signatureMap = deviceKeys.signatures?.get(userId)
if (null == signatureMap) {
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId)

View File

@ -84,7 +84,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
return
}
// todo: should we queue up requests we don't yet have keys for, in case they turn up later?
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
// if we don't have a decryptor for this room/alg, we don't have
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)

View File

@ -1,39 +0,0 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* 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 im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.session.crypto.MXCryptoError
/**
* This class represents a decryption exception
*/
class MXDecryptionException
(
/**
* the linked crypto error
*/
val cryptoError: MXCryptoError?
) : Exception() {
override val message: String?
get() = cryptoError?.message ?: super.message
override fun getLocalizedMessage(): String {
return cryptoError?.message ?: super.getLocalizedMessage()
}
}

View File

@ -1,6 +1,5 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,7 +17,6 @@
package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.util.JsonDict
import java.util.*
/**
* The result of a (successful) call to decryptEvent.
@ -28,23 +26,23 @@ data class MXEventDecryptionResult(
/**
* The plaintext payload for the event (typically containing "type" and "content" fields).
*/
var clearEvent: JsonDict? = null,
val clearEvent: JsonDict,
/**
* Key owned by the sender of this event.
* See MXEvent.senderKey.
*/
var senderCurve25519Key: String? = null,
val senderCurve25519Key: String? = null,
/**
* Ed25519 key claimed by the sender of this event.
* See MXEvent.claimedEd25519Key.
*/
var claimedEd25519Key: String? = null,
val claimedEd25519Key: String? = null,
/**
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
*/
var forwardingCurve25519KeyChain: List<String> = ArrayList()
val forwardingCurve25519KeyChain: List<String> = emptyList()
)

View File

@ -229,7 +229,7 @@ object MXMegolmExportEncryption {
throw Exception("Header line not found")
}
val line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' }
val line = fileStr.substring(lineStart, lineEnd).trim()
// start the next line after the newline
lineStart = lineEnd + 1
@ -247,9 +247,9 @@ object MXMegolmExportEncryption {
val line: String
if (lineEnd < 0) {
line = fileStr.substring(lineStart).trim { it <= ' ' }
line = fileStr.substring(lineStart).trim()
} else {
line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' }
line = fileStr.substring(lineStart, lineEnd).trim()
}
if (TextUtils.equals(line, TRAILER_LINE)) {

View File

@ -18,23 +18,20 @@
package im.vector.matrix.android.internal.crypto
import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertFromUTF8
import im.vector.matrix.android.internal.util.convertToUTF8
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmInboundGroupSession
import org.matrix.olm.OlmMessage
import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession
import org.matrix.olm.OlmUtility
import org.matrix.olm.*
import timber.log.Timber
import java.net.URLEncoder
import java.util.*
@ -86,11 +83,6 @@ internal class MXOlmDevice @Inject constructor(
// Values are true.
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>> = HashMap()
/**
* inboundGroupSessionWithId error
*/
private var inboundGroupSessionWithIdError: MXCryptoError? = null
init {
// Retrieve the account from the store
olmAccount = store.getAccount()
@ -119,13 +111,13 @@ internal class MXOlmDevice @Inject constructor(
try {
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
} catch (e: Exception) {
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error")
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
}
try {
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
} catch (e: Exception) {
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error")
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
}
}
@ -297,13 +289,13 @@ internal class MXOlmDevice @Inject constructor(
val res = HashMap<String, String>()
if (!TextUtils.isEmpty(payloadString)) {
res["payload"] = payloadString!!
if (!payloadString.isNullOrEmpty()) {
res["payload"] = payloadString
}
val sessionIdentifier = olmSession.sessionIdentifier()
if (!TextUtils.isEmpty(sessionIdentifier)) {
if (!sessionIdentifier.isNullOrEmpty()) {
res["session_id"] = sessionIdentifier
}
@ -513,24 +505,26 @@ internal class MXOlmDevice @Inject constructor(
forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>,
exportFormat: Boolean): Boolean {
val existingInboundSession = getInboundGroupSession(sessionId, senderKey, roomId)
val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat)
if (null != existingInboundSession) {
getInboundGroupSession(sessionId, senderKey, roomId).fold(
{
// Nothing to do in case of error
},
{
// If we already have this session, consider updating it
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
val existingFirstKnown = existingInboundSession.firstKnownIndex!!
val newKnownFirstIndex = session.firstKnownIndex!!
val existingFirstKnown = it.firstKnownIndex!!
val newKnownFirstIndex = session.firstKnownIndex
//If our existing session is better we keep it
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
if (session.olmInboundGroupSession != null) {
session.olmInboundGroupSession!!.releaseSession()
}
session.olmInboundGroupSession?.releaseSession()
return false
}
}
)
// sanity check
if (null == session.olmInboundGroupSession) {
@ -545,7 +539,7 @@ internal class MXOlmDevice @Inject constructor(
return false
}
} catch (e: Exception) {
session.olmInboundGroupSession!!.releaseSession()
session.olmInboundGroupSession?.releaseSession()
Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
return false
}
@ -584,13 +578,13 @@ internal class MXOlmDevice @Inject constructor(
}
// sanity check
if (null == session || null == session.olmInboundGroupSession) {
if (session?.olmInboundGroupSession == null) {
Timber.e("## importInboundGroupSession : invalid session")
continue
}
try {
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) {
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey!!)
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
continue
@ -601,21 +595,27 @@ internal class MXOlmDevice @Inject constructor(
continue
}
val existingOlmSession = getInboundGroupSession(sessionId, senderKey, roomId)
if (null != existingOlmSession) {
getInboundGroupSession(sessionId, senderKey, roomId)
.fold(
{
// Session does not already exist, add it
sessions.add(session)
},
{
// If we already have this session, consider updating it
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
// For now we just ignore updates. TODO: implement something here
if (existingOlmSession.firstKnownIndex!! <= session.firstKnownIndex!!) {
if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
//Ignore this, keep existing
session.olmInboundGroupSession!!.releaseSession()
continue
}
}
} else {
sessions.add(session)
}
Unit
}
)
}
store.storeInboundGroupSessions(sessions)
@ -644,29 +644,24 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown.
*/
@Throws(MXDecryptionException::class)
fun decryptGroupMessage(body: String,
roomId: String,
timeline: String?,
sessionId: String,
senderKey: String): MXDecryptionResult? {
val result = MXDecryptionResult()
val session = getInboundGroupSession(sessionId, senderKey, roomId)
if (null != session) {
senderKey: String): Try<OlmDecryptionResult> {
return getInboundGroupSession(sessionId, senderKey, roomId)
.flatMap { session ->
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (TextUtils.equals(roomId, session.roomId)) {
var errorMessage = ""
if (roomId == session.roomId) {
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
try {
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
} catch (e: Exception) {
} catch (e: OlmException) {
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
errorMessage = e.message ?: ""
return@flatMap Try.Failure(MXCryptoError.OlmError(e))
}
if (null != decryptResult) {
if (null != timeline) {
if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) {
inboundGroupSessionMessageIndexes[timeline] = HashMap()
@ -674,51 +669,40 @@ internal class MXOlmDevice @Inject constructor(
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
if (null != inboundGroupSessionMessageIndexes[timeline]!![messageIndexKey]) {
if (inboundGroupSessionMessageIndexes[timeline]?.get(messageIndexKey) != null) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
Timber.e("## decryptGroupMessage() : $reason")
throw MXDecryptionException(MXCryptoError(MXCryptoError.DUPLICATED_MESSAGE_INDEX_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason))
return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason))
}
inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true)
}
store.storeInboundGroupSessions(listOf(session))
try {
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
val payload = adapter.fromJson(payloadString)
result.payload = payload
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.e(e, "## decryptGroupMessage() : RLEncoder.encode failed " + e.message)
return null
}
if (null == result.payload) {
Timber.e("## decryptGroupMessage() : fails to parse the payload")
return null
return@flatMap Try.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
}
result.keysClaimed = session.keysClaimed
result.senderKey = senderKey
result.forwardingCurve25519KeyChain = session.forwardingCurve25519KeyChain
} else {
Timber.e("## decryptGroupMessage() : failed to decode the message")
throw MXDecryptionException(MXCryptoError(MXCryptoError.OLM_ERROR_CODE, errorMessage, null))
}
return@flatMap Try.just(
OlmDecryptionResult(
payload,
session.keysClaimed,
senderKey,
session.forwardingCurve25519KeyChain
)
)
} else {
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## decryptGroupMessage() : $reason")
throw MXDecryptionException(MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason))
return@flatMap Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason))
}
} else {
Timber.e("## decryptGroupMessage() : Cannot retrieve inbound group session $sessionId")
throw MXDecryptionException(inboundGroupSessionWithIdError)
}
return result
}
/**
@ -732,7 +716,7 @@ internal class MXOlmDevice @Inject constructor(
}
}
// Utilities
// Utilities
/**
* Verify an ed25519 signature on a JSON object.
@ -745,7 +729,7 @@ internal class MXOlmDevice @Inject constructor(
@Throws(Exception::class)
fun verifySignature(key: String, jsonDictionary: Map<String, Any>, signature: String) {
// Check signature on the canonical version of the JSON
olmUtility!!.verifyEd25519Signature(signature, key, MoshiProvider.getCanonicalJson<Map<*, *>>(Map::class.java, jsonDictionary))
olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary))
}
/**
@ -767,9 +751,9 @@ internal class MXOlmDevice @Inject constructor(
*/
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
// sanity check
return if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) {
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
store.getDeviceSession(sessionId, theirDeviceIdentityKey)
} else null
}
}
@ -782,26 +766,27 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the inbound group session.
*/
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper? {
inboundGroupSessionWithIdError = null
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): Try<OlmInboundGroupSessionWrapper> {
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON))
}
val session = store.getInboundGroupSession(sessionId!!, senderKey!!)
val session = store.getInboundGroupSession(sessionId, senderKey)
if (null != session) {
return if (null != session) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (!TextUtils.equals(roomId, session.roomId)) {
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
Timber.e("## getInboundGroupSession() : $errorDescription")
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, errorDescription)
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription))
} else {
Try.just(session)
}
} else {
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE,
MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null)
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON))
}
return session
}
/**
@ -813,6 +798,6 @@ internal class MXOlmDevice @Inject constructor(
* @return true if the unbound session keys are known.
*/
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
return null != getInboundGroupSession(sessionId, senderKey, roomId)
return getInboundGroupSession(sessionId, senderKey, roomId).isSuccess()
}
}

View File

@ -44,7 +44,9 @@ internal class ObjectSigner @Inject constructor(private val credentials: Credent
val content = HashMap<String, String>()
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)!!
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
?: "" //null reported by rageshake if happens during logout
result[credentials.userId] = content

View File

@ -22,8 +22,8 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmAccount
import timber.log.Timber
import java.util.*
@ -150,7 +150,7 @@ internal class OneTimeKeysUploader @Inject constructor(
val oneTimeKeys = olmDevice.getOneTimeKeys()
val oneTimeJson = HashMap<String, Any>()
val curve25519Map = oneTimeKeys!![OlmAccount.JSON_KEY_ONE_TIME_KEY]
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
if (null != curve25519Map) {
for (key_id in curve25519Map.keys) {
@ -158,7 +158,7 @@ internal class OneTimeKeysUploader @Inject constructor(
k["key"] = curve25519Map.getValue(key_id)
// the key is also signed
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, k)
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
k["signatures"] = objectSigner.signObject(canonicalJson)

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto
import android.os.Handler
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
@ -28,9 +27,11 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.createBackgroundHandler
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@SessionScope
@ -47,7 +48,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequestsTimer
private var sendOutgoingRoomKeyRequestsRunning: Boolean = false
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
/**
* Called when the client is started. Sets background processes running.
@ -101,8 +102,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
BACKGROUND_HANDLER.post {
cancelRoomKeyRequest(requestBody, false)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
@ -110,8 +113,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
BACKGROUND_HANDLER.post {
cancelRoomKeyRequest(requestBody, true)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
@ -126,12 +131,17 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
if (req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING || req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
when (req.state) {
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
// nothing to do here
} else if (req.state === OutgoingRoomKeyRequest.RequestState.UNSENT || req.state === OutgoingRoomKeyRequest.RequestState.FAILED) {
}
OutgoingRoomKeyRequest.RequestState.UNSENT,
OutgoingRoomKeyRequest.RequestState.FAILED -> {
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
} else if (req.state === OutgoingRoomKeyRequest.RequestState.SENT) {
}
OutgoingRoomKeyRequest.RequestState.SENT -> {
if (andResend) {
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
} else {
@ -142,22 +152,23 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
sendOutgoingRoomKeyRequestCancellation(req)
}
}
}
/**
* Start the background timer to send queued requests, if the timer isn't already running.
*/
private fun startTimer() {
if (sendOutgoingRoomKeyRequestsRunning) {
if (sendOutgoingRoomKeyRequestsRunning.get()) {
return
}
Handler().postDelayed(Runnable {
if (sendOutgoingRoomKeyRequestsRunning) {
BACKGROUND_HANDLER.postDelayed(Runnable {
if (sendOutgoingRoomKeyRequestsRunning.get()) {
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
return@Runnable
}
sendOutgoingRoomKeyRequestsRunning = true
sendOutgoingRoomKeyRequestsRunning.set(true)
sendOutgoingRoomKeyRequests()
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
}
@ -167,19 +178,19 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
// timer will be restarted before the promise resolves).
private fun sendOutgoingRoomKeyRequests() {
if (!isClientRunning) {
sendOutgoingRoomKeyRequestsRunning = false
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
HashSet<OutgoingRoomKeyRequest.RequestState>(Arrays.asList<OutgoingRoomKeyRequest.RequestState>(OutgoingRoomKeyRequest.RequestState.UNSENT,
setOf(OutgoingRoomKeyRequest.RequestState.UNSENT,
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)))
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
if (null == outgoingRoomKeyRequest) {
Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
sendOutgoingRoomKeyRequestsRunning = false
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
@ -213,7 +224,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
cryptoStore.updateOutgoingRoomKeyRequest(request)
}
sendOutgoingRoomKeyRequestsRunning = false
sendOutgoingRoomKeyRequestsRunning.set(false)
startTimer()
}
@ -246,7 +257,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
private fun onDone() {
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
sendOutgoingRoomKeyRequestsRunning = false
sendOutgoingRoomKeyRequestsRunning.set(false)
startTimer()
}
@ -285,15 +296,20 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
val contentMap = MXUsersDevicesMap<Any>()
for (recipient in recipients) {
contentMap.setObject(message, recipient["userId"], recipient["deviceId"]) // TODO Change this two hard coded key to something better
// TODO Change this two hard coded key to something better
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
}
sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId))
.dispatchTo(callback)
.executeOn(TaskThread.CALLER)
.callbackOn(TaskThread.CALLER)
.executeBy(taskExecutor)
}
companion object {
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
}
}

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import java.util.*
import javax.inject.Inject
@ -54,7 +53,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
}
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
results.setObject(olmSessionResult, userId, deviceId)
results.setObject(userId, deviceId, olmSessionResult)
}
}
@ -68,7 +67,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
for (device in devicesWithoutSession) {
usersDevicesToClaim.setObject(oneTimeKeyAlgorithm, device.userId, device.deviceId)
usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm)
}
// TODO: this has a race condition - if we try to send another message
@ -91,12 +90,12 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val deviceIds = it.getUserDeviceIds(userId)
if (null != deviceIds) {
for (deviceId in deviceIds) {
val olmSessionResult = results.getObject(deviceId, userId)
val olmSessionResult = results.getObject(userId, deviceId)
if (olmSessionResult!!.sessionId != null) {
// We already have a result for this device
continue
}
val key = it.getObject(deviceId, userId)
val key = it.getObject(userId, deviceId)
if (key?.type == oneTimeKeyAlgorithm) {
oneTimeKey = key
}
@ -126,12 +125,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
var isVerified = false
var errorMessage: String? = null
if (signature != null) {
try {
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
isVerified = true
} catch (e: Exception) {
errorMessage = e.message
}
}
// Check one-time key signature
if (isVerified) {

View File

@ -22,8 +22,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber
import java.util.*
@ -81,10 +80,7 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
payloadJson["recipient_keys"] = recipientsKeysMap
// FIXME We have to canonicalize the JSON
//JsonUtility.canonicalize(JsonUtility.getGson(false).toJsonTree(payloadJson)).toString()
val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, payloadJson))
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId!!, payloadString!!)!!
}
}

View File

@ -17,9 +17,9 @@
package im.vector.matrix.android.internal.crypto.algorithms
import arrow.core.Try
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.MXDecryptionException
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
@ -33,11 +33,9 @@ internal interface IMXDecrypting {
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or null in case of error
* @throws MXDecryptionException the decryption failure reason
* @return the decryption information, or an error
*/
@Throws(MXDecryptionException::class)
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult>
/**
* Handle a key event.

View File

@ -18,7 +18,6 @@
package im.vector.matrix.android.internal.crypto.algorithms
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Content
/**

View File

@ -28,9 +28,7 @@ import im.vector.matrix.android.internal.crypto.*
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
@ -39,10 +37,10 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap
internal class MXMegolmDecryption(private val credentials: Credentials,
private val olmDevice: MXOlmDevice,
@ -63,67 +61,77 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
*/
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
@Throws(MXDecryptionException::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? {
override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
return decryptEvent(event, timeline, true)
}
@Throws(MXDecryptionException::class)
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult? {
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()!!
if (TextUtils.isEmpty(encryptedEventContent.senderKey) || TextUtils.isEmpty(encryptedEventContent.sessionId) || TextUtils.isEmpty(encryptedEventContent.ciphertext)) {
throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_FIELDS_REASON))
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): Try<MXEventDecryptionResult> {
if (event.roomId.isNullOrBlank()) {
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
}
var eventDecryptionResult: MXEventDecryptionResult? = null
var cryptoError: MXCryptoError? = null
var decryptGroupMessageResult: MXDecryptionResult? = null
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
?: return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
try {
decryptGroupMessageResult = olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext!!, event.roomId!!, timeline, encryptedEventContent.sessionId!!, encryptedEventContent.senderKey!!)
} catch (e: MXDecryptionException) {
cryptoError = e.cryptoError
}
// the decryption succeeds
if (decryptGroupMessageResult?.payload != null && cryptoError == null) {
eventDecryptionResult = MXEventDecryptionResult()
eventDecryptionResult.clearEvent = decryptGroupMessageResult.payload
eventDecryptionResult.senderCurve25519Key = decryptGroupMessageResult.senderKey
if (null != decryptGroupMessageResult.keysClaimed) {
eventDecryptionResult.claimedEd25519Key = decryptGroupMessageResult.keysClaimed!!["ed25519"]
if (encryptedEventContent.senderKey.isNullOrBlank()
|| encryptedEventContent.sessionId.isNullOrBlank()
|| encryptedEventContent.ciphertext.isNullOrBlank()) {
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
}
eventDecryptionResult.forwardingCurve25519KeyChain = decryptGroupMessageResult.forwardingCurve25519KeyChain!!
} else if (cryptoError != null) {
if (cryptoError.isOlmError) {
if (MXCryptoError.UNKNOWN_MESSAGE_INDEX == cryptoError.message) {
return olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext,
event.roomId,
timeline,
encryptedEventContent.sessionId,
encryptedEventContent.senderKey)
.fold(
{ throwable ->
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
addEventToPendingList(event, timeline)
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
val reason = String.format(MXCryptoError.OLM_REASON, cryptoError.message)
val detailedReason = String.format(MXCryptoError.DETAILLED_OLM_REASON, encryptedEventContent.ciphertext, cryptoError.message)
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
throw MXDecryptionException(MXCryptoError(
MXCryptoError.OLM_ERROR_CODE,
Try.Failure(MXCryptoError.Base(
MXCryptoError.ErrorType.OLM,
reason,
detailedReason))
} else if (TextUtils.equals(cryptoError.code, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) {
}
if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
addEventToPendingList(event, timeline)
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
throw MXDecryptionException(cryptoError)
}
return eventDecryptionResult
Try.Failure(throwable)
},
{ olmDecryptionResult ->
// the decryption succeeds
if (olmDecryptionResult.payload != null) {
Try.just(
MXEventDecryptionResult(
clearEvent = olmDecryptionResult.payload,
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain ?: emptyList()
)
)
} else {
Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON))
}
}
)
}
/**
* Helper for the real decryptEvent and for _retryDecryption. If
@ -139,7 +147,8 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
val recipients = ArrayList<Map<String, String>>()
val selfMap = HashMap<String, String>()
selfMap["userId"] = credentials.userId // TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
// TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
selfMap["userId"] = credentials.userId
selfMap["deviceId"] = "*"
recipients.add(selfMap)
@ -171,17 +180,18 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
if (!pendingEvents.containsKey(pendingEventsKey)) {
pendingEvents[pendingEventsKey] = HashMap()
}
if (!pendingEvents[pendingEventsKey]!!.containsKey(timelineId)) {
pendingEvents[pendingEventsKey]!![timelineId] = ArrayList()
if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) {
pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList())
}
if (pendingEvents[pendingEventsKey]!![timelineId]!!.indexOf(event) < 0) {
if (pendingEvents[pendingEventsKey]?.get(timelineId)?.contains(event) == false) {
Timber.v("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId)
pendingEvents[pendingEventsKey]!![timelineId]!!.add(event)
pendingEvents[pendingEventsKey]?.get(timelineId)?.add(event)
}
}
@ -196,21 +206,20 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
var senderKey: String? = event.getSenderKey()
var keysClaimed: MutableMap<String, String> = HashMap()
var forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) {
Timber.e("## onRoomKeyEvent() : Key event is missing fields")
return
}
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
Timber.v("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
Timber.v("## onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" +
" sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
?: return
forwardingCurve25519KeyChain = if (forwardedRoomKeyContent.forwardingCurve25519KeyChain == null) {
ArrayList()
} else {
ArrayList(forwardedRoomKeyContent.forwardingCurve25519KeyChain)
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
forwardingCurve25519KeyChain.addAll(it)
}
if (senderKey == null) {
@ -253,7 +262,13 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
return
}
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, senderKey, forwardingCurve25519KeyChain, keysClaimed, exportFormat)
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey,
roomKeyContent.roomId,
senderKey,
forwardingCurve25519KeyChain,
keysClaimed,
exportFormat)
if (added) {
keysBackup.maybeBackupKeys()
@ -283,8 +298,10 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
}
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
return (null != request.requestBody
&& olmDevice.hasInboundSessionKeys(request.requestBody!!.roomId!!, request.requestBody!!.senderKey!!, request.requestBody!!.sessionId!!))
val roomId = request.requestBody?.roomId ?: return false
val senderKey = request.requestBody?.senderKey ?: return false
val sessionId = request.requestBody?.sessionId ?: return false
return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId)
}
override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {
@ -292,45 +309,50 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
if (request.requestBody == null) {
return
}
val userId = request.userId!!
CoroutineScope(coroutineDispatchers.crypto).launch {
val userId = request.userId ?: return
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(listOf(userId), false)
.flatMap {
val deviceId = request.deviceId
val deviceInfo = cryptoStore.getUserDevice(deviceId!!, userId)
val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId)
if (deviceInfo == null) {
throw RuntimeException()
} else {
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
val devicesByUser = mapOf(userId to listOf(deviceInfo))
ensureOlmSessionsForDevicesAction
.handle(devicesByUser)
.flatMap {
val body = request.requestBody
val olmSessionResult = it.getObject(deviceId, userId)
val olmSessionResult = it.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
Try.just(Unit)
}
Timber.v("## shareKeysWithDevice() : sharing keys for session " + body!!.senderKey + "|" + body.sessionId
+ " with device " + userId + ":" + deviceId)
val inboundGroupSession = olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
val payloadJson = HashMap<String, Any>()
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
payloadJson["content"] = inboundGroupSession!!.exportKeys()!!
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
.fold(
{
// TODO
},
{
// TODO
payloadJson["content"] = it.exportKeys() ?: ""
}
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(encodedPayload, userId, deviceId)
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
}
}
}

View File

@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm
import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.EventType
@ -37,7 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber
import java.util.*
@ -118,8 +117,8 @@ internal class MXMegolmEncryption(
for (userId in userIds) {
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(deviceId, userId)
if (null == safeSession.sharedWithDevices.getObject(deviceId, userId)) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
if (!shareMap.containsKey(userId)) {
shareMap[userId] = ArrayList()
}
@ -201,7 +200,7 @@ internal class MXMegolmEncryption(
for (userId in userIds) {
val devicesToShareWith = devicesByUser[userId]
for ((deviceID) in devicesToShareWith!!) {
val sessionResult = it.getObject(deviceID, userId)
val sessionResult = it.getObject(userId, deviceID)
if (sessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
@ -218,7 +217,7 @@ internal class MXMegolmEncryption(
}
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
contentMap.setObject(messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)), userId, deviceID)
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
haveTargets = true
}
}
@ -239,7 +238,7 @@ internal class MXMegolmEncryption(
for (userId in devicesByUser.keys) {
val devicesToShareWith = devicesByUser[userId]
for ((deviceId) in devicesToShareWith!!) {
session.sharedWithDevices.setObject(chainIndex, userId, deviceId)
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
}
}
Unit
@ -254,7 +253,8 @@ internal class MXMegolmEncryption(
/**
* process the pending encryptions
*/
private suspend fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content) = Try<Content> {
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Try<Content> {
return Try<Content> {
// Everything is in place, encrypt all pending events
val payloadJson = HashMap<String, Any>()
payloadJson["room_id"] = roomId
@ -263,7 +263,7 @@ internal class MXMegolmEncryption(
// Get canonical Json from
val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, payloadJson))
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!)
val map = HashMap<String, Any>()
@ -278,6 +278,7 @@ internal class MXMegolmEncryption(
session.useCount++
map
}
}
/**
* Get the list of devices which can encrypt data to.
@ -303,10 +304,10 @@ internal class MXMegolmEncryption(
for (userId in it.userIds) {
val deviceIds = it.getUserDeviceIds(userId) ?: continue
for (deviceId in deviceIds) {
val deviceInfo = it.getObject(deviceId, userId) ?: continue
val deviceInfo = it.getObject(userId, deviceId) ?: continue
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
// The device is not yet known by the user
unknownDevices.setObject(deviceInfo, userId, deviceId)
unknownDevices.setObject(userId, deviceId, deviceInfo)
continue
}
if (deviceInfo.isBlocked) {
@ -322,15 +323,13 @@ internal class MXMegolmEncryption(
// Don't bother sending to ourself
continue
}
devicesInRoom.setObject(deviceInfo, userId, deviceId)
devicesInRoom.setObject(userId, deviceId, deviceInfo)
}
}
if (unknownDevices.isEmpty) {
Try.just(devicesInRoom)
} else {
val cryptoError = MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)
Try.Failure(Failure.CryptoError(cryptoError))
Try.Failure(MXCryptoError.UnknownDevice(unknownDevices))
}
}
}

View File

@ -25,8 +25,6 @@ import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
@ -37,8 +35,6 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val credentials: Credentials,
private val sendToDeviceTask: SendToDeviceTask,
// FIXME Why taskExecutor is not used?
private val taskExecutor: TaskExecutor,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {

View File

@ -64,7 +64,7 @@ internal class MXOutboundSessionInfo(
val deviceIds = sharedWithDevices.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
if (null == devicesInRoom.getObject(deviceId, userId)) {
if (null == devicesInRoom.getObject(userId, deviceId)) {
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId")
return true
}

View File

@ -17,14 +17,13 @@
package im.vector.matrix.android.internal.crypto.algorithms.olm
import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.MXDecryptionException
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
@ -42,111 +41,120 @@ internal class MXOlmDecryption(
private val credentials: Credentials)
: IMXDecrypting {
@Throws(MXDecryptionException::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? {
val olmEventContent = event.content.toModel<OlmEventContent>()!!
if (null == olmEventContent.ciphertext) {
Timber.e("## decryptEvent() : missing cipher text")
throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_CIPHER_TEXT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON))
override suspend fun decryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
Timber.e("## decryptEvent() : bad event format")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON))
}
if (!olmEventContent.ciphertext!!.containsKey(olmDevice.deviceCurve25519Key)) {
Timber.e("## decryptEvent() : our device " + olmDevice.deviceCurve25519Key
+ " is not included in recipients. Event")
throw MXDecryptionException(MXCryptoError(MXCryptoError.NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON))
val cipherText = olmEventContent.ciphertext ?: run {
Timber.e("## decryptEvent() : missing cipher text")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
}
val senderKey = olmEventContent.senderKey ?: run {
Timber.e("## decryptEvent() : missing sender key")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON))
}
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS,
MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON))
}
// The message for myUser
val message = olmEventContent.ciphertext!![olmDevice.deviceCurve25519Key] as JsonDict
val decryptedPayload = decryptMessage(message, olmEventContent.senderKey!!)
val message = messageAny as JsonDict
val decryptedPayload = decryptMessage(message, senderKey)
if (decryptedPayload == null) {
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= " + event.eventId + " ) from " + olmEventContent.senderKey)
throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_ENCRYPTED_MESSAGE_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
}
val payloadString = convertFromUTF8(decryptedPayload)
if (payloadString == null) {
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= " + event.eventId + " ) from " + olmEventContent.senderKey)
throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_ENCRYPTED_MESSAGE_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE,
MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
}
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payload = adapter.fromJson(payloadString)
if (payload == null) {
Timber.e("## decryptEvent failed : null payload")
throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON))
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
}
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString)
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
Timber.e("## decryptEvent() : bad olmPayloadContent format")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT,
MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON))
}
if (TextUtils.isEmpty(olmPayloadContent.recipient)) {
if (olmPayloadContent.recipient.isNullOrBlank()) {
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
Timber.e("## decryptEvent() : $reason")
throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, reason))
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
reason))
}
if (!TextUtils.equals(olmPayloadContent.recipient, credentials.userId)) {
Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient
+ " does not match our id " + credentials.userId)
throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_RECIPIENT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)))
if (olmPayloadContent.recipient != credentials.userId) {
Timber.e("## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id ${credentials.userId}")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)))
}
if (null == olmPayloadContent.recipient_keys) {
Timber.e("## decryptEvent() : Olm event (id=" + event.eventId
+ ") contains no " + "'recipient_keys' property; cannot prevent unknown-key attack")
throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")))
val recipientKeys = olmPayloadContent.recipient_keys ?: run {
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys' property; cannot prevent unknown-key attack")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")))
}
val ed25519 = olmPayloadContent.recipient_keys!!.get("ed25519")
val ed25519 = recipientKeys["ed25519"]
if (!TextUtils.equals(ed25519, olmDevice.deviceEd25519Key)) {
Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient ed25519 key " + ed25519 + " did not match ours")
throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_RECIPIENT_KEY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_RECIPIENT_KEY_REASON))
if (ed25519 != olmDevice.deviceEd25519Key) {
Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
MXCryptoError.BAD_RECIPIENT_KEY_REASON))
}
if (TextUtils.isEmpty(olmPayloadContent.sender)) {
Timber.e("## decryptEvent() : Olm event (id=" + event.eventId
+ ") contains no 'sender' property; cannot prevent unknown-key attack")
throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")))
if (olmPayloadContent.sender.isNullOrBlank()) {
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")))
}
if (!TextUtils.equals(olmPayloadContent.sender, event.senderId)) {
Timber.e("Event " + event.eventId + ": original sender " + olmPayloadContent.sender
+ " does not match reported sender " + event.senderId)
throw MXDecryptionException(MXCryptoError(MXCryptoError.FORWARDED_MESSAGE_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)))
if (olmPayloadContent.sender != event.senderId) {
Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)))
}
if (!TextUtils.equals(olmPayloadContent.room_id, event.roomId)) {
Timber.e("## decryptEvent() : Event " + event.eventId + ": original room " + olmPayloadContent.room_id
+ " does not match reported room " + event.roomId)
throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_ROOM_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)))
if (olmPayloadContent.room_id != event.roomId) {
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}")
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)))
}
if (null == olmPayloadContent.keys) {
val keys = olmPayloadContent.keys ?: run {
Timber.e("## decryptEvent failed : null keys")
throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON))
return Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.MISSING_CIPHER_TEXT_REASON))
}
val result = MXEventDecryptionResult()
result.clearEvent = payload
result.senderCurve25519Key = olmEventContent.senderKey
result.claimedEd25519Key = olmPayloadContent.keys!!.get("ed25519")
return result
return Try.just(MXEventDecryptionResult(
clearEvent = payload,
senderCurve25519Key = senderKey,
claimedEd25519Key = keys["ed25519"]
))
}
/**
@ -167,7 +175,7 @@ internal class MXOlmDecryption(
sessionIds = ArrayList(sessionIdsSet)
}
val messageBody = message["body"] as String?
val messageBody = message["body"] as? String
var messageType: Int? = null
val typeAsVoid = message["type"]
@ -210,7 +218,7 @@ internal class MXOlmDecryption(
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.size == 0) {
if (sessionIds.isEmpty()) {
Timber.e("## decryptMessage() : No existing sessions")
} else {
Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
@ -228,7 +236,7 @@ internal class MXOlmDecryption(
return null
}
Timber.v("## decryptMessage() : Created new inbound Olm session get id " + res["session_id"] + " with " + theirDeviceIdentityKey)
Timber.v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
return res["payload"]
}

View File

@ -14,32 +14,35 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.algorithms
package im.vector.matrix.android.internal.crypto.algorithms.olm
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
/**
* This class represents the decryption result.
*/
data class MXDecryptionResult(
@JsonClass(generateAdapter = true)
data class OlmDecryptionResult(
/**
* The decrypted payload (with properties 'type', 'content')
*/
var payload: JsonDict? = null,
@Json(name = "payload") val payload: JsonDict? = null,
/**
* keys that the sender of the event claims ownership of:
* map from key type to base64-encoded key.
*/
var keysClaimed: Map<String, String>? = null,
@Json(name = "keysClaimed") val keysClaimed: Map<String, String>? = null,
/**
* The curve25519 key that the sender of the event is known to have ownership of.
*/
var senderKey: String? = null,
@Json(name = "senderKey") val senderKey: String? = null,
/**
* Devices which forwarded this session to us (normally empty).
*/
var forwardingCurve25519KeyChain: List<String>? = null
@Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null
)

View File

@ -24,10 +24,11 @@ import kotlinx.android.parcel.Parcelize
fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? {
// Check the validity of some fields
if (isValid()) {
// It's valid so the data are here
return ElementToDecrypt(
iv = this.iv!!,
k = this.key!!.k!!,
sha256 = this.hashes!!["sha256"] ?: error("")
iv = this.iv ?: "",
k = this.key?.k ?: "",
sha256 = this.hashes?.get("sha256") ?: ""
)
}

View File

@ -16,8 +16,8 @@
package im.vector.matrix.android.internal.crypto.attachments
import android.text.TextUtils
import android.util.Base64
import arrow.core.Try
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
import timber.log.Timber
@ -51,7 +51,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult? {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try<EncryptionResult> {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()
@ -115,23 +115,21 @@ object MXEncryptedAttachments {
encryptedByteArray = outStream.toByteArray()
)
outStream.close()
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
return result
return Try.just(result)
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed " + oom.message)
Timber.e(oom, "## encryptAttachment failed")
return Try.Failure(oom)
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment failed " + e.message)
}
Timber.e(e, "## encryptAttachment failed")
return Try.Failure(e)
} finally {
try {
outStream.close()
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment() : fail to close outStream")
}
return null
}
}
/**
@ -199,7 +197,7 @@ object MXEncryptedAttachments {
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))
if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) {
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close()
return null

View File

@ -45,17 +45,18 @@ import im.vector.matrix.android.internal.crypto.keysbackup.tasks.*
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.*
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -177,7 +178,7 @@ internal class KeysBackup @Inject constructor(
megolmBackupAuthData.publicKey = publicKey
}
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson)
@ -388,8 +389,8 @@ internal class KeysBackup @Inject constructor(
return keysBackupVersionTrust
}
val mySigs: Map<String, *> = authData.signatures!![myUserId] as Map<String, *>
if (mySigs.isEmpty()) {
val mySigs = authData.signatures?.get(myUserId)
if (mySigs.isNullOrEmpty()) {
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
return keysBackupVersionTrust
}
@ -402,21 +403,22 @@ internal class KeysBackup @Inject constructor(
deviceId = components[1]
}
var device: MXDeviceInfo? = null
if (deviceId != null) {
device = cryptoStore.getUserDevice(deviceId, myUserId)
val device = cryptoStore.getUserDevice(deviceId, myUserId)
var isSignatureValid = false
if (device == null) {
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId")
} else {
val fingerprint = device.fingerprint()
if (fingerprint != null) {
try {
olmDevice.verifySignature(device.fingerprint()!!, authData.signalableJSONDictionary(), mySigs[keyId] as String)
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String)
isSignatureValid = true
} catch (e: OlmException) {
Timber.v("getKeysBackupTrust: Bad signature from device " + device.deviceId + " " + e.localizedMessage)
}
}
if (isSignatureValid && device.isVerified) {
keysBackupVersionTrust.usable = true
@ -452,12 +454,11 @@ internal class KeysBackup @Inject constructor(
val myUserId = credentials.userId
// Get current signatures, or create an empty set
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap()
?: HashMap())
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap() ?: HashMap()
if (trust) {
// Add current device signature
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
val deviceSignatures = objectSigner.signObject(canonicalJson)
@ -666,7 +667,8 @@ internal class KeysBackup @Inject constructor(
// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
if (backUp) {
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: " + keysBackupVersion?.version)
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: "
+ keysBackupVersion?.version)
}
// Import them into the crypto store
@ -875,7 +877,7 @@ internal class KeysBackup @Inject constructor(
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
getKeysBackupLastVersionTask
.configureWith(Unit)
.toConfigurableTask()
.dispatchTo(object : MatrixCallback<KeysVersionResult> {
override fun onSuccess(data: KeysVersionResult) {
callback.onSuccess(data)
@ -1027,8 +1029,7 @@ internal class KeysBackup @Inject constructor(
val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData()
if (authData.signatures == null
|| authData.publicKey.isBlank()) {
if (authData?.signatures == null || authData.publicKey.isBlank()) {
return null
}
@ -1226,7 +1227,8 @@ internal class KeysBackup @Inject constructor(
}
try {
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!
.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
@ -1278,7 +1280,8 @@ internal class KeysBackup @Inject constructor(
// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
else -> // Come back to the ready state so that we will retry on the next received key
else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}

View File

@ -54,9 +54,9 @@ open class KeysAlgorithmAndData {
/**
* Facility method to convert authData to a MegolmBackupAuthData object
*/
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData {
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
return MoshiProvider.providesMoshi()
.adapter(MegolmBackupAuthData::class.java)
.fromJsonValue(authData)!!
.fromJsonValue(authData)
}
}

View File

@ -18,7 +18,6 @@
package im.vector.matrix.android.internal.crypto.model
import android.text.TextUtils
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
@ -107,30 +106,25 @@ data class MXDeviceInfo(
* @return the fingerprint
*/
fun fingerprint(): String? {
return if (null != keys && !TextUtils.isEmpty(deviceId)) {
keys!!["ed25519:$deviceId"]
} else null
return keys
?.takeIf { !deviceId.isBlank() }
?.get("ed25519:$deviceId")
}
/**
* @return the identity key
*/
fun identityKey(): String? {
return if (null != keys && !TextUtils.isEmpty(deviceId)) {
keys!!["curve25519:$deviceId"]
} else null
return keys
?.takeIf { !deviceId.isBlank() }
?.get("curve25519:$deviceId")
}
/**
* @return the display name
*/
fun displayName(): String? {
return if (null != unsigned) {
unsigned!!["device_display_name"] as String?
} else null
return unsigned?.get("device_display_name") as? String
}
/**
@ -141,9 +135,7 @@ data class MXDeviceInfo(
map["device_id"] = deviceId
if (null != userId) {
map["user_id"] = userId!!
}
map["user_id"] = userId
if (null != algorithms) {
map["algorithms"] = algorithms!!

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content
data class MXEncryptEventContentResult(
/**
* The event content
* The encrypted event content
*/
val eventContent: Content,
/**

View File

@ -1,139 +0,0 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.model;
import android.text.TextUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import timber.log.Timber;
public class MXKey implements Serializable {
/**
* Key types.
*/
public static final String KEY_CURVE_25519_TYPE = "curve25519";
public static final String KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519";
//public static final String KEY_ED_25519_TYPE = "ed25519";
/**
* The type of the key.
*/
public String type;
/**
* The id of the key.
*/
public String keyId;
/**
* The key.
*/
public String value;
/**
* signature user Id to [deviceid][signature]
*/
public Map<String, Map<String, String>> signatures;
/**
* Default constructor
*/
public MXKey() {
}
/**
* Convert a map to a MXKey
*
* @param map the map to convert
*/
public MXKey(Map<String, Map<String, Object>> map) {
if ((null != map) && (map.size() > 0)) {
List<String> mapKeys = new ArrayList<>(map.keySet());
String firstEntry = mapKeys.get(0);
setKeyFullId(firstEntry);
Map<String, Object> params = map.get(firstEntry);
value = (String) params.get("key");
signatures = (Map<String, Map<String, String>>) params.get("signatures");
}
}
/**
* @return the key full id
*/
public String getKeyFullId() {
return type + ":" + keyId;
}
/**
* Update the key fields with a key full id
*
* @param keyFullId the key full id
*/
private void setKeyFullId(String keyFullId) {
if (!TextUtils.isEmpty(keyFullId)) {
try {
String[] components = keyFullId.split(":");
if (components.length == 2) {
type = components[0];
keyId = components[1];
}
} catch (Exception e) {
Timber.e(e, "## setKeyFullId() failed");
}
}
}
/**
* @return the signed data map
*/
public Map<String, Object> signalableJSONDictionary() {
Map<String, Object> map = new HashMap<>();
if (null != value) {
map.put("key", value);
}
return map;
}
/**
* Returns a signature for an user Id and a signkey
*
* @param userId the user id
* @param signkey the sign key
* @return the signature
*/
public String signatureForUserId(String userId, String signkey) {
// sanity checks
if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(signkey)) {
if ((null != signatures) && signatures.containsKey(userId)) {
return signatures.get(userId).get(signkey);
}
}
return null;
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.model
import im.vector.matrix.android.api.util.JsonDict
import timber.log.Timber
import java.util.*
data class MXKey(
/**
* The type of the key (in the example: "signed_curve25519").
*/
val type: String,
/**
* The id of the key (in the example: "AAAAFw").
*/
private val keyId: String,
/**
* The key (in the example: "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4").
*/
val value: String,
/**
* signature user Id to [deviceid][signature]
*/
private val signatures: Map<String, Map<String, String>>
) {
/**
* @return the signed data map
*/
fun signalableJSONDictionary(): Map<String, Any> {
val map = HashMap<String, Any>()
map["key"] = value
return map
}
/**
* Returns a signature for an user Id and a signkey
*
* @param userId the user id
* @param signkey the sign key
* @return the signature
*/
fun signatureForUserId(userId: String, signkey: String): String? {
// sanity checks
if (userId.isNotBlank() && signkey.isNotBlank()) {
if (signatures.containsKey(userId)) {
return signatures[userId]?.get(signkey)
}
}
return null
}
companion object {
/**
* Key types.
*/
const val KEY_CURVE_25519_TYPE = "curve25519"
const val KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519"
// const val KEY_ED_25519_TYPE = "ed25519"
/**
* Convert a map to a MXKey
*
* @param map the map to convert
*
* Json Example:
*
* <pre>
* "signed_curve25519:AAAAFw": {
* "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
* "signatures": {
* "@userId:matrix.org": {
* "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
* }
* }
* }
* </pre>
*
* into several val members
*/
fun from(map: Map<String, JsonDict>?): MXKey? {
if (map?.isNotEmpty() == true) {
val firstKey = map.keys.first()
val components = firstKey.split(":").dropLastWhile { it.isEmpty() }
if (components.size == 2) {
val params = map[firstKey]
if (params != null) {
if (params["key"] is String) {
return MXKey(
type = components[0],
keyId = components[1],
value = params["key"] as String,
signatures = params["signatures"] as Map<String, Map<String, String>>
)
}
}
}
}
// Error case
Timber.e("## Unable to parse map")
return null
}
}
}

View File

@ -1,190 +0,0 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.model;
import android.text.TextUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MXUsersDevicesMap<E> implements Serializable {
// The device keys as returned by the homeserver: a map of a map (userId -> deviceId -> Object).
private final Map<String, Map<String, E>> mMap = new HashMap<>();
/**
* @return the inner map
*/
public Map<String, Map<String, E>> getMap() {
return mMap;
}
/**
* Default constructor constructor
*/
public MXUsersDevicesMap() {
}
/**
* The constructor
*
* @param map the map
*/
public MXUsersDevicesMap(Map<String, Map<String, E>> map) {
if (null != map) {
Set<String> keys = map.keySet();
for (String key : keys) {
mMap.put(key, new HashMap<>(map.get(key)));
}
}
}
/**
* @return a deep copy
*/
public MXUsersDevicesMap<E> deepCopy() {
MXUsersDevicesMap<E> copy = new MXUsersDevicesMap<>();
Set<String> keys = mMap.keySet();
for (String key : keys) {
copy.mMap.put(key, new HashMap<>(mMap.get(key)));
}
return copy;
}
/**
* @return the user Ids
*/
public List<String> getUserIds() {
return new ArrayList<>(mMap.keySet());
}
/**
* Provides the device ids list for an user id
*
* @param userId the user id
* @return the device ids list
*/
public List<String> getUserDeviceIds(String userId) {
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId)) {
return new ArrayList<>(mMap.get(userId).keySet());
}
return null;
}
/**
* Provides the object for a device id and an user Id
*
* @param deviceId the device id
* @param userId the object id
* @return the object
*/
public E getObject(String deviceId, String userId) {
if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId) && !TextUtils.isEmpty(deviceId)) {
return mMap.get(userId).get(deviceId);
}
return null;
}
/**
* Set an object for a dedicated user Id and device Id
*
* @param object the object to set
* @param userId the user Id
* @param deviceId the device id
*/
public void setObject(E object, String userId, String deviceId) {
if ((null != object) && !TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
Map<String, E> subMap = mMap.get(userId);
if (null == subMap) {
subMap = new HashMap<>();
mMap.put(userId, subMap);
}
subMap.put(deviceId, object);
}
}
/**
* Defines the objects map for an user Id
*
* @param objectsPerDevices the objects maps
* @param userId the user id
*/
public void setObjects(Map<String, E> objectsPerDevices, String userId) {
if (!TextUtils.isEmpty(userId)) {
if (null == objectsPerDevices) {
mMap.remove(userId);
} else {
mMap.put(userId, new HashMap<>(objectsPerDevices));
}
}
}
/**
* Removes objects for a dedicated user
*
* @param userId the user id.
*/
public void removeUserObjects(String userId) {
if (!TextUtils.isEmpty(userId)) {
mMap.remove(userId);
}
}
/**
* Clear the internal dictionary
*/
public void removeAllObjects() {
mMap.clear();
}
/**
* Add entries from another MXUsersDevicesMap
*
* @param other the other one
*/
public void addEntriesFromMap(MXUsersDevicesMap<E> other) {
if (null != other) {
mMap.putAll(other.getMap());
}
}
public boolean isEmpty(){
return mMap.isEmpty();
}
@Override
public String toString() {
if (null != mMap) {
return "MXUsersDevicesMap " + mMap.toString();
} else {
return "MXDeviceInfo : null map";
}
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.model
import java.util.*
class MXUsersDevicesMap<E> {
// A map of maps (userId -> (deviceId -> Object)).
val map = HashMap<String /* userId */, HashMap<String /* deviceId */, E>>()
/**
* @return the user Ids
*/
val userIds: List<String>
get() = ArrayList(map.keys)
val isEmpty: Boolean
get() = map.isEmpty()
/**
* Provides the device ids list for a user id
* FIXME Should maybe return emptyList and not null, to avoid many !! in the code
*
* @param userId the user id
* @return the device ids list
*/
fun getUserDeviceIds(userId: String?): List<String>? {
return if (userId?.isNotBlank() == true && map.containsKey(userId)) {
map[userId]!!.keys.toList()
} else null
}
/**
* Provides the object for a device id and a user Id
*
* @param deviceId the device id
* @param userId the object id
* @return the object
*/
fun getObject(userId: String?, deviceId: String?): E? {
return if (userId?.isNotBlank() == true && deviceId?.isNotBlank() == true && map.containsKey(userId)) {
map[userId]?.get(deviceId)
} else null
}
/**
* Set an object for a dedicated user Id and device Id
*
* @param userId the user Id
* @param deviceId the device id
* @param o the object to set
*/
fun setObject(userId: String?, deviceId: String?, o: E?) {
if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
if (map[userId] == null) {
map[userId] = HashMap()
}
map[userId]?.put(deviceId, o)
}
}
/**
* Defines the objects map for a user Id
*
* @param objectsPerDevices the objects maps
* @param userId the user id
*/
fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
if (userId?.isNotBlank() == true) {
if (null == objectsPerDevices) {
map.remove(userId)
} else {
map[userId] = HashMap(objectsPerDevices)
}
}
}
/**
* Removes objects for a dedicated user
*
* @param userId the user id.
*/
fun removeUserObjects(userId: String?) {
if (userId?.isNotBlank() == true) {
map.remove(userId)
}
}
/**
* Clear the internal dictionary
*/
fun removeAllObjects() {
map.clear()
}
/**
* Add entries from another MXUsersDevicesMap
*
* @param other the other one
*/
fun addEntriesFromMap(other: MXUsersDevicesMap<E>?) {
if (null != other) {
map.putAll(other.map)
}
}
override fun toString(): String {
return "MXUsersDevicesMap $map"
}
}

View File

@ -111,27 +111,29 @@ class OlmInboundGroupSessionWrapper : Serializable {
* @return the inbound group session as MegolmSessionData if the operation succeeds
*/
fun exportKeys(): MegolmSessionData? {
var megolmSessionData: MegolmSessionData? = MegolmSessionData()
try {
return try {
if (null == forwardingCurve25519KeyChain) {
forwardingCurve25519KeyChain = ArrayList()
}
megolmSessionData!!.senderClaimedEd25519Key = keysClaimed!!["ed25519"]
megolmSessionData.forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!)
megolmSessionData.senderKey = senderKey
megolmSessionData.senderClaimedKeys = keysClaimed
megolmSessionData.roomId = roomId
megolmSessionData.sessionId = olmInboundGroupSession!!.sessionIdentifier()
megolmSessionData.sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex)
megolmSessionData.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
} catch (e: Exception) {
megolmSessionData = null
Timber.e(e, "## export() : senderKey " + senderKey + " failed")
if (keysClaimed == null) {
return null
}
return megolmSessionData
MegolmSessionData().also {
it.senderClaimedEd25519Key = keysClaimed?.get("ed25519")
it.forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!)
it.senderKey = senderKey
it.senderClaimedKeys = keysClaimed
it.roomId = roomId
it.sessionId = olmInboundGroupSession!!.sessionIdentifier()
it.sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex)
it.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
}
} catch (e: Exception) {
Timber.e(e, "## export() : senderKey $senderKey failed")
null
}
}
/**

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
/**
* Class representing an encrypted event content
@ -52,5 +53,8 @@ data class EncryptedEventContent(
* The session id
*/
@Json(name = "session_id")
val sessionId: String? = null
val sessionId: String? = null,
//Relation context is in clear in encrypted message
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
)

View File

@ -53,8 +53,8 @@ data class OlmPayloadContent(
}
companion object {
fun fromJsonString(str: String): OlmPayloadContent {
return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).fromJson(str)!!
fun fromJsonString(str: String): OlmPayloadContent? {
return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).fromJson(str)
}
}
}

View File

@ -20,11 +20,14 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This class represents the response to /keys/query request made by claimOneTimeKeysForUsersDevices.
* This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices.
*/
@JsonClass(generateAdapter = true)
data class KeysClaimBody(
/**
* The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default.
*/
@Json(name = "timeout")
var timeout: Int? = null,

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This class represents the response to /keys/query request made by claimOneTimeKeysForUsersDevices.
* This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices.
*/
@JsonClass(generateAdapter = true)
data class KeysClaimResponse(

View File

@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.crypto.model.rest
class SendToDeviceBody {
// `Any` should implement SendToDeviceObject, but we cannot use interface here because of Gson serialization
/**
* `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization
*
* The messages to send. A map from user ID, to a map from device ID to message body.
* The device ID may also be *, meaning all known devices for the user.
*/

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.store.db.query.delete
import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.Sort
import io.realm.kotlin.where
@ -39,16 +40,17 @@ import org.matrix.olm.OlmException
import timber.log.Timber
import kotlin.collections.set
// enableFileEncryption is used to migrate the previous store
@SessionScope
internal class RealmCryptoStore(private val enableFileEncryption: Boolean = false,
private val realmConfiguration: RealmConfiguration,
internal class RealmCryptoStore(private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ==========================================================================================
* Memory cache, to correctly release JNI objects
* ========================================================================================== */
// A realm instance, for faster future getInstance. Do not use it
private var realmLocker: Realm? = null
// The olm account
private var olmAccount: OlmAccount? = null
@ -88,6 +90,8 @@ internal class RealmCryptoStore(private val enableFileEncryption: Boolean = fals
}
override fun open() {
realmLocker = Realm.getInstance(realmConfiguration)
// Ensure CryptoMetadataEntity is inserted in DB
doWithRealm(realmConfiguration) { realm ->
var currentMetadata = realm.where<CryptoMetadataEntity>().findFirst()
@ -133,6 +137,9 @@ internal class RealmCryptoStore(private val enableFileEncryption: Boolean = fals
inboundGroupSessionToRelease.clear()
olmAccount?.releaseAccount()
realmLocker?.close()
realmLocker = null
}
override fun storeDeviceId(deviceId: String) {

View File

@ -16,9 +16,9 @@
package im.vector.matrix.android.internal.crypto.store.db.model
import io.realm.RealmObject
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import io.realm.RealmObject
internal open class IncomingRoomKeyRequestEntity(
var requestId: String? = null,
@ -32,11 +32,11 @@ internal open class IncomingRoomKeyRequestEntity(
) : RealmObject() {
fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest {
return IncomingRoomKeyRequest().apply {
requestId = requestId
userId = userId
deviceId = deviceId
requestBody = RoomKeyRequestBody().apply {
return IncomingRoomKeyRequest().also {
it.requestId = requestId
it.userId = userId
it.deviceId = deviceId
it.requestBody = RoomKeyRequestBody().apply {
algorithm = requestBodyAlgorithm
roomId = requestBodyRoomId
senderKey = requestBodySenderKey

View File

@ -23,10 +23,8 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimBody
import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
import java.util.*
import javax.inject.Inject
internal interface ClaimOneTimeKeysForUsersDeviceTask : Task<ClaimOneTimeKeysForUsersDeviceTask.Params, MXUsersDevicesMap<MXKey>> {
@ -46,30 +44,27 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body)
}.flatMap { keysClaimResponse ->
Try {
val map = HashMap<String, Map<String, MXKey>>()
val map = MXUsersDevicesMap<MXKey>()
if (null != keysClaimResponse.oneTimeKeys) {
for (userId in keysClaimResponse.oneTimeKeys!!.keys) {
val mapByUserId = keysClaimResponse.oneTimeKeys!![userId]
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
for (userId in oneTimeKeys.keys) {
val mapByUserId = oneTimeKeys[userId]
val keysMap = HashMap<String, MXKey>()
if (mapByUserId != null) {
for (deviceId in mapByUserId.keys) {
val mxKey = MXKey.from(mapByUserId[deviceId])
for (deviceId in mapByUserId!!.keys) {
try {
keysMap[deviceId] = MXKey(mapByUserId[deviceId])
} catch (e: Exception) {
Timber.e(e, "## claimOneTimeKeysForUsersDevices : fail to create a MXKey ")
}
}
if (keysMap.size != 0) {
map[userId] = keysMap
if (mxKey != null) {
map.setObject(userId, deviceId, mxKey)
} else {
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
}
}
}
}
}
MXUsersDevicesMap(map)
map
}
}
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification
import android.os.Handler
import android.os.Looper
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
@ -39,7 +40,7 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
@ -55,7 +56,7 @@ import kotlin.collections.HashMap
@SessionScope
internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val myDeviceInfoHolder: MyDeviceInfoHolder,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val deviceListManager: DeviceListManager,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val sendToDeviceTask: SendToDeviceTask,
@ -70,7 +71,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
// Event received from the sync
fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event)
@ -197,7 +198,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.myDevice.fingerprint()!!,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
addTransaction(tx)
@ -222,7 +223,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
.fold(
{ error() },
{
if (it.getUserDeviceIds(otherUserId).contains(startReq.fromDevice)) {
if (it.getUserDeviceIds(otherUserId)?.contains(startReq.fromDevice) == true) {
success(it)
} else {
error()
@ -366,7 +367,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.myDevice.fingerprint()!!,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID,
userId,
deviceID)
@ -409,7 +410,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(cancelMessage, userId, userDevice)
contentMap.setObject(userId, userDevice, cancelMessage)
sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId))
.dispatchTo(object : MatrixCallback<Unit> {

View File

@ -29,8 +29,8 @@ import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
internal class IncomingSASVerificationTransaction(
@ -147,7 +147,7 @@ internal class IncomingSASVerificationTransaction(
//The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB,
// concatenated with the canonical JSON representation of the content of the m.key.verification.start message
val concat = getSAS().publicKey + MoshiProvider.getCanonicalJson(KeyVerificationStart::class.java, startReq!!)
val concat = getSAS().publicKey + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!)
accept.commitment = hashUsingAgreedHashMethod(concat) ?: ""
//we need to send this to other device now
state = SasVerificationTxState.SendingAccept

View File

@ -21,15 +21,14 @@ import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRe
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
internal class OutgoingSASVerificationRequest(
@ -102,8 +101,6 @@ internal class OutgoingSASVerificationRequest(
startMessage.shortAuthenticationStrings = KNOWN_SHORT_CODES
startReq = startMessage
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(startMessage, otherUserId, otherDeviceId)
state = SasVerificationTxState.SendingStart
sendToOther(
@ -167,7 +164,7 @@ internal class OutgoingSASVerificationRequest(
// in Bobs m.key.verification.key and the content of Alices m.key.verification.start message.
//check commitment
val concat = vKey.key + MoshiProvider.getCanonicalJson(KeyVerificationStart::class.java, startReq!!)
val concat = vKey.key + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!)
val otherCommitment = hashUsingAgreedHashMethod(concat) ?: ""
if (accepted!!.commitment.equals(otherCommitment)) {

View File

@ -285,7 +285,7 @@ internal abstract class SASVerificationTransaction(
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(keyToDevice, otherUserId, otherDeviceId)
contentMap.setObject(otherUserId, otherDeviceId, keyToDevice)
sendToDeviceTask.configureWith(SendToDeviceTask.Params(type, contentMap, transactionId))
.dispatchTo(object : MatrixCallback<Unit> {

View File

@ -31,8 +31,7 @@ class RealmLiveData<T : RealmModel>(private val realmConfiguration: RealmConfigu
override fun onActive() {
val realm = Realm.getInstance(realmConfiguration)
val results = query.invoke(realm).findAll()
value = results
val results = query.invoke(realm).findAllAsync()
results.addChangeListener(listener)
this.realm = realm
this.results = results

View File

@ -17,7 +17,11 @@
package im.vector.matrix.android.internal.database
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean
@ -29,20 +33,25 @@ internal interface LiveEntityObserver {
fun isStarted(): Boolean
}
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
: LiveEntityObserver {
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
}
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>()
private lateinit var results: AtomicReference<RealmResults<T>>
override fun start() {
if (isStarted.compareAndSet(false, true)) {
monarchy.postToMonarchyThread {
val queryResults = query.createQuery(it).findAll()
queryResults.addChangeListener { t, changeSet ->
onChanged(t, changeSet)
}
BACKGROUND_HANDLER.post {
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
val queryResults = query.createQuery(realm).findAll()
queryResults.addChangeListener(this)
results = AtomicReference(queryResults)
}
}
@ -50,8 +59,11 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
monarchy.postToMonarchyThread {
BACKGROUND_HANDLER.post {
results.getAndSet(null).removeAllChangeListeners()
backgroundRealm.getAndSet(null).also {
it.close()
}
}
}
}
@ -60,19 +72,4 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
return isStarted.get()
}
private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
val insertionIndexes = changeSet.insertions
val updateIndexes = changeSet.changes
val deletionIndexes = changeSet.deletions
val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
processChanges(inserted, updated, deleted)
}
/**
* Do quick treatment or delegate on a task
*/
protected abstract fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>)
}

View File

@ -22,9 +22,11 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.Sort
@ -32,12 +34,15 @@ import io.realm.Sort
// By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean {
assertIsManaged()
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
return timelineEvents.where()
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
.findAll()
.isEmpty()
}
internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged()
this.events.deleteAllFromRealm()
this.timelineEvents.deleteAllFromRealm()
this.deleteFromRealm()
}
@ -50,21 +55,27 @@ internal fun ChunkEntity.merge(roomId: String,
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.events.forEach { it.isUnlinked = false }
this.timelineEvents.forEach { it.root?.isUnlinked = false }
}
val eventsToMerge: List<EventEntity>
val eventsToMerge: List<TimelineEventEntity>
if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
} else {
this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
}
eventsToMerge.forEach {
add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
val events = eventsToMerge.mapNotNull { it.root?.asDomain() }
val eventIds = ArrayList<String>()
events.forEach { event ->
add(roomId, event, direction, isUnlinked = isUnlinked)
if (event.eventId != null) {
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.addAll(roomId: String,
@ -74,8 +85,20 @@ internal fun ChunkEntity.addAll(roomId: String,
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
isUnlinked: Boolean = false) {
assertIsManaged()
val eventIds = ArrayList<String>()
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset, isUnlinked)
if (event.eventId != null) {
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
for (eventId in eventIds) {
val timelineEventEntity = timelineEvents.find(eventId) ?: continue
timelineEventEntity.updateSenderData()
}
}
@ -86,7 +109,7 @@ internal fun ChunkEntity.add(roomId: String,
isUnlinked: Boolean = false) {
assertIsManaged()
if (event.eventId != null && events.fastContains(event.eventId)) {
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
return
}
var currentDisplayIndex = lastDisplayIndex(direction, 0)
@ -101,21 +124,28 @@ internal fun ChunkEntity.add(roomId: String,
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.getClearType())) {
currentStateIndex += 1
forwardsStateIndex = currentStateIndex
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
val lastEventType = events.last()?.type ?: ""
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
val lastEventType = timelineEvents.last()?.root?.type ?: ""
if (EventType.isStateEvent(lastEventType)) {
currentStateIndex -= 1
backwardsStateIndex = currentStateIndex
}
}
val eventEntity = event.toEntity(roomId).apply {
val localId = TimelineEventEntity.nextId(realm)
val eventEntity = TimelineEventEntity(localId).also {
it.root = event.toEntity(roomId).apply {
this.stateIndex = currentStateIndex
this.isUnlinked = isUnlinked
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity)
it.eventId = event.eventId ?: ""
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, it.eventId).findFirst()
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
}
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {

View File

@ -21,9 +21,10 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.fastContains
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
chunks.remove(chunkEntity)
@ -36,29 +37,39 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
}
}
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
internal fun RoomEntity.addStateEvent(stateEvent: Event,
stateIndex: Int = Int.MIN_VALUE,
filterDuplicates: Boolean = false,
isUnlinked: Boolean = false) {
assertIsManaged()
stateEvents.forEach { event ->
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
return@forEach
}
val eventEntity = event.toEntity(roomId).apply {
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
return
} else {
val entity = stateEvent.toEntity(roomId).apply {
this.stateIndex = stateIndex
this.isUnlinked = isUnlinked
this.sendState = SendState.SYNCED
}
untimelinedStateEvents.add(0, eventEntity)
untimelinedStateEvents.add(entity)
}
}
internal fun RoomEntity.addSendingEvent(event: Event) {
assertIsManaged()
val senderId = event.senderId ?: return
val eventEntity = event.toEntity(roomId).apply {
this.sendState = SendState.UNSENT
}
sendingTimelineEvents.add(0, eventEntity)
val roomMembers = RoomMembers(realm, roomId)
val myUser = roomMembers.get(senderId)
val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity
it.eventId = event.eventId ?: ""
it.roomId = roomId
it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
}
sendingTimelineEvents.add(0, timelineEventEntity)
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2019 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.next
import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.extensions.assertIsManaged
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
internal fun TimelineEventEntity.updateSenderData() {
assertIsManaged()
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return
val stateIndex = root?.stateIndex ?: return
val senderId = root?.sender ?: return
val chunkEntity = chunk?.firstOrNull() ?: return
val isUnlinked = chunkEntity.isUnlinked()
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
when {
stateIndex <= 0 -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.prevContent
}
else -> {
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
senderRoomMemberContent = senderMembershipEvent?.content
}
}
// We fallback to untimelinedStateEvents if we can't find membership events in timeline
if (senderMembershipEvent == null) {
senderMembershipEvent = roomEntity.untimelinedStateEvents
.where()
.equalTo(EventEntityFields.STATE_KEY, senderId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
.prev(since = stateIndex)
senderRoomMemberContent = senderMembershipEvent?.content
}
val senderRoomMember: RoomMember? = ContentMapper.map(senderRoomMemberContent).toModel()
this.senderAvatar = senderRoomMember?.avatarUrl
this.senderName = senderRoomMember?.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(senderRoomMember?.displayName)
this.senderMembershipEvent = senderMembershipEvent
}
internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long{
val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID)
return if (currentIdNum == null) {
1
} else {
currentIdNum.toLong() + 1
}
}
private fun RealmList<TimelineEventEntity>.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery<TimelineEventEntity> {
return where()
.equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
.equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
}

View File

@ -19,7 +19,10 @@ package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
import io.realm.RealmList
internal object EventAnnotationsSummaryMapper {
fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary {
@ -45,6 +48,35 @@ internal object EventAnnotationsSummaryMapper {
}
)
}
fun map(annotationsSummary: EventAnnotationsSummary, roomId: String): EventAnnotationsSummaryEntity {
val eventAnnotationsSummaryEntity = EventAnnotationsSummaryEntity()
eventAnnotationsSummaryEntity.eventId = annotationsSummary.eventId
eventAnnotationsSummaryEntity.roomId = roomId
eventAnnotationsSummaryEntity.editSummary = annotationsSummary.editSummary?.let {
EditAggregatedSummaryEntity(
ContentMapper.map(it.aggregatedContent),
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchos) },
it.lastEditTs
)
}
eventAnnotationsSummaryEntity.reactionsSummary = annotationsSummary.reactionsSummary?.let {
RealmList<ReactionAggregatedSummaryEntity>().apply {
addAll(it.map {
ReactionAggregatedSummaryEntity(
it.key,
it.count,
it.addedByMe,
it.firstTimestamp,
RealmList<String>().apply { addAll(it.sourceEvents) },
RealmList<String>().apply { addAll(it.localEchoEvents) }
)
})
}
}
return eventAnnotationsSummaryEntity
}
}
internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary {

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