mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 16:22:41 +02:00
Compare commits
306 Commits
v0.18.0
...
feature/sq
Author | SHA1 | Date | |
---|---|---|---|
|
9903a299b9 | ||
|
9a4cad1e45 | ||
|
649b4496a6 | ||
|
617b76c0d5 | ||
|
9415d9b5c5 | ||
|
3ba5e97392 | ||
|
671c1259af | ||
|
491f0e6032 | ||
|
63355ca256 | ||
|
8a4f0a0c00 | ||
|
4d207e6acd | ||
|
1227de3f9c | ||
|
6cad129625 | ||
|
9ccf51fbc0 | ||
|
990867204e | ||
|
5795b7e063 | ||
|
b612a7e63c | ||
|
50c73b68aa | ||
|
c7ac5e2293 | ||
|
43cb1fe68b | ||
|
f807de9a83 | ||
|
754f220596 | ||
|
3f7ca8669a | ||
|
28c6921a0a | ||
|
26bb8ce2be | ||
|
8434f9326e | ||
|
68f93c6c31 | ||
|
7961423556 | ||
|
ac07fb47d7 | ||
|
fbcbd6def5 | ||
|
3fe15f2d45 | ||
|
968377a5be | ||
|
5652140f5d | ||
|
e97c95f40a | ||
|
7ac5f58f32 | ||
|
ce2f4e163d | ||
|
cc94b6cf7d | ||
|
ab3cc90ed5 | ||
|
614127e46b | ||
|
66fc38ad4b | ||
|
f68e84d9da | ||
|
2b2e6dd6f8 | ||
|
621e78a864 | ||
|
828e972c74 | ||
|
15bd7d1c5b | ||
|
79e81dbdde | ||
|
f93f50b582 | ||
|
d934f92ebd | ||
|
d20cf484ff | ||
|
ec4458e84a | ||
|
6c1719e365 | ||
|
467f48f1a6 | ||
|
a44cb876c9 | ||
|
e79c824913 | ||
|
b8e9cc70f2 | ||
|
a2f32307f0 | ||
|
c1d39cefd5 | ||
|
0edc562120 | ||
|
8ae2f06044 | ||
|
aa496e6efb | ||
|
1372192031 | ||
|
ea03f76847 | ||
|
d74a5f9979 | ||
|
febadcc4f6 | ||
|
17ece54cb0 | ||
|
da04a74350 | ||
|
634c8947bd | ||
|
f6f6fa99fb | ||
|
dcfbfc4981 | ||
|
6201a9b8ef | ||
|
1981d2e9ac | ||
|
66f6b1ecac | ||
|
affe2b59da | ||
|
757f8ec96a | ||
|
bf5e2b96df | ||
|
5666965321 | ||
|
7bf1f916c4 | ||
|
d44e43d94b | ||
|
9fe32fe915 | ||
|
e21cb3082b | ||
|
c50bc10f92 | ||
|
1b416028b4 | ||
|
85493b7532 | ||
|
dbabe0232f | ||
|
dfc8e8ec4c | ||
|
f00db49bda | ||
|
b4a3eb2cb3 | ||
|
81012746c4 | ||
|
1deacfbb34 | ||
|
c35d854776 | ||
|
c0fa259b40 | ||
|
391d3cb6b5 | ||
|
6d56220d98 | ||
|
c57fa3f0d0 | ||
|
f2bca51046 | ||
|
6751d88ade | ||
|
e26a0bc9ae | ||
|
6dc517584f | ||
|
24d0cdef1f | ||
|
6639f89a68 | ||
|
0ef5ae38d0 | ||
|
0c0e9521f5 | ||
|
f2b684aa9e | ||
|
68ca0e9d4b | ||
|
e54077e020 | ||
|
9111800e7a | ||
|
ef6847671a | ||
|
b8cb0588fe | ||
|
667b371653 | ||
|
190fbb95ec | ||
|
f97e08b4e5 | ||
|
7242cbda40 | ||
|
c53f9359d7 | ||
|
9e3011d4c8 | ||
|
eb50256af7 | ||
|
ccacd20428 | ||
|
ac46fe9e16 | ||
|
9cfb83f0d2 | ||
|
943ba3bebd | ||
|
fccfd00949 | ||
|
68323057aa | ||
|
5081361c2d | ||
|
3ba619d45c | ||
|
f5dc0b38ff | ||
|
8357abd455 | ||
|
ede899d78e | ||
|
a703574004 | ||
|
09c98c32f0 | ||
|
fc90844556 | ||
|
0ca7e6202c | ||
|
ef2abbfbd4 | ||
|
7c0137e2dc | ||
|
96162218e4 | ||
|
ffaacdf436 | ||
|
39fdda3715 | ||
|
34dec64d9c | ||
|
3968bb3488 | ||
|
af171b3b7e | ||
|
6f2d7aebba | ||
|
366a35913b | ||
|
aec49fe542 | ||
|
f04d8b0e03 | ||
|
08af61b778 | ||
|
277f35a352 | ||
|
68512e475f | ||
|
0eff00ebee | ||
|
3dd9693f59 | ||
|
8a4a288074 | ||
|
5b1f887760 | ||
|
dcb6af6c45 | ||
|
8b0845a76b | ||
|
8e896600c9 | ||
|
3ff5952417 | ||
|
b480eb3688 | ||
|
12abca1b80 | ||
|
1d8ed387bc | ||
|
5521c094f7 | ||
|
222b72a014 | ||
|
8904ca27f2 | ||
|
6c5da97c16 | ||
|
d4d73db5ae | ||
|
1a436f962f | ||
|
dc61ee61f5 | ||
|
5b4b5e7a57 | ||
|
0164f94047 | ||
|
153587bd82 | ||
|
326f2e99fb | ||
|
1dfd6f232a | ||
|
42d61944b5 | ||
|
50a8ffeca1 | ||
|
f605bb8270 | ||
|
7ffb6113a4 | ||
|
156e6114c1 | ||
|
c91bc82cd9 | ||
|
8b481e2294 | ||
|
92bf3f1349 | ||
|
6474735662 | ||
|
45c5626267 | ||
|
c27264761d | ||
|
c6abfa14ea | ||
|
2f237cf17b | ||
|
bf5ba99653 | ||
|
8ecdac7c31 | ||
|
a40dd31543 | ||
|
dff89cb2e1 | ||
|
cdbb657961 | ||
|
443d45db6a | ||
|
a995615f87 | ||
|
024c62515c | ||
|
75e66a6550 | ||
|
e2f7890bb8 | ||
|
0a77d5014e | ||
|
91464a071e | ||
|
5244612ef6 | ||
|
e51439ade0 | ||
|
acd90657c7 | ||
|
e5482d48c0 | ||
|
5816a04a37 | ||
|
4b7da9ae6b | ||
|
f7cbc01023 | ||
|
1de57bbf3b | ||
|
42a8c561db | ||
|
5bef9aef6a | ||
|
f8c1ec985f | ||
|
12429d8091 | ||
|
3bb5e127d6 | ||
|
1d46b523b9 | ||
|
6721f337bd | ||
|
535cdf0ef5 | ||
|
19990b27bb | ||
|
4b3c5d5135 | ||
|
b6fe80faf4 | ||
|
638970fa77 | ||
|
c63f3edb06 | ||
|
9a6fe1af4e | ||
|
a01482dca4 | ||
|
5db1010e47 | ||
|
6130a0a654 | ||
|
3c1e1090e7 | ||
|
5cb47dae35 | ||
|
f68e98b2c7 | ||
|
420a55da76 | ||
|
f9aed28732 | ||
|
e30c17eab7 | ||
|
2c9a8865bf | ||
|
bddd70afdb | ||
|
c4388348f7 | ||
|
ee7828a445 | ||
|
37ac45c90a | ||
|
63d3bf93f2 | ||
|
2de8865730 | ||
|
fcffe1f3c3 | ||
|
cfcec04029 | ||
|
b56a41bec7 | ||
|
6bf89aeac9 | ||
|
e583c03751 | ||
|
d20b1cb64a | ||
|
22642e71a3 | ||
|
6e85b20b0e | ||
|
fcd290410e | ||
|
727d86236b | ||
|
2651f82337 | ||
|
3b62402cfe | ||
|
6cc8d1b205 | ||
|
49e5fafb2d | ||
|
e36367c040 | ||
|
f7fd23b153 | ||
|
4f70c40b1a | ||
|
5b875e0571 | ||
|
6db0de321c | ||
|
0c1f30208d | ||
|
08cfe79625 | ||
|
22a599633d | ||
|
14acbb2b4d | ||
|
6f5bebedf8 | ||
|
6fe77eba72 | ||
|
286a5081ff | ||
|
56c241c9bd | ||
|
68151d838f | ||
|
572b174cfe | ||
|
b71d8185a2 | ||
|
8051d9e3be | ||
|
1bf8fef292 | ||
|
b8a9397e73 | ||
|
009d691d5b | ||
|
6933159245 | ||
|
75549c41e0 | ||
|
5e2f888eaf | ||
|
d3d6d44665 | ||
|
fc6225a7ac | ||
|
3639007985 | ||
|
d5137897c1 | ||
|
b67735c31a | ||
|
8ff31ac49d | ||
|
5e0235e48d | ||
|
9e63a3219c | ||
|
757e90986e | ||
|
06fc5c2dd9 | ||
|
20dbe2dd0d | ||
|
6f56c74e9d | ||
|
78d90c8f04 | ||
|
75fa2904d3 | ||
|
a9ed55e6a2 | ||
|
b37600f536 | ||
|
ef2783e9f4 | ||
|
8827b4b5ef | ||
|
7664b716b3 | ||
|
067a22883c | ||
|
eb74523905 | ||
|
18d82b1bea | ||
|
687884431e | ||
|
3c9c2e61d7 | ||
|
e3246a1f2c | ||
|
642651a069 | ||
|
85a987ca8d | ||
|
5cb7e455b1 | ||
|
7c1428e097 | ||
|
32fd4c1be9 | ||
|
f53fc205e1 | ||
|
54eca7525e | ||
|
005a11b765 | ||
|
a8e19f3cc9 | ||
|
cb4752812a | ||
|
ccd9d2961d | ||
|
d1db17f244 | ||
|
aa4327c4da |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@
|
||||
/tmp
|
||||
|
||||
ktlint
|
||||
.idea
|
||||
|
1
.idea/sqldelight/matrix-sdk-sqldelight/.sqldelight
generated
Normal file
1
.idea/sqldelight/matrix-sdk-sqldelight/.sqldelight
generated
Normal file
@@ -0,0 +1 @@
|
||||
{"databases":[{"packageName":"im.vector.matrix.sqldelight.auth","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/auth","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/AuthDatabase","className":"AuthDatabase","dependencies":[]},{"packageName":"im.vector.matrix.sqldelight.crypto","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/crypto","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/CryptoDatabase","className":"CryptoDatabase","dependencies":[]},{"packageName":"im.vector.matrix.sqldelight.session","compilationUnits":[{"name":"main","sourceFolders":[{"path":"src/main/sqldelight/session","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/SessionDatabase","className":"SessionDatabase","dependencies":[]}]}
|
@@ -23,10 +23,10 @@ android:
|
||||
- platform-tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-28.0.3
|
||||
- build-tools-29.0.3
|
||||
|
||||
# The SDK version used to compile your project
|
||||
- android-28
|
||||
- android-29
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
|
53
CHANGES.md
53
CHANGES.md
@@ -1,3 +1,56 @@
|
||||
Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||
- Cross-Signing | Verify new session from existing session (#1134)
|
||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||
|
||||
|
||||
Improvements 🙌:
|
||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||
- Cross-Signing | Update Shield Logic for DM (#963)
|
||||
- Cross-Signing | Complete security new session design update (#1135)
|
||||
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
|
||||
- Cross-Signing | Gossip key backup recovery key (#1200)
|
||||
- Show room encryption status as a bubble tile (#1078)
|
||||
- Cross-Signing | Restore history after recover from passphrase (#1214)
|
||||
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
||||
- Emoji Verification | It's not the same butterfly! (#1220)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Missing avatar/displayname after verification request message (#841)
|
||||
- Crypto | RiotX sometimes rotate the current device keys (#1170)
|
||||
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
||||
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
|
||||
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
|
||||
- Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- Implementation of SqlCryptoStore on top of SQLDelight
|
||||
|
||||
Build 🧱:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- Increase File Logger capacities ( + use dev log preferences)
|
||||
|
||||
Changes in RiotX 0.18.1 (2020-03-17)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Implementation of /join command
|
||||
|
||||
Bugfix 🐛:
|
||||
- Message transitions in encrypted rooms are jarring #518
|
||||
- Images that failed to send are waiting to be sent forever #1145
|
||||
- Fix / Crashed when trying to send a gif from the Gboard #1136
|
||||
- Fix / Cannot click on key backup banner when new keys are available
|
||||
|
||||
|
||||
Changes in RiotX 0.18.0 (2020-03-11)
|
||||
===================================================
|
||||
|
||||
|
@@ -16,7 +16,7 @@ buildscript {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
|
||||
|
||||
classpath 'com.squareup.sqldelight:gradle-plugin:1.2.2'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
@@ -53,7 +53,7 @@ allprojects {
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
// Warnings are potential errors, so stop ignoring them
|
||||
kotlinOptions.allWarningsAsErrors = true
|
||||
kotlinOptions.allWarningsAsErrors = false
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
@@ -38,6 +38,8 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.3'
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
|
||||
|
@@ -27,44 +27,34 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.rx2.asObservable
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
|
||||
return room.getRoomSummaryLive()
|
||||
.asObservable()
|
||||
.startWithCallable { room.roomSummary().toOptional() }
|
||||
return room.getRoomSummaryLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
|
||||
return room.getRoomMembersLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
room.getRoomMembers(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
|
||||
fun liveRoomMember(userId: String): Observable<Optional<RoomMemberSummary>> {
|
||||
return room.getRoomMemberLive(userId).asObservable()
|
||||
}
|
||||
|
||||
fun liveAnnotationSummary(eventId: String): Observable<EventAnnotationsSummary> {
|
||||
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
|
||||
.startWithCallable {
|
||||
room.getEventAnnotationsSummary(eventId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
|
||||
return room.getTimeLineEventLive(eventId).asObservable()
|
||||
.startWithCallable {
|
||||
room.getTimeLineEvent(eventId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
|
||||
return room.getStateEventLive(eventType, stateKey).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvent(eventType, stateKey).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveReadMarker(): Observable<Optional<String>> {
|
||||
@@ -93,7 +83,7 @@ class RxRoom(private val room: Room) {
|
||||
}
|
||||
|
||||
fun liveNotificationState(): Observable<RoomNotificationState> {
|
||||
return room.getLiveRoomNotificationState().asObservable()
|
||||
return room.getRoomNotificationStateLive().asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
|
||||
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.RoomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.model.Breadcrumb
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
@@ -34,28 +35,20 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.rx2.asObservable
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.getRoomSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getRoomSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
|
||||
return session.getGroupSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getGroupSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
|
||||
fun liveBreadcrumbs(): Observable<List<Breadcrumb>> {
|
||||
return session.getBreadcrumbsLive().asObservable()
|
||||
.startWithCallable {
|
||||
session.getBreadcrumbs()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveSyncState(): Observable<SyncState> {
|
||||
@@ -68,9 +61,6 @@ class RxSession(private val session: Session) {
|
||||
|
||||
fun liveUser(userId: String): Observable<Optional<User>> {
|
||||
return session.getUserLive(userId).asObservable()
|
||||
.startWithCallable {
|
||||
session.getUser(userId).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
@@ -125,9 +115,6 @@ class RxSession(private val session: Session) {
|
||||
|
||||
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
|
||||
return session.getLiveAccountDataEvents(types).asObservable()
|
||||
.startWithCallable {
|
||||
session.getAccountDataEvents(types)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,12 +19,12 @@ androidExtensions {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
// Multidex is useful for tests
|
||||
@@ -71,6 +71,10 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'META-INF/kotlinx-coroutines-core.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
static def gitRevision() {
|
||||
@@ -97,6 +101,7 @@ dependencies {
|
||||
def coroutines_version = "1.3.2"
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def work_version = '2.3.3'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
@@ -118,15 +123,20 @@ dependencies {
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
|
||||
// Image
|
||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
|
||||
implementation 'id.zelory:compressor:3.0.0'
|
||||
|
||||
// Database
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||
implementation "com.squareup.sqldelight:android-driver:1.2.2"
|
||||
implementation 'net.zetetic:android-database-sqlcipher:4.3.0'
|
||||
implementation project(':matrix-sdk-sqldelight')
|
||||
implementation "com.squareup.sqldelight:coroutines-extensions:1.2.1"
|
||||
implementation "com.squareup.sqldelight:android-paging-extensions:1.2.1"
|
||||
|
||||
// Work
|
||||
implementation "androidx.work:work-runtime-ktx:2.3.3"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
// FP
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
|
@@ -18,8 +18,5 @@ package im.vector.matrix.android
|
||||
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main,
|
||||
Executors.newSingleThreadExecutor().asCoroutineDispatcher())
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main, Main, Main)
|
||||
|
@@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -265,8 +266,26 @@ class CommonTestHelper(context: Context) {
|
||||
* @param latch
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
fun await(latch: CountDownLatch) {
|
||||
assertTrue(latch.await(TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
||||
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
|
||||
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
||||
}
|
||||
|
||||
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||
GlobalScope.launch {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
if (condition()) {
|
||||
latch.countDown()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
|
||||
val latch = CountDownLatch(1)
|
||||
block(latch)
|
||||
await(latch, timout)
|
||||
}
|
||||
|
||||
// Transform a method with a MatrixCallback to a synchronous method
|
||||
|
@@ -22,8 +22,8 @@ object TestConstants {
|
||||
|
||||
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
|
||||
|
||||
// Time out to use when waiting for server response. 10s
|
||||
private const val AWAIT_TIME_OUT_MILLIS = 10_000
|
||||
// Time out to use when waiting for server response. 20s
|
||||
private const val AWAIT_TIME_OUT_MILLIS = 20_000
|
||||
|
||||
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
|
||||
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
|
||||
|
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.gossiping
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
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.rest.UserPasswordAuth
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.fail
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class KeyShareTests : InstrumentedTest {
|
||||
|
||||
private val mTestHelper = CommonTestHelper(context())
|
||||
|
||||
@Test
|
||||
fun test_DoNotSelfShareIfNotTrusted() {
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
// Create an encrypted room and add a message
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(
|
||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
||||
it
|
||||
)
|
||||
}
|
||||
val room = aliceSession.getRoom(roomId)
|
||||
assertNotNull(room)
|
||||
Thread.sleep(4_000)
|
||||
assertTrue(room?.isEncrypted() == true)
|
||||
val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
|
||||
|
||||
// Open a new sessionx
|
||||
|
||||
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||
|
||||
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||
|
||||
val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId)
|
||||
assertNotNull(receivedEvent)
|
||||
assert(receivedEvent!!.isEncrypted())
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
fail("should fail")
|
||||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
||||
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
// Try to request
|
||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
val waitLatch = CountDownLatch(1)
|
||||
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
||||
|
||||
var outGoingRequestId: String? = null
|
||||
|
||||
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
.filter { req ->
|
||||
// filter out request that was known before
|
||||
!outgoingRequestBefore.any { req.requestId == it.requestId }
|
||||
}
|
||||
.let {
|
||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||
outGoingRequestId = outgoing?.requestId
|
||||
outgoing != null
|
||||
}
|
||||
}
|
||||
mTestHelper.await(waitLatch)
|
||||
|
||||
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
|
||||
// We should have a new request
|
||||
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size)
|
||||
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
|
||||
|
||||
// The first session should see an incoming request
|
||||
// the request should be refused, because the device is not trusted
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// DEBUG LOGS
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach { keyRequest ->
|
||||
Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}")
|
||||
}
|
||||
Log.v("TEST", "=========================")
|
||||
}
|
||||
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId }
|
||||
incoming?.state == GossipingRequestState.REJECTED
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
fail("should fail")
|
||||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
||||
// Mark the device as trusted
|
||||
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
|
||||
aliceSession2.sessionParams.credentials.deviceId ?: "")
|
||||
|
||||
// Re request
|
||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
Log.v("TEST", "Incoming request Session 1")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach {
|
||||
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
|
||||
}
|
||||
Log.v("TEST", "=========================")
|
||||
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(6_000)
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
} catch (failure: Throwable) {
|
||||
fail("should have been able to decrypt")
|
||||
}
|
||||
|
||||
mTestHelper.signOutAndClose(aliceSession)
|
||||
mTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_ShareSSSSSecret() {
|
||||
val aliceSession1 = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
aliceSession1.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(UserPasswordAuth(
|
||||
user = aliceSession1.myUserId,
|
||||
password = TestConstants.PASSWORD
|
||||
), it)
|
||||
}
|
||||
|
||||
// Also bootstrap keybackup on first session
|
||||
val creationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> {
|
||||
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
}
|
||||
val version = mTestHelper.doSync<KeysVersion> {
|
||||
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||
}
|
||||
// Save it for gossiping
|
||||
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||
|
||||
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
|
||||
|
||||
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
|
||||
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
||||
|
||||
// force keys download
|
||||
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
|
||||
}
|
||||
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
|
||||
}
|
||||
|
||||
var session1ShortCode: String? = null
|
||||
var session2ShortCode: String? = null
|
||||
|
||||
aliceVerificationService1.addListener(object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||
if (tx is SasVerificationTransaction) {
|
||||
if (tx.state == VerificationTxState.OnStarted) {
|
||||
(tx as IncomingSasVerificationTransaction).performAccept()
|
||||
}
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
aliceVerificationService2.addListener(object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
||||
if (tx is SasVerificationTransaction) {
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val txId: String = "m.testVerif12"
|
||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
|
||||
?: "", txId)
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(session1ShortCode)
|
||||
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
|
||||
assertNotNull(session2ShortCode)
|
||||
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
|
||||
assertEquals(session1ShortCode, session2ShortCode)
|
||||
|
||||
// SSK and USK private keys should have been shared
|
||||
|
||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
|
||||
aliceSession2.cryptoService().crossSigningService().canCrossSign()
|
||||
}
|
||||
}
|
||||
|
||||
// Test that key backup key has been shared to
|
||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,7 +34,6 @@ import im.vector.matrix.android.common.assertDictEquals
|
||||
import im.vector.matrix.android.common.assertListEquals
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
@@ -326,46 +325,46 @@ class KeysBackupTest : InstrumentedTest {
|
||||
* - Restore must be successful
|
||||
* - *** There must be no more pending key share requests
|
||||
*/
|
||||
@Test
|
||||
fun restoreKeysBackupAndKeyShareRequestTest() {
|
||||
fail("Check with Valere for this test. I think we do not send key share request")
|
||||
|
||||
val testData = createKeysBackupScenarioWithPassword(null)
|
||||
|
||||
// - Check the SDK sent key share requests
|
||||
val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||
val unsentRequest = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
val sentRequest = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
||||
|
||||
// Request is either sent or unsent
|
||||
assertTrue(unsentRequest != null || sentRequest != null)
|
||||
|
||||
// - Restore the e2e backup from the homeserver
|
||||
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
||||
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
|
||||
// - There must be no more pending key share requests
|
||||
val unsentRequestAfterRestoration = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
val sentRequestAfterRestoration = cryptoStore2
|
||||
.getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.SENT))
|
||||
|
||||
// Request is either sent or unsent
|
||||
assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
||||
|
||||
testData.cleanUp(mTestHelper)
|
||||
}
|
||||
// @Test
|
||||
// fun restoreKeysBackupAndKeyShareRequestTest() {
|
||||
// fail("Check with Valere for this test. I think we do not send key share request")
|
||||
//
|
||||
// val testData = createKeysBackupScenarioWithPassword(null)
|
||||
//
|
||||
// // - Check the SDK sent key share requests
|
||||
// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||
// val unsentRequest = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||
// val sentRequest = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||
//
|
||||
// // Request is either sent or unsent
|
||||
// assertTrue(unsentRequest != null || sentRequest != null)
|
||||
//
|
||||
// // - Restore the e2e backup from the homeserver
|
||||
// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
|
||||
// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
|
||||
// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// it
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
|
||||
//
|
||||
// // - There must be no more pending key share requests
|
||||
// val unsentRequestAfterRestoration = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
|
||||
// val sentRequestAfterRestoration = cryptoStore2
|
||||
// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
|
||||
//
|
||||
// // Request is either sent or unsent
|
||||
// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
|
||||
//
|
||||
// testData.cleanUp(mTestHelper)
|
||||
// }
|
||||
|
||||
/**
|
||||
* - Do an e2e backup to the homeserver with a recovery key
|
||||
|
@@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
|
||||
val TEST_KEY_ID = "my.test.Key"
|
||||
|
||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
||||
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||
}
|
||||
|
||||
// Assert Account data is updated
|
||||
@@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
|
||||
val TEST_KEY_ID = "my.test.Key"
|
||||
|
||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
||||
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||
}
|
||||
|
||||
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
|
||||
@@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest {
|
||||
val quadS = session.sharedSecretStorageService
|
||||
|
||||
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||
quadS.generateKey(keyId, keyId, emptyKeySigner, it)
|
||||
quadS.generateKey(keyId, null, keyId, emptyKeySigner, it)
|
||||
}
|
||||
|
||||
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
@@ -23,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.SasMode
|
||||
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||
@@ -355,6 +357,7 @@ class SASTest : InstrumentedTest {
|
||||
val aliceAcceptedLatch = CountDownLatch(1)
|
||||
val aliceListener = object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
Log.v("TEST", "== aliceTx state ${tx.state} => ${(tx as? OutgoingSasVerificationTransaction)?.uxState}")
|
||||
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
|
||||
val at = tx as SASDefaultVerificationTransaction
|
||||
accepted = at.accepted
|
||||
@@ -367,7 +370,9 @@ class SASTest : InstrumentedTest {
|
||||
|
||||
val bobListener = object : VerificationService.Listener {
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
Log.v("TEST", "== bobTx state ${tx.state} => ${(tx as? IncomingSasVerificationTransaction)?.uxState}")
|
||||
if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
|
||||
bobVerificationService.removeListener(this)
|
||||
val at = tx as IncomingSasVerificationTransaction
|
||||
at.performAccept()
|
||||
}
|
||||
@@ -515,4 +520,96 @@ class SASTest : InstrumentedTest {
|
||||
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
|
||||
cryptoTestData.cleanUp(mTestHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_ConcurrentStart() {
|
||||
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession
|
||||
|
||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||
val bobVerificationService = bobSession!!.cryptoService().verificationService()
|
||||
|
||||
val req = aliceVerificationService.requestKeyVerificationInDMs(
|
||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
bobSession.myUserId,
|
||||
cryptoTestData.roomId
|
||||
)
|
||||
|
||||
var requestID : String? = null
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull()
|
||||
requestID = prAlicePOV?.transactionId
|
||||
Log.v("TEST", "== alicePOV is $prAlicePOV")
|
||||
prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("TEST", "== requestID is $requestID")
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
val prBobPOV = bobVerificationService.getExistingVerificationRequest(aliceSession.myUserId)?.firstOrNull()
|
||||
Log.v("TEST", "== prBobPOV is $prBobPOV")
|
||||
prBobPOV?.transactionId == requestID
|
||||
}
|
||||
}
|
||||
|
||||
bobVerificationService.readyPendingVerification(
|
||||
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
aliceSession.myUserId,
|
||||
requestID!!
|
||||
)
|
||||
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
val prAlicePOV = aliceVerificationService.getExistingVerificationRequest(bobSession.myUserId)?.firstOrNull()
|
||||
Log.v("TEST", "== prAlicePOV is $prAlicePOV")
|
||||
prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
|
||||
}
|
||||
}
|
||||
|
||||
// Start concurrent!
|
||||
aliceVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID!!,
|
||||
cryptoTestData.roomId,
|
||||
bobSession.myUserId,
|
||||
bobSession.sessionParams.credentials.deviceId!!,
|
||||
null)
|
||||
|
||||
bobVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID!!,
|
||||
cryptoTestData.roomId,
|
||||
aliceSession.myUserId,
|
||||
aliceSession.sessionParams.credentials.deviceId!!,
|
||||
null)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: SasVerificationTransaction?
|
||||
var bobPovTx: SasVerificationTransaction?
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction
|
||||
Log.v("TEST", "== alicePovTx is $alicePovTx")
|
||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is $bobPovTx")
|
||||
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(mTestHelper)
|
||||
}
|
||||
}
|
||||
|
@@ -18,14 +18,18 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
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.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -229,4 +233,46 @@ class VerificationTest : InstrumentedTest {
|
||||
|
||||
cryptoTestData.cleanUp(mTestHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_alice_sends_text_message_and_bob_can_decrypt() {
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val roomFromBobPOV = bobSession.getRoom(cryptoTestData.roomId)!!
|
||||
|
||||
aliceSession.getRoom(cryptoTestData.roomId)!!.sendTextMessage("test")
|
||||
|
||||
val bobEventsListener = object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
if (snapshot.isNotEmpty() && snapshot.first().root.isEncrypted()) {
|
||||
lock.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
|
||||
bobTimeline.start()
|
||||
bobTimeline.addListener(bobEventsListener)
|
||||
|
||||
mTestHelper.await(lock)
|
||||
|
||||
bobTimeline.getTimelineEventAtIndex(0)?.root?.let {
|
||||
val decryptionResult = bobSession.cryptoService().decryptEvent(it, "")
|
||||
val text = (decryptionResult.clearEvent["content"] as Map<*, *>)["body"]
|
||||
assertEquals("test", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
data class MatrixConfiguration(
|
||||
@@ -61,7 +62,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
||||
Monarchy.init(context)
|
||||
DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
||||
if (context.applicationContext !is Configuration.Provider) {
|
||||
WorkManager.initialize(context, Configuration.Builder().build())
|
||||
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||
}
|
||||
|
@@ -23,5 +23,14 @@ data class MXCryptoConfig(
|
||||
// Tell whether the encryption of the event content is enabled for the invited members.
|
||||
// SDK clients can disable this by settings it to false.
|
||||
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
|
||||
var enableEncryptionForInvitedMembers: Boolean = true
|
||||
var enableEncryptionForInvitedMembers: Boolean = true,
|
||||
|
||||
/**
|
||||
* If set to true, the SDK will automatically ignore room key request (gossiping)
|
||||
* coming from your other untrusted sessions (or blocked).
|
||||
* If set to false, the request will be forwarded to the application layer; in this
|
||||
* case the application can decide to prompt the user.
|
||||
*/
|
||||
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
|
||||
|
||||
)
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.extensions
|
||||
|
||||
inline fun <A> tryThis(operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
@@ -31,3 +31,9 @@ fun Throwable.shouldBeRetried(): Boolean {
|
||||
return this is Failure.NetworkConnection
|
||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||
}
|
||||
|
||||
fun Throwable.isInvalidPassword(): Boolean {
|
||||
return this is Failure.ServerError
|
||||
&& error.code == MatrixError.M_FORBIDDEN
|
||||
&& error.message == "Invalid password"
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.failure.GlobalError
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
@@ -59,7 +60,8 @@ interface Session :
|
||||
InitialSyncProgressService,
|
||||
HomeServerCapabilitiesService,
|
||||
SecureStorageService,
|
||||
AccountDataService {
|
||||
AccountDataService,
|
||||
AccountService {
|
||||
|
||||
/**
|
||||
* The params associated to the session
|
||||
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.account
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to manage the account. It's implemented at the session level.
|
||||
*/
|
||||
interface AccountService {
|
||||
|
||||
/**
|
||||
* Ask the homeserver to change the password.
|
||||
* @param password Current password.
|
||||
* @param newPassword New password
|
||||
*/
|
||||
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.account.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
|
||||
/**
|
||||
* Class to pass request parameters to update the password.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class ChangePasswordParams(
|
||||
@Json(name = "auth")
|
||||
val auth: UserPasswordAuth? = null,
|
||||
|
||||
@Json(name = "new_password")
|
||||
val newPassword: String? = null
|
||||
) {
|
||||
companion object {
|
||||
fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
|
||||
return ChangePasswordParams(
|
||||
auth = UserPasswordAuth(user = userId, password = oldPassword),
|
||||
newPassword = newPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,12 +16,12 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.accountdata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AccountDataService {
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ interface AccountDataService {
|
||||
/**
|
||||
* Observe the account data with the provided type
|
||||
*/
|
||||
fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>>
|
||||
fun getLiveAccountDataEvent(type: String): Flow<Optional<UserAccountDataEvent>>
|
||||
|
||||
/**
|
||||
* Retrieve the account data with the provided types. The return list can have a different size that
|
||||
@@ -44,7 +44,7 @@ interface AccountDataService {
|
||||
/**
|
||||
* Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed
|
||||
*/
|
||||
fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>>
|
||||
fun getLiveAccountDataEvents(types: Set<String>): Flow<List<UserAccountDataEvent>>
|
||||
|
||||
/**
|
||||
* Update the account data with the provided type and the provided account data content
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.content
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
@@ -29,8 +30,7 @@ data class ContentAttachmentData(
|
||||
val width: Long? = 0,
|
||||
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
val name: String? = null,
|
||||
val queryUri: String,
|
||||
val path: String,
|
||||
val queryUri: Uri,
|
||||
private val mimeType: String?,
|
||||
val type: Type
|
||||
) : Parcelable {
|
||||
|
@@ -26,6 +26,11 @@ interface ContentUrlResolver {
|
||||
SCALE("scale")
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to use to upload content
|
||||
*/
|
||||
val uploadUrl: String
|
||||
|
||||
/**
|
||||
* Get the actual URL for accessing the full-size image of a Matrix media content URI.
|
||||
*
|
||||
|
@@ -22,12 +22,14 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
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.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
@@ -86,13 +88,15 @@ interface CryptoService {
|
||||
|
||||
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
|
||||
|
||||
fun requestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
@@ -129,4 +133,8 @@ interface CryptoService {
|
||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
|
||||
interface CrossSigningService {
|
||||
|
||||
@@ -52,6 +53,8 @@ interface CrossSigningService {
|
||||
|
||||
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
@@ -68,4 +71,7 @@ interface CrossSigningService {
|
||||
fun checkDeviceTrust(otherUserId: String,
|
||||
otherDeviceId: String,
|
||||
locallyTrusted: Boolean?): DeviceTrustResult
|
||||
|
||||
fun onSecretSSKGossip(sskPrivateKey: String)
|
||||
fun onSecretUSKGossip(uskPrivateKey: String)
|
||||
}
|
||||
|
@@ -21,3 +21,5 @@ const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master"
|
||||
const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing"
|
||||
|
||||
const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing"
|
||||
|
||||
const val KEYBACKUP_SECRET_SSSS_NAME = "m.megolm_backup.v1"
|
||||
|
@@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCre
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||
|
||||
interface KeysBackupService {
|
||||
/**
|
||||
@@ -172,6 +173,8 @@ interface KeysBackupService {
|
||||
password: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
fun onSecretKeyGossip(secret: String)
|
||||
|
||||
/**
|
||||
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
|
||||
*
|
||||
@@ -210,4 +213,10 @@ interface KeysBackupService {
|
||||
val isEnabled: Boolean
|
||||
val isStucked: Boolean
|
||||
val state: KeysBackupState
|
||||
|
||||
// For gossiping
|
||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||
|
||||
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
|
||||
}
|
||||
|
@@ -17,12 +17,13 @@
|
||||
package im.vector.matrix.android.api.session.crypto.keyshare
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||
|
||||
/**
|
||||
* Room keys events listener
|
||||
*/
|
||||
interface RoomKeysRequestListener {
|
||||
interface GossipingRequestListener {
|
||||
/**
|
||||
* An room key request has been received.
|
||||
*
|
||||
@@ -30,10 +31,16 @@ interface RoomKeysRequestListener {
|
||||
*/
|
||||
fun onRoomKeyRequest(request: IncomingRoomKeyRequest)
|
||||
|
||||
/**
|
||||
* Returns the secret value to be shared
|
||||
* @return true if is handled
|
||||
*/
|
||||
fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation)
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
||||
}
|
@@ -16,7 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.verification
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class EmojiRepresentation(val emoji: String,
|
||||
@StringRes val nameResId: Int)
|
||||
@StringRes val nameResId: Int,
|
||||
@DrawableRes val drawableRes: Int? = null
|
||||
)
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
|
||||
/**
|
||||
@@ -59,6 +60,8 @@ interface VerificationService {
|
||||
roomId: String,
|
||||
localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
|
||||
|
||||
fun cancelVerificationRequest(request: PendingVerificationRequest)
|
||||
|
||||
/**
|
||||
* Request a key verification from another user using toDevice events.
|
||||
*/
|
||||
@@ -136,4 +139,6 @@ interface VerificationService {
|
||||
return age in tooInThePast..tooInTheFuture
|
||||
}
|
||||
}
|
||||
|
||||
fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event)
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ sealed class VerificationTxState {
|
||||
|
||||
// Will be used to ask the user if the other user has correctly scanned
|
||||
object QrScannedByOther : VerificationQrTxState()
|
||||
object WaitingOtherReciprocateConfirm : VerificationQrTxState()
|
||||
|
||||
// Terminal states
|
||||
abstract class TerminalTxState : VerificationTxState()
|
||||
|
@@ -142,12 +142,12 @@ data class Event(
|
||||
}
|
||||
|
||||
fun toContentStringWithIndent(): String {
|
||||
val contentMap = toContent().toMutableMap()
|
||||
val contentMap = toContent()
|
||||
return JSONObject(contentMap).toString(4)
|
||||
}
|
||||
|
||||
fun toClearContentStringWithIndent(): String? {
|
||||
val contentMap = this.mxDecryptionResult?.payload?.toMutableMap()
|
||||
val contentMap = this.mxDecryptionResult?.payload
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.group
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to get groups. It's implemented at the session level.
|
||||
@@ -48,5 +49,5 @@ interface GroupService {
|
||||
* Get a live list of group summaries. This list is refreshed as soon as the data changes.
|
||||
* @return the [LiveData] of [GroupSummary]
|
||||
*/
|
||||
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
|
||||
fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): Flow<List<GroupSummary>>
|
||||
}
|
||||
|
@@ -25,9 +25,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
|
||||
data class GroupSummary(
|
||||
val groupId: String,
|
||||
val membership: Membership,
|
||||
val displayName: String = "",
|
||||
val shortDescription: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val roomIds: List<String> = emptyList(),
|
||||
val userIds: List<String> = emptyList()
|
||||
val displayName: String? = null,
|
||||
val shortDescription: String? = null,
|
||||
val avatarUrl: String? = null
|
||||
)
|
||||
|
@@ -17,6 +17,10 @@
|
||||
package im.vector.matrix.android.api.session.homeserver
|
||||
|
||||
data class HomeServerCapabilities(
|
||||
/**
|
||||
* True if it is possible to change the password of the account.
|
||||
*/
|
||||
val canChangePassword: Boolean = true,
|
||||
/**
|
||||
* Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
|
||||
*/
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.pushers
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.UUID
|
||||
|
||||
interface PushersService {
|
||||
@@ -72,9 +73,9 @@ interface PushersService {
|
||||
fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Get the current pushers, as a LiveData
|
||||
* Get the current pushers, as a Flow
|
||||
*/
|
||||
fun getPushersLive(): LiveData<List<Pusher>>
|
||||
fun getPushersLive(): Flow<List<Pusher>>
|
||||
|
||||
/**
|
||||
* Get the current pushers
|
||||
|
@@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.state.StateService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
import im.vector.matrix.android.api.session.room.typing.TypingService
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact within a room.
|
||||
@@ -56,7 +57,7 @@ interface Room :
|
||||
* A live [RoomSummary] associated with the room
|
||||
* You can observe this summary to get dynamic data from this room.
|
||||
*/
|
||||
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
|
||||
fun getRoomSummaryLive(): Flow<Optional<RoomSummary>>
|
||||
|
||||
/**
|
||||
* A current snapshot of [RoomSummary] associated with the room
|
||||
|
@@ -18,10 +18,12 @@ package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.Breadcrumb
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@@ -67,21 +69,21 @@ interface RoomService {
|
||||
|
||||
/**
|
||||
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
|
||||
* @return the [LiveData] of List[RoomSummary]
|
||||
* @return the [Flow] of List[RoomSummary]
|
||||
*/
|
||||
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
|
||||
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): Flow<List<RoomSummary>>
|
||||
|
||||
/**
|
||||
* Get a snapshot list of Breadcrumbs
|
||||
* @return the immutable list of [RoomSummary]
|
||||
* @return the immutable list of [Breadcrumb]
|
||||
*/
|
||||
fun getBreadcrumbs(): List<RoomSummary>
|
||||
fun getBreadcrumbs(): List<Breadcrumb>
|
||||
|
||||
/**
|
||||
* Get a live list of Breadcrumbs
|
||||
* @return the [LiveData] of [RoomSummary]
|
||||
* @return the [Flow] of [Breadcrumb]
|
||||
*/
|
||||
fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
|
||||
fun getBreadcrumbsLive(): Flow<List<Breadcrumb>>
|
||||
|
||||
/**
|
||||
* Inform the Matrix SDK that a room is displayed.
|
||||
@@ -102,5 +104,5 @@ interface RoomService {
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
|
||||
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
|
||||
*/
|
||||
data class RoomSummaryQueryParams(
|
||||
val fromGroupId: String?,
|
||||
val displayName: QueryStringValue,
|
||||
val canonicalAlias: QueryStringValue,
|
||||
val memberships: List<Membership>
|
||||
@@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
|
||||
|
||||
class Builder {
|
||||
|
||||
var fromGroupId: String? = null
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
fromGroupId = fromGroupId,
|
||||
displayName = displayName,
|
||||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships
|
||||
|
@@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to handling membership. It's implemented at the room level.
|
||||
@@ -40,6 +42,14 @@ interface MembershipService {
|
||||
*/
|
||||
fun getRoomMember(userId: String): RoomMemberSummary?
|
||||
|
||||
/**
|
||||
* Return a live version of an optionnal roomMember with the given userId.
|
||||
* @param userId the userId param to look for
|
||||
*
|
||||
* @return a [Flow] of [Optional] [RoomMemberSummary] with userId
|
||||
*/
|
||||
fun getRoomMemberLive(userId: String): Flow<Optional<RoomMemberSummary>>
|
||||
|
||||
/**
|
||||
* Return all the roomMembers of the room with params
|
||||
* @param queryParams the params to query for
|
||||
@@ -50,9 +60,10 @@ interface MembershipService {
|
||||
/**
|
||||
* Return all the roomMembers of the room filtered by memberships
|
||||
* @param queryParams the params to query for
|
||||
* @return a [LiveData] of roomMember list.
|
||||
* @return a [Flow] of roomMember list.
|
||||
*/
|
||||
fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMemberSummary>>
|
||||
fun getRoomMembersLive(queryParams: RoomMemberQueryParams): Flow<List<RoomMemberSummary>>
|
||||
|
||||
|
||||
fun getNumberOfJoinedMembers(): Int
|
||||
|
||||
|
@@ -0,0 +1,18 @@
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
|
||||
/**
|
||||
* This data class holds data about a breadcrumb.
|
||||
*/
|
||||
data class Breadcrumb(
|
||||
val roomId: String,
|
||||
val displayName: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val notificationCount: Int = 0,
|
||||
val highlightCount: Int = 0,
|
||||
val hasUnreadMessages: Boolean = false,
|
||||
val userDrafts: List<UserDraft> = emptyList(),
|
||||
var isEncrypted: Boolean,
|
||||
val typingRoomMemberIds: List<String> = emptyList()
|
||||
)
|
@@ -31,7 +31,6 @@ data class RoomSummary constructor(
|
||||
val topic: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val canonicalAlias: String? = null,
|
||||
val aliases: List<String> = emptyList(),
|
||||
val isDirect: Boolean = false,
|
||||
val joinedMembersCount: Int? = 0,
|
||||
val invitedMembersCount: Int? = 0,
|
||||
|
@@ -24,7 +24,7 @@ data class AudioInfo(
|
||||
/**
|
||||
* The mimetype of the audio e.g. "audio/aac".
|
||||
*/
|
||||
@Json(name = "mimetype") val mimeType: String,
|
||||
@Json(name = "mimetype") val mimeType: String?,
|
||||
|
||||
/**
|
||||
* The size of the audio clip in bytes.
|
||||
|
@@ -29,7 +29,8 @@ data class MessageLocationContent(
|
||||
@Json(name = "msgtype") override val msgType: String,
|
||||
|
||||
/**
|
||||
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
||||
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind
|
||||
* of content description for accessibility e.g. 'location attachment'.
|
||||
*/
|
||||
@Json(name = "body") override val body: String,
|
||||
|
||||
|
@@ -39,5 +39,5 @@ data class ThumbnailInfo(
|
||||
/**
|
||||
* The mimetype of the image, e.g. "image/jpeg".
|
||||
*/
|
||||
@Json(name = "mimetype") val mimeType: String
|
||||
@Json(name = "mimetype") val mimeType: String?
|
||||
)
|
||||
|
@@ -22,6 +22,7 @@ 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
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* In some cases, events may wish to reference other events.
|
||||
@@ -113,12 +114,12 @@ interface RelationService {
|
||||
* @param eventId the eventId to look for EventAnnotationsSummary
|
||||
* @return the EventAnnotationsSummary found
|
||||
*/
|
||||
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary?
|
||||
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary
|
||||
|
||||
/**
|
||||
* Get a LiveData of EventAnnotationsSummary for the specified eventId
|
||||
* @param eventId the eventId to look for EventAnnotationsSummary
|
||||
* @return the LiveData of EventAnnotationsSummary
|
||||
* @return a [Flow] of EventAnnotationsSummary
|
||||
*/
|
||||
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
||||
fun getEventAnnotationsSummaryLive(eventId: String): Flow<EventAnnotationsSummary>
|
||||
}
|
||||
|
@@ -16,13 +16,13 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.notification
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface RoomPushRuleService {
|
||||
|
||||
fun getLiveRoomNotificationState(): LiveData<RoomNotificationState>
|
||||
fun getRoomNotificationStateLive(): Flow<RoomNotificationState>
|
||||
|
||||
fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
||||
@@ -55,16 +56,16 @@ interface ReadService {
|
||||
/**
|
||||
* Returns a live read marker id for the room.
|
||||
*/
|
||||
fun getReadMarkerLive(): LiveData<Optional<String>>
|
||||
fun getReadMarkerLive(): Flow<Optional<String>>
|
||||
|
||||
/**
|
||||
* Returns a live read receipt id for the room.
|
||||
*/
|
||||
fun getMyReadReceiptLive(): LiveData<Optional<String>>
|
||||
fun getMyReadReceiptLive(): Flow<Optional<String>>
|
||||
|
||||
/**
|
||||
* Returns a live list of read receipts for a given event
|
||||
* @param eventId: the event
|
||||
*/
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
fun getEventReadReceiptsLive(eventId: String): Flow<List<ReadReceipt>>
|
||||
}
|
||||
|
@@ -16,9 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DraftService {
|
||||
|
||||
@@ -36,5 +36,5 @@ interface DraftService {
|
||||
* Return the current drafts if any, as a live data
|
||||
* The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
|
||||
*/
|
||||
fun getDraftsLive(): LiveData<List<UserDraft>>
|
||||
fun getDraftsLive(): Flow<List<UserDraft>>
|
||||
}
|
||||
|
@@ -16,10 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.state
|
||||
|
||||
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.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface StateService {
|
||||
|
||||
@@ -30,5 +30,5 @@ interface StateService {
|
||||
|
||||
fun getStateEvent(eventType: String, stateKey: String): Event?
|
||||
|
||||
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
|
||||
fun getStateEventLive(eventType: String, stateKey: String): Flow<Optional<Event>>
|
||||
}
|
||||
|
@@ -16,8 +16,8 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with the timeline. It's implemented at the room level.
|
||||
@@ -38,5 +38,5 @@ interface TimelineService {
|
||||
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>>
|
||||
}
|
||||
|
@@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
interface KeySigner {
|
||||
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
|
||||
}
|
||||
|
||||
class EmptyKeySigner : KeySigner {
|
||||
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? = null
|
||||
}
|
||||
|
@@ -35,12 +35,14 @@ interface SharedSecretStorageService {
|
||||
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
|
||||
*
|
||||
* @param keyId the ID of the key
|
||||
* @param key keep null if you want to generate a random key
|
||||
* @param keyName a human readable name
|
||||
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
|
||||
*
|
||||
* @param callback Get key creation info
|
||||
*/
|
||||
fun generateKey(keyId: String,
|
||||
key: SsssKeySpec?,
|
||||
keyName: String,
|
||||
keySigner: KeySigner?,
|
||||
callback: MatrixCallback<SsssKeyCreationInfo>)
|
||||
@@ -111,6 +113,8 @@ interface SharedSecretStorageService {
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
||||
data class KeyRef(
|
||||
val keyId: String?,
|
||||
val keySpec: SsssKeySpec?
|
||||
|
@@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
data class SsssKeyCreationInfo(
|
||||
val keyId: String = "",
|
||||
var content: SecretStorageKeyContent?,
|
||||
val recoveryKey: String = ""
|
||||
val recoveryKey: String = "",
|
||||
val keySpec: SsssKeySpec
|
||||
)
|
||||
|
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to get users. It's implemented at the session level.
|
||||
@@ -48,27 +49,27 @@ interface UserService {
|
||||
/**
|
||||
* Observe a live user from a userId
|
||||
* @param userId the userId to look for.
|
||||
* @return a LiveData of user with userId
|
||||
* @return a Flow of user with userId
|
||||
*/
|
||||
fun getUserLive(userId: String): LiveData<Optional<User>>
|
||||
fun getUserLive(userId: String): Flow<Optional<User>>
|
||||
|
||||
/**
|
||||
* Observe a live list of users sorted alphabetically
|
||||
* @return a Livedata of users
|
||||
* @return a Flow of users
|
||||
*/
|
||||
fun getUsersLive(): LiveData<List<User>>
|
||||
fun getUsersLive(): Flow<List<User>>
|
||||
|
||||
/**
|
||||
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
|
||||
* @param filter the filter. It will look into userId and displayName.
|
||||
* @return a Livedata of users
|
||||
* @return a Flow of users
|
||||
*/
|
||||
fun getPagedUsersLive(filter: String? = null): LiveData<PagedList<User>>
|
||||
fun getPagedUsersLive(filter: String? = null): Flow<PagedList<User>>
|
||||
|
||||
/**
|
||||
* Get list of ignored users
|
||||
*/
|
||||
fun getIgnoredUsersLive(): LiveData<List<User>>
|
||||
fun getIgnoredUsersLive(): Flow<List<User>>
|
||||
|
||||
/**
|
||||
* Ignore users
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util
|
||||
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Breadcrumb
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
@@ -148,6 +149,8 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava
|
||||
|
||||
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
|
||||
|
||||
fun Breadcrumb.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
|
||||
|
||||
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
|
||||
|
||||
// If no name is available, use room alias as Riot-Web does
|
||||
|
@@ -17,16 +17,19 @@
|
||||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
|
||||
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
|
||||
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||
import im.vector.matrix.android.internal.auth.realm.AuthRealmMigration
|
||||
import im.vector.matrix.android.internal.auth.realm.AuthRealmModule
|
||||
import im.vector.matrix.android.internal.auth.sqlite.AuthSchema
|
||||
import im.vector.matrix.android.internal.auth.sqlite.SqlitePendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.sqlite.SqliteSessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import im.vector.matrix.sqldelight.auth.AuthDatabase
|
||||
import io.realm.RealmConfiguration
|
||||
import java.io.File
|
||||
|
||||
@@ -39,16 +42,16 @@ internal abstract class AuthModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@AuthDatabase
|
||||
fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
@im.vector.matrix.android.internal.di.RealmAuthDatabase
|
||||
@MatrixScope
|
||||
fun providesRealmConfiguration(context: Context, databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
|
||||
val old = File(context.filesDir, "matrix-sdk-auth")
|
||||
if (old.exists()) {
|
||||
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
|
||||
}
|
||||
|
||||
return RealmConfiguration.Builder()
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, DB_ALIAS)
|
||||
databaseKeysUtils.configureEncryption(this, DB_ALIAS)
|
||||
}
|
||||
.name("matrix-sdk-auth.realm")
|
||||
.modules(AuthRealmModule())
|
||||
@@ -56,13 +59,22 @@ internal abstract class AuthModule {
|
||||
.migration(AuthRealmMigration)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@MatrixScope
|
||||
fun providesAuthDatabase(context: Context, authSchema: AuthSchema, databaseKeysUtils: DatabaseKeysUtils): AuthDatabase {
|
||||
val supportFactory = databaseKeysUtils.createEncryptedSQLiteOpenHelperFactory(DB_ALIAS)
|
||||
val driver = AndroidSqliteDriver(authSchema, context, "matrix-sdk-auth.db", factory = supportFactory)
|
||||
return AuthDatabase.invoke(driver)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
||||
abstract fun bindSessionParamsStore(sessionParamsStore: SqliteSessionParamsStore): SessionParamsStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore
|
||||
abstract fun bindPendingSessionStore(pendingSessionStore: SqlitePendingSessionStore): PendingSessionStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService
|
||||
|
@@ -20,13 +20,7 @@ import android.net.Uri
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.auth.data.Versions
|
||||
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
|
||||
import im.vector.matrix.android.api.auth.data.isSupportedBySdk
|
||||
import im.vector.matrix.android.api.auth.data.*
|
||||
import im.vector.matrix.android.api.auth.login.LoginWizard
|
||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
@@ -35,7 +29,7 @@ import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
|
||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
|
||||
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
|
||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
@@ -114,7 +108,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
return withContext(coroutineDispatchers.computation) {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
// First check the homeserver version
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
|
||||
/**
|
||||
* Store for elements when doing login or registration
|
||||
|
@@ -30,7 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
|
||||
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
|
||||
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.sessionId
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import io.realm.annotations.RealmModule
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
@@ -14,11 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.ThreePidData
|
||||
import javax.inject.Inject
|
||||
|
@@ -14,17 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||
import im.vector.matrix.android.internal.di.RealmAuthDatabase
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
|
||||
@AuthDatabase
|
||||
@RealmAuthDatabase
|
||||
private val realmConfiguration: RealmConfiguration
|
||||
) : PendingSessionStore {
|
||||
|
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.auth.data.sessionId
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||
import im.vector.matrix.android.internal.di.RealmAuthDatabase
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.exceptions.RealmPrimaryKeyConstraintException
|
||||
@@ -29,7 +29,7 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
|
||||
@AuthDatabase
|
||||
@RealmAuthDatabase
|
||||
private val realmConfiguration: RealmConfiguration
|
||||
) : SessionParamsStore {
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.realm
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
@@ -28,6 +29,15 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
private val credentialsAdapter = moshi.adapter(Credentials::class.java)
|
||||
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
|
||||
|
||||
fun map(credentialsJson: String, homeServerConnectionConfigJson: String, isTokenValid: Boolean): SessionParams {
|
||||
val credentials = credentialsAdapter.fromJson(credentialsJson)
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(homeServerConnectionConfigJson)
|
||||
if (credentials == null || homeServerConnectionConfig == null) {
|
||||
throw JsonDataException()
|
||||
}
|
||||
return SessionParams(credentials, homeServerConnectionConfig, isTokenValid)
|
||||
}
|
||||
|
||||
fun map(entity: SessionParamsEntity?): SessionParams? {
|
||||
if (entity == null) {
|
||||
return null
|
@@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.auth.AuthAPI
|
||||
import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.db
|
||||
package im.vector.matrix.android.internal.auth.registration
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
|
||||
import im.vector.matrix.android.internal.auth.registration.ThreePidData
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This class holds all pending data when creating a session, either by login or by register
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 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.auth.sqlite
|
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import im.vector.matrix.sqldelight.auth.AuthDatabase
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AuthSchema @Inject constructor(@im.vector.matrix.android.internal.di.RealmAuthDatabase private val realmConfiguration: RealmConfiguration) : SqlDriver.Schema by AuthDatabase.Schema {
|
||||
|
||||
override fun create(driver: SqlDriver) {
|
||||
AuthDatabase.Schema.create(driver)
|
||||
AuthDatabase(driver).apply {
|
||||
val sessionParamsQueries = this.sessionParamsQueries
|
||||
sessionParamsQueries.transaction {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.where(im.vector.matrix.android.internal.auth.realm.SessionParamsEntity::class.java).findAll().forEach { realmSessionParams ->
|
||||
sessionParamsQueries.insert(realmSessionParams.toSqlModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun im.vector.matrix.android.internal.auth.realm.SessionParamsEntity.toSqlModel(): im.vector.matrix.sqldelight.auth.SessionParamsEntity {
|
||||
return im.vector.matrix.sqldelight.auth.SessionParamsEntity.Impl(
|
||||
sessionId,
|
||||
userId,
|
||||
credentialsJson,
|
||||
homeServerConnectionConfigJson,
|
||||
isTokenValid
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2020 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.auth.sqlite
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.auth.registration.ThreePidData
|
||||
import im.vector.matrix.sqldelight.auth.PendingSessionEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SqlitePendingSessionMapper @Inject constructor(moshi: Moshi) {
|
||||
|
||||
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
|
||||
private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
|
||||
private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
|
||||
|
||||
fun map(entity: PendingSessionEntity?): PendingSessionData? {
|
||||
if (entity == null) {
|
||||
return null
|
||||
}
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.home_server_connection_config_json)!!
|
||||
val resetPasswordData = entity.reset_password_data_json?.let { resetPasswordDataAdapter.fromJson(it) }
|
||||
val threePidData = entity.current_three_pid_data_json?.let { threePidDataAdapter.fromJson(it) }
|
||||
|
||||
return PendingSessionData(
|
||||
homeServerConnectionConfig = homeServerConnectionConfig,
|
||||
clientSecret = entity.client_secret,
|
||||
sendAttempt = entity.send_attempts,
|
||||
resetPasswordData = resetPasswordData,
|
||||
currentSession = entity.current_session,
|
||||
isRegistrationStarted = entity.is_registration_started,
|
||||
currentThreePidData = threePidData)
|
||||
}
|
||||
|
||||
fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
|
||||
if (sessionData == null) {
|
||||
return null
|
||||
}
|
||||
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
|
||||
val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
|
||||
val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
|
||||
|
||||
return PendingSessionEntity.Impl(
|
||||
home_server_connection_config_json = homeServerConnectionConfigJson,
|
||||
client_secret = sessionData.clientSecret,
|
||||
send_attempts = sessionData.sendAttempt,
|
||||
reset_password_data_json = resetPasswordDataJson,
|
||||
current_session = sessionData.currentSession,
|
||||
is_registration_started = sessionData.isRegistrationStarted,
|
||||
current_three_pid_data_json = currentThreePidDataJson
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 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.auth.sqlite
|
||||
|
||||
import im.vector.matrix.android.internal.auth.PendingSessionStore
|
||||
import im.vector.matrix.android.internal.auth.registration.PendingSessionData
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.sqldelight.auth.PendingSessionQueries
|
||||
import im.vector.matrix.sqldelight.auth.AuthDatabase
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SqlitePendingSessionStore @Inject constructor(database: AuthDatabase,
|
||||
private val mapper: SqlitePendingSessionMapper,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) : PendingSessionStore {
|
||||
|
||||
private val pendingSessionQueries: PendingSessionQueries = database.pendingSessionQueries
|
||||
|
||||
override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) = withContext(coroutineDispatchers.dbTransaction) {
|
||||
val pendingSessionEntity = mapper.map(pendingSessionData)
|
||||
if (pendingSessionEntity != null) {
|
||||
pendingSessionQueries.transaction {
|
||||
pendingSessionQueries.delete()
|
||||
pendingSessionQueries.insertOrUpdate(pendingSessionEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPendingSessionData(): PendingSessionData? {
|
||||
val pendingSessionEntity = pendingSessionQueries.get().executeAsOneOrNull()
|
||||
return mapper.map(pendingSessionEntity)
|
||||
}
|
||||
|
||||
override suspend fun delete() = withContext(coroutineDispatchers.dbTransaction) {
|
||||
pendingSessionQueries.delete()
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2020 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.auth.sqlite
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.auth.data.sessionId
|
||||
import im.vector.matrix.sqldelight.auth.SessionParamsEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SqliteSessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
|
||||
val credentialsAdapter = moshi.adapter(Credentials::class.java)
|
||||
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
|
||||
|
||||
fun map(credentialsJson: String, homeServerConnectionConfigJson: String, isTokenValid: Boolean): SessionParams {
|
||||
val credentials = credentialsAdapter.fromJson(credentialsJson)
|
||||
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(homeServerConnectionConfigJson)
|
||||
if (credentials == null || homeServerConnectionConfig == null) {
|
||||
throw JsonDataException()
|
||||
}
|
||||
return SessionParams(credentials, homeServerConnectionConfig, isTokenValid)
|
||||
}
|
||||
|
||||
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
|
||||
if (sessionParams == null) {
|
||||
return null
|
||||
}
|
||||
val credentialsJson = credentialsAdapter.toJson(sessionParams.credentials)
|
||||
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionParams.homeServerConnectionConfig)
|
||||
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
|
||||
return null
|
||||
}
|
||||
return SessionParamsEntity.Impl(
|
||||
session_id = sessionParams.credentials.sessionId(),
|
||||
user_id = sessionParams.credentials.userId,
|
||||
credentials_json = credentialsJson,
|
||||
home_server_connection_config_json = homeServerConnectionConfigJson,
|
||||
is_token_valid = sessionParams.isTokenValid)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 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.auth.sqlite
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.auth.data.sessionId
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.sqldelight.auth.SessionParamsQueries
|
||||
import im.vector.matrix.sqldelight.auth.AuthDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SqliteSessionParamsStore @Inject constructor(database: AuthDatabase,
|
||||
private val mapper: SqliteSessionParamsMapper) : SessionParamsStore {
|
||||
|
||||
private val sessionParamsQueries: SessionParamsQueries = database.sessionParamsQueries
|
||||
|
||||
override fun get(sessionId: String): SessionParams? {
|
||||
return sessionParamsQueries.getSessionParamsWithId(sessionId) { credentials_json, home_server_connection_config_json, is_token_valid ->
|
||||
mapper.map(credentials_json, home_server_connection_config_json, is_token_valid)
|
||||
}.executeAsOneOrNull()
|
||||
}
|
||||
|
||||
override fun getLast(): SessionParams? {
|
||||
return getAll().lastOrNull()
|
||||
}
|
||||
|
||||
override fun getAll(): List<SessionParams> {
|
||||
return sessionParamsQueries.getAllSessionParams { credentials_json, home_server_connection_config_json, is_token_valid ->
|
||||
mapper.map(credentials_json, home_server_connection_config_json, is_token_valid)
|
||||
}.executeAsList()
|
||||
}
|
||||
|
||||
override suspend fun save(sessionParams: SessionParams) {
|
||||
val sessionParamsEntity = mapper.map(sessionParams)
|
||||
if (sessionParamsEntity != null) {
|
||||
sessionParamsQueries.insert(sessionParamsEntity)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTokenInvalid(sessionId: String) {
|
||||
sessionParamsQueries.setTokenInvalid(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun updateCredentials(newCredentials: Credentials) {
|
||||
val newCredentialsJson = mapper.credentialsAdapter.toJson(newCredentials)
|
||||
sessionParamsQueries.updateCredentials(newCredentialsJson, newCredentials.sessionId())
|
||||
}
|
||||
|
||||
override suspend fun delete(sessionId: String) {
|
||||
sessionParamsQueries.delete(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun deleteAll() {
|
||||
sessionParamsQueries.deleteAll()
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package im.vector.matrix.android.internal.concurrency
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
|
||||
internal class NamedThreadFactory(private val name: String) : ThreadFactory {
|
||||
|
||||
override fun newThread(runnable: Runnable): Thread {
|
||||
return Thread(runnable, name)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun newNamedSingleThreadExecutor(name: String): Executor {
|
||||
return Executors.newSingleThreadExecutor(NamedThreadFactory(name))
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
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.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CancelGossipRequestWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
val requestId: String,
|
||||
val recipients: Map<String, List<String>>
|
||||
) {
|
||||
companion object {
|
||||
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
|
||||
return Params(
|
||||
sessionId = sessionId,
|
||||
requestId = request.requestId,
|
||||
recipients = request.recipients
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val toDeviceContent = ShareRequestCancellation(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.requestId
|
||||
)
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = EventType.ROOM_KEY_REQUEST,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = EventType.ROOM_KEY_REQUEST,
|
||||
contentMap = contentMap,
|
||||
transactionId = localId
|
||||
)
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,9 +16,15 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import com.squareup.sqldelight.logs.LogSqliteDriver
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
@@ -55,9 +61,9 @@ import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessio
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import im.vector.matrix.android.internal.crypto.store.db.SqlCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceTask
|
||||
@@ -66,6 +72,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultDownloadKeysForUser
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||
@@ -78,19 +85,22 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
|
||||
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
||||
import im.vector.matrix.sqldelight.crypto.CrossSigningInfoEntity
|
||||
import im.vector.matrix.sqldelight.crypto.CryptoDatabase
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -106,15 +116,15 @@ internal abstract class CryptoModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
@RealmCryptoDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
||||
databaseKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
||||
}
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
@@ -123,6 +133,31 @@ internal abstract class CryptoModule {
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionScope
|
||||
fun providesCryptoDatabase(context: Context, @SessionFilesDirectory directory: File): CryptoDatabase {
|
||||
val name = "${directory.name}-matrix-sdk-crypto.db"
|
||||
val driver = if (BuildConfig.DEBUG) {
|
||||
LogSqliteDriver(AndroidSqliteDriver(CryptoDatabase.Schema, context, name)) { log ->
|
||||
Log.d("SQLite", log)
|
||||
}
|
||||
} else {
|
||||
AndroidSqliteDriver(CryptoDatabase.Schema, context, name)
|
||||
}
|
||||
|
||||
val listOfStringAdapter = object : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String) = databaseValue.split(",")
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = ",")
|
||||
}
|
||||
return CryptoDatabase(
|
||||
driver = driver,
|
||||
crossSigningInfoEntityAdapter = CrossSigningInfoEntity.Adapter(
|
||||
usagesAdapter = listOfStringAdapter
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionScope
|
||||
@@ -132,8 +167,8 @@ internal abstract class CryptoModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
fun providesClearCacheTask(@CryptoDatabase
|
||||
@RealmCryptoDatabase
|
||||
fun providesClearCacheTask(@RealmCryptoDatabase
|
||||
realmConfiguration: RealmConfiguration): ClearCacheTask {
|
||||
return RealmClearCacheTask(realmConfiguration)
|
||||
}
|
||||
@@ -241,8 +276,11 @@ internal abstract class CryptoModule {
|
||||
abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
||||
abstract fun bindCryptoStore(store: SqlCryptoStore): IMXCryptoStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
}
|
||||
|
@@ -23,7 +23,6 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
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.NoOpMatrixCallback
|
||||
@@ -33,7 +32,9 @@ import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
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.api.session.events.model.EventType
|
||||
@@ -50,29 +51,19 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFa
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
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.*
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
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.DeleteDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
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.whereType
|
||||
import im.vector.matrix.android.internal.database.repository.CurrentStateEventDataSource
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
@@ -80,16 +71,12 @@ import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembers
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||
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.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 im.vector.matrix.android.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import kotlinx.coroutines.*
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -115,6 +102,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
// the crypto store
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
|
||||
// Olm device
|
||||
private val olmDevice: MXOlmDevice,
|
||||
// Set of parameters used to configure/customize the end-to-end crypto.
|
||||
@@ -134,9 +122,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
|
||||
private val crossSigningService: DefaultCrossSigningService,
|
||||
//
|
||||
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
//
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
// Actions
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||
@@ -152,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
private val setDeviceNameTask: SetDeviceNameTask,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
@@ -171,16 +159,16 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
|
||||
fun onStateEvent(roomId: String, event: Event) {
|
||||
when {
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLiveEvent(roomId: String, event: Event) {
|
||||
when {
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||
}
|
||||
}
|
||||
@@ -188,6 +176,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// bg refresh of crypto device
|
||||
@@ -206,6 +195,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceTask
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@@ -214,6 +204,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceWithUserPasswordTask
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@@ -230,6 +221,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.configureWith {
|
||||
// this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@@ -238,6 +230,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
||||
getDeviceInfoTask
|
||||
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
@@ -300,14 +293,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
runCatching {
|
||||
uploadDeviceKeys()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackupService.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
@@ -328,8 +320,6 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
|
||||
outgoingRoomKeyRequestManager.stop()
|
||||
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
@@ -368,7 +358,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,7 +489,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
|
||||
val alg: IMXEncrypting = when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||
else -> olmEncryptionFactory.create(roomId)
|
||||
else -> olmEncryptionFactory.create(roomId)
|
||||
}
|
||||
|
||||
synchronized(roomEncryptors) {
|
||||
@@ -533,12 +523,10 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
|
||||
*/
|
||||
override fun isRoomEncrypted(roomId: String): Boolean {
|
||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.findFirst()
|
||||
}
|
||||
return encryptionEvent != null
|
||||
return sessionDatabase.eventQueries
|
||||
.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.executeAsList()
|
||||
.firstOrNull() != null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -627,7 +615,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return internalDecryptEvent(event, timeline)
|
||||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,15 +676,26 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
* @param event the event
|
||||
*/
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
// event have already been decrypted
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
else -> {
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
else -> {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -710,18 +709,59 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
||||
Timber.e("## GOSSIP 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("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||
return
|
||||
}
|
||||
alg.onRoomKeyEvent(event, keysBackupService)
|
||||
}
|
||||
|
||||
private fun onSecretSendReceived(event: Event) {
|
||||
Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||
if (!event.isEncrypted()) {
|
||||
// secret send messages must be encrypted
|
||||
Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (event.senderId != credentials.userId) {
|
||||
Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||
return
|
||||
}
|
||||
|
||||
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
|
||||
|
||||
val existingRequest = cryptoStore
|
||||
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
||||
|
||||
if (existingRequest == null) {
|
||||
Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||
return
|
||||
}
|
||||
|
||||
when (existingRequest.secretName) {
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
USER_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
else -> {
|
||||
// Ask to application layer?
|
||||
Timber.v("## onSecretSend() : secret not handled by SDK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an m.room.encryption event.
|
||||
*
|
||||
@@ -735,25 +775,21 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable)
|
||||
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRoomUserIds(roomId: String): List<String> {
|
||||
var userIds: List<String> = emptyList()
|
||||
monarchy.doWithRealm { realm ->
|
||||
// Check whether the event content must be encrypted for the invited members.
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
// Check whether the event content must be encrypted for the invited members.
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
|
||||
userIds = if (encryptForInvitedMembers) {
|
||||
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
return if (encryptForInvitedMembers) {
|
||||
RoomMemberHelper(sessionDatabase, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(sessionDatabase, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
return userIds
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -997,14 +1033,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||
}
|
||||
|
||||
// TODO Check if this method is still necessary
|
||||
// TODO Check if this method is still necessary
|
||||
/**
|
||||
* Cancel any earlier room key request
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1013,38 +1049,54 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
* @param event the event to decrypt again.
|
||||
*/
|
||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content
|
||||
if (wireContent == null) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||
return
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
algorithm = wireContent["algorithm"]?.toString(),
|
||||
algorithm = wireContent.algorithm,
|
||||
roomId = event.roomId,
|
||||
senderKey = wireContent["sender_key"]?.toString(),
|
||||
sessionId = wireContent["session_id"]?.toString()
|
||||
senderKey = wireContent.senderKey,
|
||||
sessionId = wireContent.sessionId
|
||||
)
|
||||
|
||||
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
||||
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
override fun requestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
||||
}
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## requestRoomKeyForEvent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
}
|
||||
roomDecryptorProvider
|
||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||
?.requestKeysForEvent(event) ?: run {
|
||||
Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RoomKeysRequestListener listener.
|
||||
* Add a GossipingRequestListener listener.
|
||||
*
|
||||
* @param listener listener
|
||||
*/
|
||||
override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
|
||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RoomKeysRequestListener listener.
|
||||
* Add a GossipingRequestListener listener.
|
||||
*
|
||||
* @param listener listener
|
||||
*/
|
||||
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1084,11 +1136,23 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override fun removeSessionListener(listener: NewSessionListener) {
|
||||
roomDecryptorProvider.removeSessionListener(listener)
|
||||
}
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
|
||||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
}
|
||||
|
@@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
|
||||
// Handle cross signing keys update
|
||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
||||
Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
||||
}
|
||||
cryptoStore.storeUserCrossSigningKeys(
|
||||
userId,
|
||||
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
enum class GossipRequestType {
|
||||
KEY,
|
||||
SECRET
|
||||
}
|
||||
|
||||
enum class GossipingRequestState {
|
||||
NONE,
|
||||
PENDING,
|
||||
REJECTED,
|
||||
ACCEPTING,
|
||||
ACCEPTED,
|
||||
FAILED_TO_ACCEPTED,
|
||||
// USER_REJECTED,
|
||||
UNABLE_TO_PROCESS,
|
||||
CANCELLED_BY_REQUESTER,
|
||||
RE_REQUESTED
|
||||
}
|
||||
|
||||
enum class OutgoingGossipingRequestState {
|
||||
UNSENT,
|
||||
SENDING,
|
||||
SENT,
|
||||
CANCELLING,
|
||||
CANCELLED,
|
||||
FAILED_TO_SEND,
|
||||
FAILED_TO_CANCEL
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.CancelableWork
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class GossipingWorkManager @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider
|
||||
) {
|
||||
|
||||
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.startChain(startChain)
|
||||
.setInputData(data)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Prevent sending queue to stay broken after app restart
|
||||
// The unique queue id will stay the same as long as this object is instanciated
|
||||
val queueSuffixApp = System.currentTimeMillis()
|
||||
|
||||
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
|
||||
.enqueue()
|
||||
|
||||
return CancelableWork(workManagerProvider.workManager, workRequest.id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
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.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingDefaultContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class IncomingGossipingRequestManager @Inject constructor(
|
||||
@SessionId private val sessionId: String,
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val cryptoConfig: MXCryptoConfig,
|
||||
private val gossipingWorkManager: GossipingWorkManager,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
||||
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
|
||||
|
||||
// the listeners
|
||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
||||
|
||||
init {
|
||||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||
}
|
||||
|
||||
// Recently verified devices (map of deviceId and timestamp)
|
||||
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
||||
|
||||
/**
|
||||
* Called when a session has been verified.
|
||||
* This information can be used by the manager to decide whether or not to fullfil gossiping requests
|
||||
*/
|
||||
fun onVerificationCompleteForDevice(deviceId: String) {
|
||||
// For now we just keep an in memory cache
|
||||
synchronized(recentlyVerifiedDevices) {
|
||||
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||
val verifTimestamp: Long?
|
||||
synchronized(recentlyVerifiedDevices) {
|
||||
verifTimestamp = recentlyVerifiedDevices[deviceId]
|
||||
}
|
||||
if (verifTimestamp == null) return false
|
||||
|
||||
val age = System.currentTimeMillis() - verifTimestamp
|
||||
|
||||
return age < FIVE_MINUTES_IN_MILLIS
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we get an m.room_key_request event
|
||||
* It must be called on CryptoThread
|
||||
*
|
||||
* @param event the announcement event.
|
||||
*/
|
||||
fun onGossipingRequestEvent(event: Event) {
|
||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
||||
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
when (roomKeyShare?.action) {
|
||||
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
||||
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
IncomingSecretShareRequest.fromEvent(event)?.let {
|
||||
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||
// ignore, it was sent by me as *
|
||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||
} else {
|
||||
// save in DB
|
||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
receivedGossipingRequests.add(it)
|
||||
}
|
||||
}
|
||||
} else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
IncomingRoomKeyRequest.fromEvent(event)?.let {
|
||||
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||
// ignore, it was sent by me as *
|
||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||
} else {
|
||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
receivedGossipingRequests.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
|
||||
IncomingRequestCancellation.fromEvent(event)?.let {
|
||||
receivedRequestCancellations.add(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any m.room_key_request or m.secret.request events which were queued up during the
|
||||
* current sync.
|
||||
* It must be called on CryptoThread
|
||||
*/
|
||||
fun processReceivedGossipingRequests() {
|
||||
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
||||
receivedGossipingRequests.clear()
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
if (request is IncomingRoomKeyRequest) {
|
||||
processIncomingRoomKeyRequest(request)
|
||||
} else if (request is IncomingSecretShareRequest) {
|
||||
processIncomingSecretShareRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
|
||||
|
||||
synchronized(this.receivedRequestCancellations) {
|
||||
if (this.receivedRequestCancellations.isNotEmpty()) {
|
||||
receivedRequestCancellations = this.receivedRequestCancellations.toList()
|
||||
this.receivedRequestCancellations.clear()
|
||||
}
|
||||
}
|
||||
|
||||
receivedRequestCancellations?.forEach { request ->
|
||||
Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
||||
// ignore remote echo
|
||||
return@forEach
|
||||
}
|
||||
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
||||
if (matchingIncoming == null) {
|
||||
// ignore that?
|
||||
return@forEach
|
||||
} else {
|
||||
// If it was accepted from this device, keep the information, do not mark as cancelled
|
||||
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
||||
onRoomKeyRequestCancellation(request)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
val body = request.requestBody
|
||||
val roomId = body!!.roomId
|
||||
val alg = body.algorithm
|
||||
|
||||
Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||
if (userId == null || credentials.userId != userId) {
|
||||
// TODO: determine if we sent this device the keys already: in
|
||||
Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
// 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)
|
||||
if (null == decryptor) {
|
||||
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||
Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
request.share = Runnable {
|
||||
decryptor.shareKeysWithDevice(request)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||
}
|
||||
request.ignore = Runnable {
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
// if the device is verified already, share the keys
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||
if (device != null) {
|
||||
if (device.isVerified) {
|
||||
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
|
||||
request.share?.run()
|
||||
return
|
||||
}
|
||||
|
||||
if (device.isBlocked) {
|
||||
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// As per config we automatically discard untrusted devices request
|
||||
if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
|
||||
Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
|
||||
// At this point the device is unknown, we don't want to bother user with that
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
// Pass to application layer to decide what to do
|
||||
onRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
|
||||
val secretName = request.secretName ?: return Unit.also {
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name")
|
||||
}
|
||||
|
||||
val userId = request.userId
|
||||
if (userId == null || credentials.userId != userId) {
|
||||
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
val deviceId = request.deviceId
|
||||
?: return Unit.also {
|
||||
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
||||
?: return Unit.also {
|
||||
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
|
||||
if (!device.isVerified || device.isBlocked) {
|
||||
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
return
|
||||
}
|
||||
|
||||
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
|
||||
|
||||
// Should SDK always Silently reject any request for the master key?
|
||||
when (secretName) {
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
|
||||
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
|
||||
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
|
||||
?.let {
|
||||
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
|
||||
}
|
||||
else -> null
|
||||
}?.let { secretValue ->
|
||||
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
|
||||
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
|
||||
val params = SendGossipWorker.Params(
|
||||
sessionId = sessionId,
|
||||
secretValue = secretValue,
|
||||
request = request
|
||||
)
|
||||
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
} else {
|
||||
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
|
||||
|
||||
request.ignore = Runnable {
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
}
|
||||
|
||||
request.share = { secretValue ->
|
||||
|
||||
val params = SendGossipWorker.Params(
|
||||
sessionId = userId,
|
||||
secretValue = secretValue,
|
||||
request = request
|
||||
)
|
||||
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
|
||||
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
|
||||
}
|
||||
|
||||
onShareRequest(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onRoomKeyRequest
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
for (listener in gossipingRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequest(request)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onRoomKeyRequest() failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for a value to the listeners, and take the first one
|
||||
*/
|
||||
private fun onShareRequest(request: IncomingSecretShareRequest) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
for (listener in gossipingRequestListeners) {
|
||||
try {
|
||||
if (listener.onSecretShareRequest(request)) {
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## GOSSIP onRoomKeyRequest() failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Not handled, ignore
|
||||
request.ignore?.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
for (listener in gossipingRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequestCancellation(request)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
gossipingRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
|
||||
}
|
||||
}
|
@@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.crypto
|
||||
|
||||
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.internal.crypto.model.rest.RoomKeyShareCancellation
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequestCancellation describes the incoming room key cancellation.
|
||||
* IncomingRequestCancellation describes the incoming room key cancellation.
|
||||
*/
|
||||
data class IncomingRoomKeyRequestCancellation(
|
||||
data class IncomingRequestCancellation(
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
@@ -37,22 +37,24 @@ data class IncomingRoomKeyRequestCancellation(
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
override val requestId: String? = null
|
||||
) : IncomingRoomKeyRequestCommon {
|
||||
override val requestId: String? = null,
|
||||
override val localCreationTimestamp: Long?
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? {
|
||||
fun fromEvent(event: Event): IncomingRequestCancellation? {
|
||||
return event.getClearContent()
|
||||
.toModel<RoomKeyShareCancellation>()
|
||||
.toModel<ShareRequestCancellation>()
|
||||
?.let {
|
||||
IncomingRoomKeyRequestCancellation(
|
||||
IncomingRequestCancellation(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId
|
||||
requestId = it.requestId,
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
@@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest(
|
||||
*/
|
||||
val requestBody: RoomKeyRequestBody? = null,
|
||||
|
||||
val state: GossipingRequestState = GossipingRequestState.NONE,
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@@ -56,8 +58,9 @@ data class IncomingRoomKeyRequest(
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null
|
||||
) : IncomingRoomKeyRequestCommon {
|
||||
var ignore: Runnable? = null,
|
||||
override val localCreationTimestamp: Long?
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
@@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId,
|
||||
requestBody = it.body ?: RoomKeyRequestBody()
|
||||
requestBody = it.body ?: RoomKeyRequestBody(),
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,203 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
private val receivedRoomKeyRequests = ArrayList<IncomingRoomKeyRequest>()
|
||||
private val receivedRoomKeyRequestCancellations = ArrayList<IncomingRoomKeyRequestCancellation>()
|
||||
|
||||
// the listeners
|
||||
private val roomKeysRequestListeners: MutableSet<RoomKeysRequestListener> = HashSet()
|
||||
|
||||
init {
|
||||
receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we get an m.room_key_request event
|
||||
* It must be called on CryptoThread
|
||||
*
|
||||
* @param event the announcement event.
|
||||
*/
|
||||
fun onRoomKeyRequestEvent(event: Event) {
|
||||
when (val roomKeyShareAction = event.getClearContent()?.get("action") as? String) {
|
||||
RoomKeyShare.ACTION_SHARE_REQUEST -> IncomingRoomKeyRequest.fromEvent(event)?.let { receivedRoomKeyRequests.add(it) }
|
||||
RoomKeyShare.ACTION_SHARE_CANCELLATION -> IncomingRoomKeyRequestCancellation.fromEvent(event)?.let { receivedRoomKeyRequestCancellations.add(it) }
|
||||
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action $roomKeyShareAction")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any m.room_key_request events which were queued up during the
|
||||
* current sync.
|
||||
* It must be called on CryptoThread
|
||||
*/
|
||||
fun processReceivedRoomKeyRequests() {
|
||||
val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
|
||||
receivedRoomKeyRequests.clear()
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
val body = request.requestBody
|
||||
val roomId = body!!.roomId
|
||||
val alg = body.algorithm
|
||||
|
||||
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||
if (userId == null || credentials.userId != userId) {
|
||||
// TODO: determine if we sent this device the keys already: in
|
||||
Timber.w("## 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?
|
||||
// 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)
|
||||
if (null == decryptor) {
|
||||
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
|
||||
continue
|
||||
}
|
||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
|
||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
request.share = Runnable {
|
||||
decryptor.shareKeysWithDevice(request)
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
request.ignore = Runnable {
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
// if the device is verified already, share the keys
|
||||
val device = cryptoStore.getUserDevice(userId, deviceId!!)
|
||||
if (device != null) {
|
||||
if (device.isVerified) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
request.share?.run()
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.isBlocked) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If cross signing is available on account we automatically discard untrust devices request
|
||||
if (cryptoStore.getMyCrossSigningInfo() != null) {
|
||||
// At this point the device is unknown, we don't want to bother user with that
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
|
||||
cryptoStore.storeIncomingRoomKeyRequest(request)
|
||||
onRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
||||
|
||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
||||
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
|
||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
||||
this.receivedRoomKeyRequestCancellations.clear()
|
||||
}
|
||||
}
|
||||
|
||||
if (null != receivedRoomKeyRequestCancellations) {
|
||||
for (request in receivedRoomKeyRequestCancellations!!) {
|
||||
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
|
||||
+ ":" + request.deviceId + " id " + request.requestId)
|
||||
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
onRoomKeyRequestCancellation(request)
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onRoomKeyRequest
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequest(request)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onRoomKeyRequest() failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
private fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequestCancellation(request)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onRoomKeyRequestCancellation() failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
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.internal.crypto.model.rest.SecretShareRequest
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||
*/
|
||||
data class IncomingSecretShareRequest(
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
override val userId: String? = null,
|
||||
|
||||
/**
|
||||
* The device id
|
||||
*/
|
||||
override val deviceId: String? = null,
|
||||
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
override val requestId: String? = null,
|
||||
|
||||
/**
|
||||
* The request body
|
||||
*/
|
||||
val secretName: String? = null,
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@Transient
|
||||
var share: ((String) -> Unit)? = null,
|
||||
|
||||
/**
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null,
|
||||
|
||||
override val localCreationTimestamp: Long?
|
||||
|
||||
) : IncomingShareRequestCommon {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
fun fromEvent(event: Event): IncomingSecretShareRequest? {
|
||||
return event.getClearContent()
|
||||
.toModel<SecretShareRequest>()
|
||||
?.let {
|
||||
IncomingSecretShareRequest(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
requestId = it.requestId,
|
||||
secretName = it.secretName,
|
||||
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
interface IncomingRoomKeyRequestCommon {
|
||||
interface IncomingShareRequestCommon {
|
||||
/**
|
||||
* The user id
|
||||
*/
|
||||
@@ -31,4 +31,6 @@ interface IncomingRoomKeyRequestCommon {
|
||||
* The request id
|
||||
*/
|
||||
val requestId: String?
|
||||
|
||||
val localCreationTimestamp: Long?
|
||||
}
|
@@ -59,9 +59,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||
var deviceEd25519Key: String? = null
|
||||
private set
|
||||
|
||||
// The OLM lib account instance.
|
||||
private var olmAccount: OlmAccount? = null
|
||||
|
||||
// The OLM lib utility instance.
|
||||
private var olmUtility: OlmUtility? = null
|
||||
|
||||
@@ -86,19 +83,10 @@ internal class MXOlmDevice @Inject constructor(
|
||||
|
||||
init {
|
||||
// Retrieve the account from the store
|
||||
olmAccount = store.getAccount()
|
||||
|
||||
if (null == olmAccount) {
|
||||
Timber.v("MXOlmDevice : create a new olm account")
|
||||
// Else, create it
|
||||
try {
|
||||
olmAccount = OlmAccount()
|
||||
store.storeAccount(olmAccount!!)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||
}
|
||||
} else {
|
||||
Timber.v("MXOlmDevice : use an existing account")
|
||||
try {
|
||||
store.getOrCreateOlmAccount()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -109,13 +97,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||
}
|
||||
|
||||
try {
|
||||
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||
deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
||||
}
|
||||
|
||||
try {
|
||||
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||
deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
||||
}
|
||||
@@ -126,7 +114,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
||||
try {
|
||||
return olmAccount!!.oneTimeKeys()
|
||||
return store.getOlmAccount().oneTimeKeys()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getOneTimeKeys() : failed")
|
||||
}
|
||||
@@ -138,14 +126,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return The maximum number of one-time keys the olm account can store.
|
||||
*/
|
||||
fun getMaxNumberOfOneTimeKeys(): Long {
|
||||
return olmAccount?.maxOneTimeKeys() ?: -1
|
||||
return store.getOlmAccount().maxOneTimeKeys()
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the instance
|
||||
*/
|
||||
fun release() {
|
||||
olmAccount?.releaseAccount()
|
||||
olmUtility?.releaseUtility()
|
||||
}
|
||||
|
||||
@@ -157,7 +144,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
fun signMessage(message: String): String? {
|
||||
try {
|
||||
return olmAccount!!.signMessage(message)
|
||||
return store.getOlmAccount().signMessage(message)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## signMessage() : failed")
|
||||
}
|
||||
@@ -170,8 +157,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
fun markKeysAsPublished() {
|
||||
try {
|
||||
olmAccount!!.markOneTimeKeysAsPublished()
|
||||
store.storeAccount(olmAccount!!)
|
||||
store.getOlmAccount().markOneTimeKeysAsPublished()
|
||||
store.saveOlmAccount()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## markKeysAsPublished() : failed")
|
||||
}
|
||||
@@ -184,8 +171,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
fun generateOneTimeKeys(numKeys: Int) {
|
||||
try {
|
||||
olmAccount!!.generateOneTimeKeys(numKeys)
|
||||
store.storeAccount(olmAccount!!)
|
||||
store.getOlmAccount().generateOneTimeKeys(numKeys)
|
||||
store.saveOlmAccount()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## generateOneTimeKeys() : failed")
|
||||
}
|
||||
@@ -205,7 +192,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
|
||||
try {
|
||||
olmSession = OlmSession()
|
||||
olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey)
|
||||
olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
|
||||
|
||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
||||
|
||||
@@ -245,7 +232,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
try {
|
||||
try {
|
||||
olmSession = OlmSession()
|
||||
olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext)
|
||||
olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## createInboundSession() : the session creation failed")
|
||||
return null
|
||||
@@ -254,8 +241,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||
Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
||||
|
||||
try {
|
||||
olmAccount!!.removeOneTimeKeys(olmSession)
|
||||
store.storeAccount(olmAccount!!)
|
||||
store.getOlmAccount().removeOneTimeKeys(olmSession)
|
||||
store.saveOlmAccount()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||
}
|
||||
@@ -654,7 +641,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
if (null != timeline) {
|
||||
if (timeline?.isNotBlank() == true) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
@@ -770,7 +757,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
return session
|
||||
}
|
||||
} else {
|
||||
Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
interface OutgoingGossipingRequest {
|
||||
var recipients: Map<String, List<String>>
|
||||
var requestId: String
|
||||
var state: OutgoingGossipingRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// var cancellationTxnId: String?
|
||||
}
|
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
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.di.SessionId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val gossipingWorkManager: GossipingWorkManager) {
|
||||
|
||||
/**
|
||||
* Send off a room key request, if we haven't already done so.
|
||||
*
|
||||
*
|
||||
* The `requestBody` is compared (with a deep-equality check) against
|
||||
* previous queued or sent requests and if it matches, no change is made.
|
||||
* Otherwise, a request is added to the pending list, and a job is started
|
||||
* in the background to send it.
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param recipients recipients
|
||||
*/
|
||||
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
|
||||
// Don't resend if it's already done, you need to cancel first (reRequest)
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
|
||||
return@launch
|
||||
}
|
||||
|
||||
sendOutgoingGossipingRequest(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// A bit dirty, but for better stability give other party some time to mark
|
||||
// devices trusted :/
|
||||
delay(1500)
|
||||
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||
// TODO check if there is already one that is being sent?
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||
Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
||||
return@launch
|
||||
}
|
||||
|
||||
sendOutgoingGossipingRequest(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param andResend true to resend the key request
|
||||
*/
|
||||
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
?: // no request was made for this key
|
||||
return Unit.also {
|
||||
Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request")
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestCancellation(req, andResend)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
|
||||
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
|
||||
|
||||
val params = SendGossipRequestWorker.Params(
|
||||
sessionId = sessionId,
|
||||
keyShareRequest = request as? OutgoingRoomKeyRequest,
|
||||
secretShareRequest = request as? OutgoingSecretRequest
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
|
||||
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
|
||||
Timber.v("$request")
|
||||
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||
|
||||
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
|
||||
if (resend) {
|
||||
val reSendParams = SendGossipRequestWorker.Params(
|
||||
sessionId = sessionId,
|
||||
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
|
||||
)
|
||||
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
|
||||
gossipingWorkManager.postWork(reSendWorkRequest)
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,22 +17,26 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
class OutgoingRoomKeyRequest(
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OutgoingRoomKeyRequest(
|
||||
// RequestBody
|
||||
var requestBody: RoomKeyRequestBody?, // list of recipients for the request
|
||||
var recipients: List<Map<String, String>>, // Unique id for this request. Used for both
|
||||
var requestBody: RoomKeyRequestBody?,
|
||||
// list of recipients for the request
|
||||
override var recipients: Map<String, List<String>>,
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
var requestId: String, // current state of this request
|
||||
var state: RequestState) {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
var cancellationTxnId: String? = null
|
||||
override var requestId: String, // current state of this request
|
||||
override var state: OutgoingGossipingRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// override var cancellationTxnId: String? = null
|
||||
) : OutgoingGossipingRequest {
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
@@ -53,66 +57,4 @@ class OutgoingRoomKeyRequest(
|
||||
get() = if (null != requestBody) {
|
||||
requestBody!!.sessionId
|
||||
} else null
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
*
|
||||
* The state machine looks like:
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* V
|
||||
* UNSENT -----------------------------+
|
||||
* | |
|
||||
* | (send successful) | (cancellation requested)
|
||||
* V |
|
||||
* SENT |
|
||||
* |-------------------------------- | --------------+
|
||||
* | | |
|
||||
* | | | (cancellation requested with intent
|
||||
* | | | to resend a new request)
|
||||
* | (cancellation requested) | |
|
||||
* V | V
|
||||
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* | | |
|
||||
* | (cancellation sent) | | (cancellation sent. Create new request
|
||||
* | | | in the UNSENT state)
|
||||
* V | |
|
||||
* (deleted) <---------------------------+----------------+
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
enum class RequestState {
|
||||
/**
|
||||
* request not yet sent
|
||||
*/
|
||||
UNSENT,
|
||||
/**
|
||||
* request sent, awaiting reply
|
||||
*/
|
||||
SENT,
|
||||
/**
|
||||
* reply received, cancellation not yet sent
|
||||
*/
|
||||
CANCELLATION_PENDING,
|
||||
/**
|
||||
* Cancellation not yet sent, once sent, a new request will be done
|
||||
*/
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
/**
|
||||
* sending failed
|
||||
*/
|
||||
FAILED;
|
||||
|
||||
companion object {
|
||||
fun from(state: Int) = when (state) {
|
||||
0 -> UNSENT
|
||||
1 -> SENT
|
||||
2 -> CANCELLATION_PENDING
|
||||
3 -> CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
else /*4*/ -> FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,320 +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
|
||||
|
||||
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
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
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.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
// running
|
||||
private var isClientRunning: Boolean = false
|
||||
|
||||
// transaction counter
|
||||
private var txnCtr: Int = 0
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequestsTimer
|
||||
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
fun start() {
|
||||
isClientRunning = true
|
||||
startTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
fun stop() {
|
||||
isClientRunning = false
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Make up a new transaction id
|
||||
*
|
||||
* @return {string} a new, unique, transaction id
|
||||
*/
|
||||
private fun makeTxnId(): String {
|
||||
return "m" + System.currentTimeMillis() + "." + txnCtr++
|
||||
}
|
||||
|
||||
/**
|
||||
* Send off a room key request, if we haven't already done so.
|
||||
*
|
||||
*
|
||||
* The `requestBody` is compared (with a deep-equality check) against
|
||||
* previous queued or sent requests and if it matches, no change is made.
|
||||
* Otherwise, a request is added to the pending list, and a job is started
|
||||
* in the background to send it.
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param recipients recipients
|
||||
*/
|
||||
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List<Map<String, String>>) {
|
||||
val req = cryptoStore.getOrAddOutgoingRoomKeyRequest(
|
||||
OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
|
||||
if (req?.state == OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param andResend true to resend the key request
|
||||
*/
|
||||
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
?: // no request was made for this key
|
||||
return
|
||||
|
||||
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
|
||||
|
||||
when (req.state) {
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
// nothing to do here
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.FAILED -> {
|
||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.SENT -> {
|
||||
if (andResend) {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
} else {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
}
|
||||
req.cancellationTxnId = makeTxnId()
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
||||
sendOutgoingRoomKeyRequestCancellation(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
||||
*/
|
||||
private fun startTimer() {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
return
|
||||
}
|
||||
BACKGROUND_HANDLER.postDelayed(Runnable {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning.set(true)
|
||||
sendOutgoingRoomKeyRequests()
|
||||
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
// look for and send any queued requests. Runs itself recursively until
|
||||
// there are no more requests, or there is an error (in which case, the
|
||||
// timer will be restarted before the promise resolves).
|
||||
private fun sendOutgoingRoomKeyRequests() {
|
||||
if (!isClientRunning) {
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
|
||||
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
|
||||
setOf(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
||||
|
||||
if (null == outgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.state) {
|
||||
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest)
|
||||
} else {
|
||||
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody
|
||||
+ " from " + request.recipients + " id " + request.requestId)
|
||||
|
||||
val requestMessage = RoomKeyShareRequest(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.requestId,
|
||||
body = request.requestBody
|
||||
)
|
||||
|
||||
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
|
||||
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
|
||||
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
|
||||
} else {
|
||||
request.state = state
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest succeed")
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.SENT)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## sendOutgoingRoomKeyRequest failed")
|
||||
onDone(OutgoingRoomKeyRequest.RequestState.FAILED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody
|
||||
+ " to " + request.recipients
|
||||
+ " cancellation id " + request.cancellationTxnId)
|
||||
|
||||
val roomKeyShareCancellation = RoomKeyShareCancellation(
|
||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
||||
requestId = request.cancellationTxnId
|
||||
)
|
||||
|
||||
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
|
||||
private fun onDone() {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done")
|
||||
val resend = request.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
|
||||
onDone()
|
||||
|
||||
// Resend the request with a new ID
|
||||
if (resend) {
|
||||
sendRoomKeyRequest(request.requestBody, request.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## sendOutgoingRoomKeyRequestCancellation failed")
|
||||
onDone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a SendToDeviceObject to a list of recipients
|
||||
*
|
||||
* @param message the message
|
||||
* @param recipients the recipients.
|
||||
* @param transactionId the transaction id
|
||||
* @param callback the asynchronous callback.
|
||||
*/
|
||||
private fun sendMessageToDevices(message: Any,
|
||||
recipients: List<Map<String, String>>,
|
||||
transactionId: String?,
|
||||
callback: MatrixCallback<Unit>) {
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
for (recipient in recipients) {
|
||||
// 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)) {
|
||||
this.callback = callback
|
||||
this.callbackThread = TaskThread.CALLER
|
||||
this.executionThread = TaskThread.CALLER
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
|
||||
|
||||
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
class OutgoingSecretRequest(
|
||||
// Secret Name
|
||||
val secretName: String?,
|
||||
// list of recipients for the request
|
||||
override var recipients: Map<String, List<String>>,
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
override var requestId: String,
|
||||
// current state of this request
|
||||
override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
}
|
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
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.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipRequestWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
val keyShareRequest: OutgoingRoomKeyRequest? = null,
|
||||
val secretShareRequest: OutgoingSecretRequest? = null
|
||||
)
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
val eventType: String
|
||||
val requestId: String
|
||||
when {
|
||||
params.keyShareRequest != null -> {
|
||||
eventType = EventType.ROOM_KEY_REQUEST
|
||||
requestId = params.keyShareRequest.requestId
|
||||
val toDeviceContent = RoomKeyShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.keyShareRequest.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
body = params.keyShareRequest.requestBody
|
||||
)
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
params.secretShareRequest != null -> {
|
||||
eventType = EventType.REQUEST_SECRET
|
||||
requestId = params.secretShareRequest.requestId
|
||||
val toDeviceContent = SecretShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.secretShareRequest.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
secretName = params.secretShareRequest.secretName
|
||||
)
|
||||
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
return Result.success(errorOutputData).also {
|
||||
Timber.e("Unknown empty gossiping request: $params")
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING)
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = eventType,
|
||||
contentMap = contentMap,
|
||||
transactionId = localId
|
||||
)
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||
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.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
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.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SendGossipWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: CoroutineWorker(context, params) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
val sessionId: String,
|
||||
val secretValue: String,
|
||||
val request: IncomingSecretShareRequest
|
||||
)
|
||||
|
||||
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var credentials: Credentials
|
||||
@Inject lateinit var messageEncrypter: MessageEncrypter
|
||||
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.success(errorOutputData)
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
// TODO, can this happen? should I update local echo?
|
||||
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
|
||||
}
|
||||
sessionComponent.inject(this)
|
||||
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
val eventType: String = EventType.SEND_SECRET
|
||||
|
||||
val toDeviceContent = SecretSendEventContent(
|
||||
requestId = params.request.requestId ?: "",
|
||||
secretValue = params.secretValue
|
||||
)
|
||||
|
||||
val requestingUserId = params.request.userId ?: ""
|
||||
val requestingDeviceId = params.request.deviceId ?: ""
|
||||
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
|
||||
?: return Result.success(errorOutputData).also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
|
||||
}
|
||||
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
|
||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
return Result.success(errorOutputData).also {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Timber.e("no session with this device, probably because there were no one-time keys.")
|
||||
}
|
||||
}
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.SEND_SECRET,
|
||||
"content" to toDeviceContent.toContent()
|
||||
)
|
||||
|
||||
try {
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
|
||||
}
|
||||
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
try {
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = EventType.ENCRYPTED,
|
||||
contentMap = sendToDeviceMap,
|
||||
transactionId = localId
|
||||
)
|
||||
)
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
|
||||
return Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
return if (exception.shouldBeRetried()) {
|
||||
Result.retry()
|
||||
} else {
|
||||
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
|
||||
Result.success(errorOutputData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user